传输层,从端口号到 UDP 协议

📅 2026/7/11 14:06:56 👁️ 阅读次数 📝 编程学习
传输层,从端口号到 UDP 协议

目录

一、传输层:网络通信的 “快递分拣站”

二、端口号:传输层的 “门牌号”

2.1 什么是端口号?

2.2 五元组:唯一标识一次通信

2.3 端口号的范围划分

(1)知名端口号(0-1023):“公共服务的固定门牌号”

(2)动态端口号(1024-65535):“客户端的临时门牌号”

2.4 两个常见问题:端口号的 “绑定规则”

问题 1:一个进程能绑定多个端口号吗?—— 可以!

问题 2:一个端口号能被多个进程绑定吗?—— 通常不可以!

三、UDP 协议:传输层的 “快速快递”

3.1 UDP 的报头:数据的 “快递面单”

3.2 UDP 的三大核心特点

(1)无连接:“不用提前打招呼,直接发”

(2)不可靠:“发完不管,丢了不补”

(3)面向数据报:“整包发,整包收,不能拆”

3.3 UDP 的缓冲区:“临时存放数据的小仓库”

(1)发送缓冲区:“没有真正的缓存,直接交内核”

(2)接收缓冲区:“临时存数据,供应用层读取”

3.4 UDP 的 “全双工” 特性:“同时收发,互不干扰”

3.5 UDP 的使用注意事项

(1)数据长度限制:单次最大传 64K

(2)基于 UDP 的常见应用层协议

四、实战:手把手写 UDP 客户端和服务器

4.1 核心函数详解

(1)创建 UDP 套接字:socket()

(2)绑定端口:bind()

(3)发送数据:sendto()

(4)接收数据:recvfrom()

(5)关闭套接字:close()

4.2 完整代码:UDP 服务器

4.3 完整代码:UDP 客户端

4.4 代码运行步骤

(1)编译代码

(2)启动服务器

(3)启动客户端(新终端)

(4)查看通信效果

五、总结


在网络通信中,有一层 “承上启下” 的关键环节 ——传输层。它一边接收应用层(比如 HTTP、QQ)的 “数据包裹”,一边通过网络层(IP 协议)把包裹送到目标主机,还得确保 “包裹” 能精准交到对方的应用程序手上。这篇博客会从传输层的 “门牌号”(端口号)讲起,深入 UDP 协议的原理,最后用实战代码带你亲手实现 UDP 通信,让你彻底搞懂传输层是怎么工作的。

一、传输层:网络通信的 “快递分拣站”

先搞明白传输层的核心作用:

  • 承上:从应用层接收数据,给数据 “贴标签”(比如端口号),告诉网络层 “这个数据要交给对方的哪个应用”;
  • 启下:接收网络层传来的外部数据,根据 “标签” 把数据交给本机对应的应用层程序;
  • 核心目标:解决 “数据交给谁” 的问题(定位应用程序),并提供不同的传输策略(比如 UDP 的 “快”、TCP 的 “稳”)。

举个生活类比:你在淘宝买了东西,商家(应用层)把快递(数据)交给快递站(传输层)。快递站会贴上面单 —— 收件人地址(IP)、收件人门牌号(端口号)、快递类型(协议,比如 UDP/TCP),然后交给物流(网络层)运输。对方快递站收到后,根据门牌号把快递交给收件人(对方应用程序)。

二、端口号:传输层的 “门牌号”

要让数据精准找到对方的应用程序,就需要一个 “唯一标识”——端口号(Port)。它就像你家的门牌号,快递员(传输层)根据门牌号,才能把快递(数据)准确送到你家(应用程序),而不是邻居家。

2.1 什么是端口号?

端口号是一个16 位的整数(范围 0-65535),用于标识一台主机上的 “唯一应用程序”。每个需要网络通信的应用(比如浏览器、微信、服务器),都会占用一个或多个端口号。

