> 文章列表 > 计算机网络 - 网络通信 (TCP与UDP)

计算机网络 - 网络通信 (TCP与UDP)

计算机网络 - 网络通信 (TCP与UDP)


前言

本篇通过了解套接字,TCP与UDP协议的特点,使用UDP的api套接字与TCP的api套接字进行回显程序的网络通信,如有错误,请在评论区指正,让我们一起交流,共同进步!


文章目录

    • 前言
    • 1. 认识网络通信需要的套接字 - socket
    • 2. UDP的api
      • 2.1 DatagramSocket
      • 2.2 DatagramPacket
      • 2.3 根据DatagramSocket 与 DatagramPacket 模拟网络通信
      • ==1.模拟实现UDP回显服务器==
      • ==2.模拟实现客户端==
    • 3. TCP的api
      • 3.1 ServerSocet
      • 3.2 Socket
      • 3.3 模拟实现TCP回显程序
      • 3.3.2 模拟实现客户端
    • 总结

本文开始

1. 认识网络通信需要的套接字 - socket

1.1 认识套接字
套接字:应用层调用传输层,操作系统内核给应用层提供一组用来网络编程的api,这就称为 socket api = 套接字api;

针对TCP与UDP协议,这里主要认识关于UDP的api 与 TCP的api 来用于网络通信;
认识api先认识 TCP 与 UDP 的特点吧!

1.2 TCP 与 UDP的主要特点
UDP:

① 无连接:使用UDP通信,双方不需要保存对端(客户端与服务器)的信息;
例如:发短信,不需要记录对方的电话,对方也不需要记录你的电话 (不用建立连接),直接根据电话号直接发送信息;
② 不可靠传输:一方发送消息,不需要知道另一方是否接收到了信息;
③ 面向数据报:以一个UDP数据报为基本单位读写数据;- 有限制;
④ 全双工:一条路径双向通信;
【注】有无连接:通信双方是否记录对方的信息;
有连接:通信双方需要记录对方的信息;
无连接:通信双方不需要记录对方的信息;

TCP:

① 有连接:使用TCP通信,双方需要保存对端的信息;
例如:打电话,一方打电话,另一方需要接通电话 (建立连接),双方才能通信;
② 可靠传输:一方发送信息,尽量保证信息发送到了另一方(但也不能保证一定成功,只是尽全力);
③ 面向字节流:以一个字节为传输基本单位 - 读写数据比较灵活
④ 全双工:双向通信;
【注】半双工:在一条路径上,A方向B方发送消息,B只能等待A发完消息后才能向A发送信息;
全双工:A向B发送消息的同时,B也可以向A发送消息;

2. UDP的api

认识socket对象:
socket 是系统中一个特殊的文件进行网络通信,需要socket文件对象,通过socket文件对象,间接操作网卡;

2.1 DatagramSocket

Datagram: 数据报;Socket: socket对象;

计算机网络 - 网络通信 (TCP与UDP)
构造方法:
计算机网络 - 网络通信 (TCP与UDP)

【注】socket对象可以被客户端 与 服务器使用;服务器使用socket需要指定关联一个端口号 - 端口号不变才能方便客户端找到服务器;客户端使用socket不需要手动指定,系统会自动分配空闲的端口号;

2.2 DatagramPacket

DatagramPacket: udp数据报对象;

计算机网络 - 网络通信 (TCP与UDP)
构造方法
计算机网络 - 网络通信 (TCP与UDP)

2.3 根据DatagramSocket 与 DatagramPacket 模拟网络通信

回显程序:自己给自己发送并接收信息;

1.模拟实现UDP回显服务器

服务器的核心工作:
① 读取客户端请求并解析
② 根据请求计算响应
③ 把响应写回到客户端

首先构造socket对象,从而间接操作网卡进行读取;
再构造带有一个参数的构造方法,给服务器分配端口;

	private DatagramSocket socket = null;//绑定端口号不一定会成功;- 端口号可能被别的进程占用//同一主机,同一时刻,一个端口只能被一个进程所占用public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}

服务器主要逻辑过程:
① 读取客户端请求:使用receive()方法, 需要构造requestPacket数据报把数据读取到数据报中;
② 知道客户端输入的是字符,为方便操作将数据报(字节数组)构造为字符串;
③ 根据请求计算具体响应 - 根据具体情况修改;
④ 将响应发送回客户端,根据响应是字符串,构造数据报此时需要指定客户端的IP和端口;

