这里是Themberfue
在上一节中,我们初始了网络的基础知识
在本节中,我们将通过代码来体会网络通信的过程~~~
TCP/UDP
在日常开发网络通信相关的代码时,程序员大多处在 TCP/IP五层模型 的最上层,也就是应用层。
在上一节的学习中,我们了解到应用层主要通过传输层所提供的接口进行编写相应的代码,这些接口都是操作系统提供的一组API,也就是 Socket API。
Socket 是对网络通信端点的抽象,用于在两个程序之间通过网络进行双向通信;在网络编程中,Socket 作为应用层与传输层之间的接口;它屏蔽了底层协议的复杂性,提供了一种统一的通信方式。
传输层中有两个协议是非常重要的,TCP协议 和 UDP协议。
TCP
TCP(传输控制协议,Transmission Control Protocol) ,主要特点是 有连接 、可靠传输 、面向字节流 、全双工。
有连接 :连接通常指的不是物理上的连接,而是逻辑上的连接,并不是两个事物通过链条连接在一起,而是通过某种抽象的东西将这两个事物所连接起来,使其有关系;在使用 TCP协议 进行网络通信前,通常需要双方保存对方的信息,使其知道谁是和他们建立连接的那个;通过保存对端的信息,双方就 "连接" 在一起了。
可靠传输 :在实际的网络通信过程中,需要经过需要节点或路由,那么所发送的数据不可能百分百到达对方,在传输途中,数据可能会因为某些不可抗力因素导致比特翻转(就是1变成0,0变成1),从而导致解析出的数据出现错误,也有可能整个数据在传输的过程丢失,也就是我们常说的 "丢包";可靠传输也就是尽可能的使发送出去的数据到达对方,利用某些特性,但不能做到百分百到达对方,只是一种 "可靠"。
面向字节流 :在发送数据或者读取数,也就是读写数据时,是以字节为单位的。面向字节流指数据作为一个连续的字节流进行传输,发送和接收方处理的是数据流的一部分。
全双工 :双工指通信双方可以同时发送和接收数据,即通信是双向的,且不受时序限制;通常需要两条独立的通信通道:一条负责发送,一条负责接收;数据传输效率高。
**TCP 更适合需要高可靠性、数据完整性且传输顺序敏感的场景:**网页浏览、文件传输、电子邮件、数据库连接。
UDP
UDP(用户数据报协议,User Datagram Protocol) ,主要特点是 无连接 、不可靠传输 、面向数据报 、全双工。
无连接:与有连接相反,双方在网络通信前,不会进行逻辑上的连接,通常通过代码来告诉该数据发送到哪个 IP 和 端口。
不可靠传输:数据在传输过程中是可能丢失或者出现错误的,UDP 协议在发送数据后,便不再管发出去的数据了,不管你有没有到达,我发出去就行了。
面向数据报 :在读写数据时,是以数据报为单位。面向数据报指数据以 **独立的消息包(数据报)**为单位进行传输,每个数据报都有明确的边界和独立性;每个数据报都是完整的单元,具有独立的头部和内容;数据报可以乱序到达,也可能丢失;不依赖于底层的可靠性机制,发送后不确认接收结果。
全双工 :既然介绍了全双工,那么肯定有半双工:半双工指通信双方可以双向发送和接收数据,但在同一时刻只能有一个方向的数据传输。共享同一条通信通道,时序严格;发送和接收需要交替进行,效率较低。
**UDP 适合对实时性要求高、容忍少量数据丢失的场景:**视频/语音通话、在线游戏、流媒体直播、简单的请求-响应服务。
UDP Socket
Java对操作系统提供的 SocketAPI 又进行了一层封装。
前面我们了解到,计算机的 "文件" 通常是一个相对广义的概念,文件也可以指代一些硬件,将这些硬件抽象为文件进行管理,以便于读写数据。
这里我们把网卡当作 socket文件,在操作网卡时,就跟操作文件一样,正常的读写操作就可以。
基于 UDP 封装的 SocketAPI:
DatagramSocket
|------------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建⼀个UDP数据报套接字的Socket,绑定到本机任意⼀个随机端口(⼀般用于客户端) |
| DatagramSocket(int port) | 创建⼀个UDP数据报套接字的Socket,绑定到本机指定的端口(⼀般用于服务端) |DatagramSocket提供的方法
|------------------------------------|---------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |DatagramPacket:表示一个完整的 UDP数据报,是 UDP Socket 发送和接收的数据报
|------------------------------|--------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
| int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
| byte[] getData() | 获取数据报中的数据 |InetSocketAddress(SocketAddress 的子类)
|-----------------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetAddress address,int port) | 创建⼀个Socket地址,包含IP地址和端口号 |
代码编写与讲解
UdpEchoServer
javaimport java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; /** * @author: Themberfue * @date: 2024/11/3 14:55 * @description: */ public class UdpEchoServer { private DatagramSocket socket = null; // 服务端的端口一般是指定的,不能经常改变 public UdpEchoServer(int port) throws SocketException { this.socket = new DatagramSocket(port); } public void start() throws IOException { // 启动服务器 System.out.println("服务器启动"); while (true) { // 循环一次,就相当于处理一次请求 // 1. 读取请求并解析 // 创建请求数据报 // DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组,将存储 UDP 的载荷部分 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); // 从网卡读取客户端发送来的请求 socket.receive(requestPacket); // 把读取到的二进制数据转化为字符串,只是构造有效的部分 String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); // 2. 根据请求,计算响应(服务器最关键的逻辑) String response = process(request); // 3. 把响应返回给客户端 // 创建响应数据报 // 根据 response 构造 DatagramPacket,发送给客户端 // 此处不能使用 response.length() DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); // 此处还不能直接发送,UDP 协议为无连接,没有保存对方的信息 // 所有在响应数据报中需要指定目标ip和目标端口,也就是请求数据包的源ip和源端口 // 通过网卡发送响应到客户端 socket.send(responsePacket); // 4. 打印日志 System.out.printf("[%s:%d], req: %s, resp: %s\n", requestPacket.getAddress(), responsePacket.getPort(), request, response); } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { UdpEchoServer server = new UdpEchoServer(9090); // 启动服务器 server.start(); } }
服务器的端口一般不能变,由程序员写死
对于服务器来说,客户端什么时候发请求,发多少个请求,都是无法预测的;因此服务器中通常都有一个死循环,不断尝试读取客户端发来的请求,7*24 小时不间断工作。
创建一个请求数据报,用于存储客户端发送来的请求;这里我们将参数作为返回值,也就是 "输出型参数"。
此时创建的请求数据包是空,但是我们通过调用 receive 方法,将 requestPacket 作为输出型参数,从而获取到客户端发送来的请求。
此时 requestPacket 存储的就是客户端发送来的请求,其包含一系列数据,请求正文、客户端IP地址、客户端端口等其他相关数据。
将接收到的请求数据根据业务要求进行相关的处理,得到响应数据。
这里的 getData() 就是获取 requestPacket 所读取到的请求正文,随即调用 process() 进行处理返回得到 response响应数据。
得到响应数据后,我们还需构造响应数据报 ,发送给客户端
通过 getBytes() 拿到字节数组的长度;需要注意的是,如果通过 response.length() 获取长度,则是错误的;必须将其转化为字节数组再获取长度才是正确的。
由于 UDP协议 是无连接 的,所以需要在响应数据报带上客户端的 IP地址 和 端口号;客户端的 IP地址 和 端口号 则在 请求数据报 里。
最后,将构造好的 响应数据报 发送给客户端即可。
简单打印日志,确保接收和发送是正确的。
最后在 main方法构造服务器程序并启动服务器程序。
不用关闭吗?文件要关闭,就必须考虑清楚这个文件对象的生命周期,此处的 socket 对象,伴随整个 UDP服务器;如果服务器关闭,进程结束时就会自动释放 PCB(文件描述符表),自然就不需要手动调用 close 了。
UdpEchoClient
javaimport javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.net.*; import java.util.Scanner; /** * @author: Themberfue * @date: 2024/11/3 15:17 * @description: */ public class UdpEchoClient { private DatagramSocket socket = null; // UDP 本身不保存对端的信息,所以得自己通过代码保存 private final String serverIp; private final int serverPort; // 客户端一般不需要指定端口,客户端的端口是经常变的,由操作系统随机分配闲置的端口号 // 此处的构造方法是要访问服务器的 ip 和端口号 public UdpEchoClient(String serverIp, int serverPort) throws SocketException { this.serverIp = serverIp; this.serverPort = serverPort; this.socket = new DatagramSocket(); } public void start() throws IOException { Scanner in = new Scanner(System.in); while (true) { // 输入要发送的请求 System.out.println("请输入要发送的请求"); if (!in.hasNextLine()) { break; } String request = in.nextLine(); // 创建请求数据报 // 在构造过程中,不光要构造载荷,还要加上服务器的 ip 和端口号 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort); // 发送请求数据报到服务端 socket.send(requestPacket); // 创建响应数据报 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); // 接收服务器返回来的响应数据报 socket.receive(responsePacket); // 将接收到的数据进行解析 String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); System.out.println(response); } } public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090); client.start(); } }
这里的代码与服务器的类似,我就不作过多阐述~~~
不过有一点需要注意的是:在 new DatagramSocket() 时,一定要使用无参版本的构造方法,这是因为客户端的端口最好由操作系统随机分配,不应该人为的指定的端口号;万一你指定的端口号已经被使用了,那么程序就出bug了,操作系统随机分配的一定是空闲的端口号。
本节的内容到这就结束了~~~
下节将以 TCP协议 编写代码~~~
毕竟不知后事如何,且听下回分解
❤️❤️❤️❤️❤️❤️❤️