比如:

  • 浏览器访问百度时,百度服务器的 HTTP 服务用80 端口
  • 你用微信发消息时,微信客户端会被系统分配一个动态端口号(比如 56789)。

2.2 五元组:唯一标识一次通信

传输层要区分 “哪个应用的哪次通信”,靠的是五元组—— 用 5 个信息组合,唯一确定一次网络通信:

组成部分作用类比举例
源 IP 地址寄件人地址你的电脑 IP:192.168.1.100
源端口号寄件人门牌号微信客户端端口:56789
目的 IP 地址收件人地址微信服务器 IP:123.123.123.123
目的端口号收件人门牌号微信服务器端口:8080
协议号快递类型(如顺丰、中通)UDP 协议号:17;TCP 协议号:6

为什么需要五元组?比如你同时用浏览器(端口 5000)和微信(端口 56789)访问网络:

  • 浏览器→百度(80 端口)的通信:五元组是(192.168.1.100:5000 → 202.108.22.5:80,TCP);
  • 微信→微信服务器(8080 端口)的通信:五元组是(192.168.1.100:56789 → 123.123.123.123:8080,UDP);传输层通过五元组,能准确区分这两次通信,不会把百度的响应发给微信,也不会把微信的消息发给浏览器。

查看五元组的实战命令:在 Linux/macOS 终端输入netstat -n,就能看到当前所有通信的五元组信息:

plaintext

Proto Recv-Q Send-Q Local Address Foreign Address State udp 0 0 192.168.1.100:56789 123.123.123.123:8080 ESTABLISHED tcp 0 0 192.168.1.100:5000 202.108.22.5:80 ESTABLISHED
  • Local Address:源 IP: 源端口;
  • Foreign Address:目的 IP: 目的端口;
  • Proto:协议号(udp/tcp)。

2.3 端口号的范围划分

端口号总共 65536 个(0-65535),按用途分为两类:

(1)知名端口号(0-1023):“公共服务的固定门牌号”

这些端口号是 “约定俗成” 的,对应常用的公共服务,比如:

端口号对应服务应用场景
22SSH(远程登录)远程管理 Linux 服务器
21FTP(文件传输)服务器文件上传下载
23Telnet(远程终端)早期远程登录(已被 SSH 替代)
80HTTP(网页服务)浏览器访问网页
443HTTPS(安全网页)淘宝、支付宝等安全访问

这些端口号一般由系统或服务器程序占用,普通客户端程序不能用。

(2)动态端口号(1024-65535):“客户端的临时门牌号”

普通客户端程序(比如你的浏览器、微信、游戏)启动时,会由操作系统随机分配一个这个范围的端口号,通信结束后释放。比如你打开浏览器访问百度,操作系统可能给浏览器分配 5000 端口,关闭浏览器后,5000 端口就会被释放,下次打开浏览器可能分配 5001 端口。

2.4 两个常见问题:端口号的 “绑定规则”

问题 1:一个进程能绑定多个端口号吗?—— 可以!

比如一个服务器程序(如 Nginx),可以同时绑定 80 端口(HTTP)和 443 端口(HTTPS),这样客户端既能用 HTTP 访问,也能用 HTTPS 访问。原理:进程可以创建多个 “套接字(Socket)”,每个套接字绑定一个不同的端口号,独立接收数据。

问题 2:一个端口号能被多个进程绑定吗?—— 通常不可以!

端口号的核心作用是 “唯一标识一个应用程序”,如果一个端口号被多个进程绑定,传输层收到数据后,就不知道该交给哪个进程了。例外:特殊场景下(如负载均衡),可以通过内核参数配置 “端口复用”,但这是高级操作,普通开发不用考虑。

三、UDP 协议:传输层的 “快速快递”

传输层有两个核心协议:UDP 和 TCP。今天先讲UDP(用户数据报协议)—— 它的特点是 “快、简单”,适合对实时性要求高、允许少量数据丢失的场景(比如视频通话、DNS 查询)。