public void start() throws IOException {System.out.println("服务器启动!");//服务器执行不可能只执行一个请求,需要执行多个请求就用到了循环while (true) {//1.读取客户端请求DatagramPacket requestPacket = new DatagramPacket(new byte[5000],5000);//receive时输出型参数:在其中传入空的packet对象,然后receive方法内部就会把参数packet填充;// -》从网卡读取内容,写的数据报中;socket.receive(requestPacket);//服务器先启动,如果此时没有客户端请求,就会阻塞等待,等待客户端发送数据过来;//解析 -> 此处为了方便后续操作,拿字节数组构造成字符串String request = new String(requestPacket.getData(),0,requestPacket.getLength());//getLength : 获取真实长度;//2.根据请求计算响应String response = process(request);//3.把响应写回客户端 - 写回网卡DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes(StandardCharsets.UTF_8).length,//requestPacket是客户端得到的,getSocketAddress得到的就是客户端的IP和端口requestPacket.getSocketAddress());socket.send(responsePacket);//4.打印日志System.out.printf("[%s : %d] request: %s, response: %s\\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}//回显服务器,写什么,就返回什么;// 以后有其他服务器,可以根据具体请求重新构造响应;private String process(String request) {return request;}

【注】receive时输出型参数:在其中传入空的packet对象,然后receive方法内部就会把参数packet填充;

服务器总代码:

public class UdpEchoServer {private DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while (true) {//1.读取客户端请求DatagramPacket requestPacket = new DatagramPacket(new byte[5000],5000);socket.receive(requestPacket);String request = new String(requestPacket.getData(),0,requestPacket.getLength());//2.根据请求计算响应String response = process(request);//3.把响应写回客户端 - 写回网卡DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes(StandardCharsets.UTF_8).length,requestPacket.getSocketAddress());socket.send(responsePacket);//4.打印System.out.printf("[%s : %d] request: %s, response: %s\\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer udpEchoServer = new UdpEchoServer(9090);udpEchoServer.start();}
}

2.模拟实现客户端

首先构造socket对象,方便操作读取网卡;
再构造带两个参数的构造方法:指定服务器的IP和端口,让客户端容易找到服务器;

public class UdpEchoClient {private DatagramSocket socket = null;private String serverIP;private int serverPort;//客户端启动需要知道服务器的位置,根据服务器的IP和端口public UdpEchoClient(String serverIP, int serverPort) throws SocketException {//客户端不用指定端口,会自动分配空闲端口;socket = new DatagramSocket();//为了方便之后使用this.serverIP = serverIP;this.serverPort = serverPort;}
}

客户端执行逻辑:
① 等待客户端输入请求;
② 把请求发送给服务器,需构造数据报并指定服务器IP和端口;
③ 客户端接收响应,构造数据报使用receive()接收;
④ 打印响应结果,再将数据报转换为字符串
代码如下:

     public void start() throws IOException {Scanner scanner = new Scanner(System.in);//多次交互while (true) {System.out.print(" -> ");//1.等待控制台输入String request = scanner.next();//2.把请求发送到服务器DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,//通过静态发送InetAddress设置IP地址;        InetAddress.getByName(serverIP),serverPort);socket.send(requestPacket);//3.客户端接收响应DatagramPacket responsePacket = new DatagramPacket(new byte[5000],5000);socket.receive(responsePacket);String response = new String(responsePacket.getData(),0,responsePacket.getLength());//4.打印日志System.out.printf("request: %s, response: %s\\n", request,response);}}

【注】构造数据报DatagramPacket requestPacket:请求的起始0位置和请求的最后长度length; 需要服务器的IP和端口;
区别:SocketAddress:包含IP和端口;InetSocketAddress设置 IP 再单独设置端口;

客户端总代码:

public class UdpEchoClient {private DatagramSocket socket = null;private String serverIP;private int serverPort;//客户端启动需要知道服务器的位置,根据服务器的IP和端口public UdpEchoClient(String serverIP, int serverPort) throws SocketException {//客户端不用指定端口,会自动分配空闲端口;socket = new DatagramSocket();this.serverIP = serverIP;this.serverPort = serverPort;}public void start() throws IOException {Scanner scanner = new Scanner(System.in);//多次交互while (true) {System.out.print(" -> ");//1.等待控制台输入String request = scanner.next();//2.把请求发送到服务器DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIP),serverPort);socket.send(requestPacket);//3.客户端接收响应DatagramPacket responsePacket = new DatagramPacket(new byte[5000],5000);socket.receive(responsePacket);String response = new String(responsePacket.getData(),0,responsePacket.getLength());//4.打印日志System.out.printf("request: %s, response: %s\\n", request,response);}}public static void main(String[] args) throws IOException {UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8080);udpEchoClient.start();}
}

客户端与服务器的网络通信过程:
① 服务器先启动,执行到receive方法后就进行阻塞,等待客户端传输数据 / 请求;
② 客户端运行,等待控制台输入数据并读取,包装成数据报后发送send() 给服务器;
③ 客户端发送数据报后,继续向下执行,到receive方法会阻塞等待服务器返回响应;
     在客户端发送请求后的后的同时,服务器会读取客户端的请求,根据请求生成响应,把响应包装成数据报再次send发送给客户端,并打印日志;
④客户端收到服务器的响应,就会解除receive的阻塞,把响应转换为字符串进行打印;
      此时服务器会进入下一轮循环,再次等待新的客户端请求;
⑤ 客户端打印完,进入下一轮,再次等待控制台输入新的数据请求;
【注】本机IP地址:127.0.0.1; - 自己与自己发信息;

3. TCP的api

3.1 ServerSocet

ServerSocket: 服务器使用的
ServerSocket构造时需要让服务器绑定一个指定的端口,方便客户端能够找到;

计算机网络 - 网络通信 (TCP与UDP)
构造方法:
计算机网络 - 网络通信 (TCP与UDP)

3.2 Socket

Socket: 服务器 和 客户端都可以使用

计算机网络 - 网络通信 (TCP与UDP)
构造方法
计算机网络 - 网络通信 (TCP与UDP)

3.3 模拟实现TCP回显程序

3.3.1 模拟实现回显服务器
了解InputStream, OutputStream操作字节流对象;
InputStream: 读数据,相当于从网卡读数据;
OutputSteam: 写数据,相当于从网卡发送数据;

首先类TcpEchoServer, 类中创建ServerSocket变量,方便以后操作网卡读取数据;
构造一个参数的构造方法,构造serverSocket对象并指定端口号;

public class TcpEchoServer {private ServerSocket serverSocket = null;public TcpEchoServer(int port) throws IOException {//与之前的UDP的DatagramSocket中的socket一样,需要绑定端口serverSocket = new ServerSocket(port);}
}

然后服务器中的主逻辑:
① 先调用serverSocket的accept方法,等待与客户端建立连接

计算机网络 - 网络通信 (TCP与UDP)

② 建立连接后,执行客户端连接的方法processConnectin();

核心逻辑代码:

public void start() throws IOException {System.out.println("服务器启动!");while (true) {//监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象Socket clientSocket = serverSocket.accept();Thread t = new Thread(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});t.start();}

为什么使用线程呢?
服务器启动后,如果不使用线程,当一个客户端占用processConnection(clientSocket)方法,有其他客户端想要再次使用processConnection(clientSocket)方法,就需要一直等待;使用多线程,来一个客户端就创建一个新线程来执行processConnection(clientSocket)方法,就可以多线程使用了;

优化为线程池:
线程的频繁创建和销毁也会带来资源销毁,这里可以优化为线程池;将线程放入线程池,减少线程的创建和销毁;

public void start() throws IOException {System.out.println("服务器启动!");ExecutorService executorService = Executors.newCachedThreadPool();while (true) {//监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象Socket clientSocket = serverSocket.accept();executorService.submit(new Runnable() {@Overridepublic void run() {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}

processConnection(clientSocket)方法具体执行内容:
代码逻辑:
① 读取数据需要对网卡的操作,就用到了InputStream,OutputStream; - try() 中允许写多个流对象;
② 上述字节流,不能清楚读取到哪是一个请求;这里约定服务器是字符串请求,请求之间用\\n分隔,所以包装为字符流Scanner , PrintWrite 方便读写数据; - 根据具体情况可更改;
③ 执行客户端请求,与UDP相似,但这里需要判断客户端是否断开连接使用hasNext()方法;
【注】 根据请求计算完响应,发送响应时也应该带上 \\n 让客户端分清楚请求 - 使用println加 \\n ;(客户端也一样)
clientSocket 是文件,不用了需要关闭;而ServerSocket不需要,它的生命周期长;(循环创建clientSocket,有一个客户端就创建一个,为防止文件资源泄露,需要close()关闭一下文件;)

代码如下:

private void processConnection(Socket clientSocket) {System.out.printf("客户端上线![%s : %d]\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());try(InputStream inputStrem = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//为了后续操作,字节流包装为字符流 - 可根据具体情况修改;Scanner scanner = new Scanner(inputStrem);PrintWriter printWriter = new PrintWriter(outputStream);//执行具体请求while (true) {//1.读取客户端请求//判断客户端是否关闭了if(!scanner.hasNext()) {System.out.printf("客户端下线![%s : %d]\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}//scanner.next读取换行结束,读取一段请求;String request = scanner.next();// 读到空白符结束;空白符:\\n,空格,制表符;//2.根据请求做出响应String response = process(request);//3.返回响应,响应中加上\\n,上述读取一段请求时next不读取\\n,这里发送时需要加上;printWriter.println(response);printWriter.flush();//刷新,将缓冲区数据立即发送//4.打印日志System.out.printf("[%s : %d] request: %s, response: %s\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(), request, response);}} catch (IOException e) {e.printStackTrace();} finally {clientSocket.close();}}

为什么InputStream,OutputStream;写到try括号里?
放在try()中,InputStream,OutputStream最后会自动关闭,不用手动close;

为什么需要printWriter.flush() ?
发送的数据,先会写入内存的发送缓冲区,等到缓冲区满了,才会写入网卡;这里使用flush(), 使数据立即写入网卡,不用等待缓冲区满;- 客户端与服务器相同,都需要手动flush()一下;

3.3.2 模拟实现客户端

首先创建TcpEchoClient类,类中创建Socekt操作网卡,创建两个参数的构造方法,让客户端能够找到服务器,与服务器来连接;

public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String rerverIP, int port) throws IOException {socket = new Socket(rerverIP,port);}
}

核心逻辑代码:
与UDP类似;

public void start() {Scanner scanner = new Scanner(System.in);try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {//包装为字符流PrintWriter printWriter = new PrintWriter(outputStream);Scanner scFromSocket = new Scanner(inputStream);//读取网卡请求while (true) {//1.等待控制台输入System.out.println(" --> ");String request = scanner.next();//这里next以分隔符\\n来分隔每段请求//2.发送请求给服务器printWriter.println(request);printWriter.flush();//3.接收服务器响应String response = scFromSocket.next();//读取字符流读取字符//4.打印日志System.out.printf("[%s : %d] request: %s, response: %s\\n",socket.getInetAddress().toString(),socket.getPort(),request,response);}} catch (IOException e) {e.printStackTrace();}

TCP客户端-服务器执行过程:
① 服务器先启动,阻塞在accept(),等待客户端连接
② 客户端启动,调用构造方法与服务器建立连接,此时服务器中accept()返回Socket对象;
③ 服务器执行processConnection方法,阻塞到读取客户端请求;
而客户端在调用构造方法后一直往下执行,阻塞等待用户输入,用户输入后读取数据;
④ 客户端读取用户数据发送给服务器,执行到要读取客户端响应后阻塞等待服务器返回响应;
⑤ 服务器接收请求后,读取请求,计算响应,并返回响应;
⑥ 服务器发送响应后重写循环等待下一轮客户端连接;
客户端收到响应,打印结果,再次循环,等待控制台输入;

TCP 客户端总代码:

public class TcpEchoServer {private ServerSocket serverSocket = null;public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");ExecutorService executorService = Executors.newCachedThreadPool();while (true) {//监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象Socket clientSocket = serverSocket.accept();executorService.submit(new Runnable() {@Overridepublic void run() {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}private void processConnection(Socket clientSocket) throws IOException {System.out.printf("客户端上线![%s : %d]\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());try(InputStream inputStrem = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//为了后续操作Scanner scanner = new Scanner(inputStrem);PrintWriter printWriter = new PrintWriter(outputStream);//执行具体请求while (true) {//1.读取客户端请求//判断客户端是否关闭了if(!scanner.hasNext()) {System.out.printf("客户端下线![%s : %d]\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}String request = scanner.next();//2.根据请求做出响应String response = process(request);//3.返回响应printWriter.println(response);printWriter.flush();//4.打印日志System.out.printf("[%s : %d] request: %s, response: %s\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(), request, response);}} catch (IOException e) {e.printStackTrace();} finally {clientSocket.close();}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer tcpEchoServer = new TcpEchoServer(8080);tcpEchoServer.start();}
}

总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!