3.1 UDP 的报头:数据的 “快递面单”

UDP 数据在传输时,会在应用层数据前加一个8 字节的报头(相当于快递面单),报头包含 4 个字段,每个字段都是 16 位(2 字节):

字段长度作用说明
源端口号16 位数据从哪个端口发来(可选,0 表示无)
目的端口号16 位数据要送到哪个端口(必须有)
UDP 长度16 位整个 UDP 数据报的长度(报头 + 数据)
UDP 校验和16 位检查数据是否损坏(出错则丢弃)

报头的作用

  • 传输层收到 UDP 数据后,先读 8 字节报头,根据 “目的端口号” 找到对应的应用程序;
  • 通过 “UDP 长度” 知道后续要读多少字节的应用层数据;
  • 通过 “校验和” 判断数据在传输中是否被篡改(比如网络干扰),如果出错,直接丢弃数据,不通知应用层。

3.2 UDP 的三大核心特点

(1)无连接:“不用提前打招呼,直接发”

UDP 通信前,不需要像 TCP 那样 “三次握手” 建立连接 —— 只要知道对方的 IP 和端口号,就能直接发送数据,就像发短信:不用先拨号,编辑好内容直接发给对方号码。

优点:通信延迟低,适合实时场景(比如语音聊天,不能等连接建立再说话);缺点:不知道对方是否在线,可能发了 “空包”(对方没收到)。

(2)不可靠:“发完不管,丢了不补”

UDP 没有 “确认机制” 和 “重传机制”:

  • 发送方发完数据,不知道对方有没有收到;
  • 如果数据在传输中丢失(比如网络拥堵),UDP 不会重传,也不会告诉应用层 “数据丢了”。

举例:你用 UDP 发 10 条消息,对方可能只收到 8 条,丢了 2 条,但你完全不知道。适合场景:允许少量丢失的场景(比如视频通话,丢几帧画面不影响整体观看;DNS 查询,丢了再查一次就行)。

(3)面向数据报:“整包发,整包收,不能拆”

UDP 把应用层的数据当作 “一个完整的数据包”(数据报),既不拆分,也不合并:

  • 发送方:调用一次sendto(发送函数),发送 100 字节数据,UDP 就把这 100 字节当作一个数据报发送;
  • 接收方:必须调用一次recvfrom(接收函数),一次性接收这 100 字节,不能分 10 次每次收 10 字节。

反例:如果发送方分两次各发 50 字节,接收方不能一次收 100 字节 —— 会只收到第一个 50 字节,第二个 50 字节下次收。

3.3 UDP 的缓冲区:“临时存放数据的小仓库”

UDP 的发送和接收缓冲区是分开的,作用不同,很多人容易搞混,这里重点讲:

(1)发送缓冲区:“没有真正的缓存,直接交内核”

UDP 没有 “真正的发送缓冲区”—— 应用层调用sendto发送数据时,数据会直接拷贝到内核缓冲区,内核尽快把数据发给网络层,发送完成后,内核缓冲区的 data 就会被释放,不会保留(不像 TCP 会保留数据用于重传)。

注意:如果内核缓冲区满了,sendto会阻塞(默认)或返回错误,需要等内核腾出空间。

(2)接收缓冲区:“临时存数据,供应用层读取”

UDP 有 “接收缓冲区”—— 内核收到 UDP 数据后,会把数据报放到接收缓冲区,应用层调用recvfrom时,从缓冲区里取一个数据报。

接收缓冲区的两个关键特性:

  1. 无序存放:数据报的接收顺序可能和发送顺序不一致(比如网络路由不同),缓冲区会按 “收到的顺序” 存放,不管发送顺序;
  2. 满了就丢:如果缓冲区满了,新收到的 UDP 数据报会直接被丢弃,不会通知应用层。

为什么需要接收缓冲区?如果没有接收缓冲区,应用层必须 “实时读取” 数据 —— 一旦应用层忙(比如处理其他任务),内核收到的数据就会被丢弃,浪费网络资源。有了缓冲区,数据可以临时存放,应用层空闲时再读。

3.4 UDP 的 “全双工” 特性:“同时收发,互不干扰”

UDP 的套接字(Socket)既能发送数据,也能接收数据,这种特性叫 “全双工”,就像打电话:你可以一边说话(发送),一边听对方说话(接收),不用等对方说完再开口。

举例:UDP 客户端和服务器可以同时给对方发消息,不用等对方回复再发。

3.5 UDP 的使用注意事项

(1)数据长度限制:单次最大传 64K

UDP 报头的 “UDP 长度” 是 16 位,最大能表示 65535 字节(64K),所以单次 UDP 数据报的最大长度是 64K(包含 8 字节报头),实际应用层数据最大约 64K-8=65527 字节。

如果要传超过 64K 的数据(比如传一个 100K 的文件),需要在应用层手动分包

  • 发送方:把 100K 文件分成 2 个包(64K 和 36K),每次发一个,给每个包加 “包序号”(比如 1、2);
  • 接收方:收到包后,按 “包序号” 拼接成完整文件。
(2)基于 UDP 的常见应用层协议

UDP 虽然不可靠,但因为 “快”,被很多核心协议采用:

协议名称用途为什么用 UDP?
DNS域名解析(如百度→IP)查询快,丢了再查一次,延迟低
DHCP自动分配 IP 地址不需要可靠,一次广播就能分配 IP
TFTP简单文件传输协议简单,适合小文件(如路由器固件)
视频 / 语音实时通话实时性优先,丢几帧不影响体验

四、实战:手把手写 UDP 客户端和服务器

我们用 C++ 写一个简单的 UDP 通信程序:

  • 服务器:绑定 8888 端口,接收客户端消息,返回 “收到了:XXX”;
  • 客户端:输入消息,发给服务器,接收服务器的回复并显示。

4.1 核心函数详解

在写代码前,先搞懂 UDP 编程的 4 个核心函数:socketbindsendtorecvfromclose

(1)创建 UDP 套接字:socket()
#include <sys/socket.h> int socket(int domain, int type, int protocol);
  • 作用:创建一个 UDP “通信管道”(套接字),用于收发数据。
  • 参数
    • domain:协议族,必须是AF_INET(IPv4);
    • type:套接字类型,SOCK_DGRAM(表示 UDP);
    • protocol:协议,填 0(自动选择 UDP 协议);
  • 返回值
    • 成功:返回 “套接字描述符”(非负整数,类似文件句柄);
    • 失败:返回 - 1,错误原因存在errno中。
  • 示例
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket创建失败"); // 打印错误原因 exit(1); }
(2)绑定端口:bind()
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:给 UDP 套接字绑定一个固定的 IP 和端口号(服务器必须绑定,客户端可选)。
  • 参数
    • sockfdsocket()返回的套接字描述符;
    • addr:存放 IP 和端口的结构体(struct sockaddr_in);
    • addrlen:结构体的大小(sizeof(struct sockaddr_in));
  • 返回值
    • 成功:0;
    • 失败:-1。
  • 示例(服务器绑定 8888 端口)
    struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); // 初始化结构体 server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(8888); // 端口号(转网络字节序) server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡的IP(0.0.0.0) if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("绑定端口失败"); close(sockfd); exit(1); }
  • 为什么服务器必须绑定端口?客户端需要知道服务器的端口号才能发消息,如果服务器不绑定固定端口,每次启动端口号都变,客户端找不到服务器。
(3)发送数据:sendto()
#include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 作用:通过 UDP 套接字给指定 IP 和端口的目标发送数据。
  • 参数
    • sockfd:套接字描述符;
    • buf:要发送的数据缓冲区(比如字符串);
    • len:数据的长度(字节数);
    • flags:发送标志,填 0(默认);
    • dest_addr:目标的 IP 和端口结构体(客户端填服务器的,服务器填客户端的);
    • addrlen:目标结构体的大小;
  • 返回值
    • 成功:返回实际发送的字节数;
    • 失败:返回 - 1。
  • 示例(客户端发消息给服务器)
    const char *msg = "Hello UDP Server!"; struct sockaddr_in server_addr; // 服务器的IP和端口 // 初始化server_addr(略,见客户端代码) ssize_t n = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&server_addr, sizeof(server_addr)); if (n == -1) { perror("发送失败"); close(sockfd); exit(1); } printf("发送了%d字节:%s\n", (int)n, msg);
(4)接收数据:recvfrom()
#include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • 作用:从 UDP 套接字接收数据,并获取发送方的 IP 和端口。
  • 参数
    • sockfd:套接字描述符;
    • buf:存放接收数据的缓冲区;
    • len:缓冲区的大小(避免溢出);
    • flags:接收标志,填 0(默认,阻塞等待数据);
    • src_addr:输出参数,存放发送方的 IP 和端口;
    • addrlen:输入输出参数,先填sizeof(src_addr),接收后存实际长度;
  • 返回值
    • 成功:返回实际接收的字节数;
    • 失败:返回 - 1;
    • 注意:UDP 没有 “连接关闭” 的概念,不会返回 0。
  • 示例(服务器接收客户端消息)
    char buf[1024] = {0}; // 缓冲区,大小1024字节 struct sockaddr_in client_addr; // 存放客户端的IP和端口 socklen_t client_len = sizeof(client_addr); ssize_t n = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&client_addr, &client_len); if (n == -1) { perror("接收失败"); close(sockfd); exit(1); } buf[n] = '\0'; // 加字符串结束符 // 把客户端IP从网络字节序转成字符串 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); printf("收到来自%s:%d的消息(%d字节):%s\n", client_ip, ntohs(client_addr.sin_port), (int)n, buf);
(5)关闭套接字:close()
#include <unistd.h> int close(int fd);
  • 作用:关闭 UDP 套接字,释放资源。
  • 参数fd:套接字描述符;
  • 返回值:成功 0,失败 - 1;
  • 示例close(sockfd);

4.2 完整代码:UDP 服务器

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8888 // 服务器绑定的端口号 #define BUF_SIZE 1024 // 缓冲区大小 int main() { // 1. 创建UDP套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket创建失败"); exit(1); } printf("UDP套接字创建成功(fd=%d)\n", sockfd); // 2. 绑定端口(服务器必须绑定) struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(PORT); // 端口号转网络字节序 server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡IP(0.0.0.0) if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("绑定端口失败"); close(sockfd); exit(1); } printf("服务器绑定端口%d成功,等待客户端消息...\n", PORT); // 3. 循环接收客户端消息,并发回响应 while (1) { char buf[BUF_SIZE] = {0}; struct sockaddr_in client_addr; // 存放客户端信息 socklen_t client_len = sizeof(client_addr); // 接收客户端消息 ssize_t recv_len = recvfrom(sockfd, buf, BUF_SIZE-1, 0, (struct sockaddr*)&client_addr, &client_len); if (recv_len == -1) { perror("接收消息失败"); continue; } buf[recv_len] = '\0'; // 加字符串结束符 // 解析客户端IP和端口 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); uint16_t client_port = ntohs(client_addr.sin_port); printf("收到来自%s:%d的消息:%s\n", client_ip, client_port, buf); // 构造响应消息(在客户端消息前加“收到了:”) char resp[BUF_SIZE]; snprintf(resp, BUF_SIZE, "收到了:%s", buf); // 发送响应给客户端 ssize_t send_len = sendto(sockfd, resp, strlen(resp), 0, (struct sockaddr*)&client_addr, client_len); if (send_len == -1) { perror("发送响应失败"); continue; } printf("给%s:%d发送响应:%s\n", client_ip, client_port, resp); } // 4. 关闭套接字(循环不会退出,实际不会执行) close(sockfd); return 0; }

4.3 完整代码:UDP 客户端

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" // 服务器IP(本地测试用127.0.0.1) #define SERVER_PORT 8888 // 服务器端口号 #define BUF_SIZE 1024 // 缓冲区大小 int main() { // 1. 创建UDP套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket创建失败"); exit(1); } printf("UDP套接字创建成功(fd=%d)\n", sockfd); // 2. 初始化服务器地址结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(SERVER_PORT); // 服务器端口转网络字节序 // 服务器IP转网络字节序 if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) { perror("服务器IP无效"); close(sockfd); exit(1); } // 3. 循环输入消息,发送给服务器,并接收响应 char buf[BUF_SIZE]; while (1) { // 输入要发送的消息 printf("请输入要发送的消息(输入q退出):"); fgets(buf, BUF_SIZE, stdin); // 去掉fgets自带的换行符 buf[strcspn(buf, "\n")] = '\0'; // 输入q退出 if (strcmp(buf, "q") == 0) { printf("客户端退出\n"); break; } // 发送消息给服务器 ssize_t send_len = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&server_addr, sizeof(server_addr)); if (send_len == -1) { perror("发送消息失败"); continue; } printf("发送给服务器(%s:%d)的消息:%s\n", SERVER_IP, SERVER_PORT, buf); // 接收服务器的响应 char resp[BUF_SIZE] = {0}; struct sockaddr_in server_resp_addr; // 存放服务器响应的地址(可忽略) socklen_t server_len = sizeof(server_resp_addr); ssize_t recv_len = recvfrom(sockfd, resp, BUF_SIZE-1, 0, (struct sockaddr*)&server_resp_addr, &server_len); if (recv_len == -1) { perror("接收响应失败"); continue; } resp[recv_len] = '\0'; printf("收到服务器的响应:%s\n", resp); } // 4. 关闭套接字 close(sockfd); return 0; }

4.4 代码运行步骤

(1)编译代码

在 Linux/macOS 终端输入:

# 编译服务器 gcc udp_server.c -o udp_server # 编译客户端 gcc udp_client.c -o udp_client
(2)启动服务器
./udp_server # 输出: # UDP套接字创建成功(fd=3) # 服务器绑定端口8888成功,等待客户端消息...
(3)启动客户端(新终端)
./udp_client # 输出: # UDP套接字创建成功(fd=3) # 请输入要发送的消息(输入q退出):Hello UDP!
(4)查看通信效果
  • 客户端输入Hello UDP!后,会显示:

    plaintext

    发送给服务器(127.0.0.1:8888)的消息:Hello UDP! 收到服务器的响应:收到了:Hello UDP!
  • 服务器会显示:

    plaintext

    收到来自127.0.0.1:56789的消息:Hello UDP! 给127.0.0.1:56789发送响应:收到了:Hello UDP!

(注:客户端的端口号 56789 是系统动态分配的,每次启动可能不同)

五、总结

这篇博客从传输层的基础讲起,带你掌握了两个核心知识点:

  1. 端口号:传输层的 “门牌号”,用 16 位整数标识应用程序,五元组唯一确定一次通信,知名端口号是公共服务的固定端口;
  2. UDP 协议:传输层的 “快速快递”,无连接、不可靠、面向数据报,适合实时场景,核心函数是socketbindsendtorecvfrom

---------------------------------------------------------------------------------------------------------------------------------

通过实战代码,你亲手实现了 UDP 的客户端和服务器,理解了 UDP 通信的完整流程。下一篇我们会讲传输层的另一个核心协议 ——TCP,它的 “可靠传输” 特性是如何实现的,以及和 UDP 的区别。