🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。
🔥 本文定位 :写过
sendto与recvfrom之后,再从协议和内核接口的角度理解 UDP:数据怎样找到应用,一次发送对应什么,以及数据为什么会丢失。💡 学习目标:理解端口与五元组;拆解 UDP 首部;掌握数据报边界、截断与零长度消息;分清接收队列、用户缓冲区、协议长度上限和路径 MTU。
📌 阅读范围:以 Linux、IPv4、普通阻塞 UDP socket 为主。涉及 IPv6、端口复用与应用层可靠性时,明确适用条件;实验使用回环地址,不依赖外部服务器。
在前面的 Socket 编程中,我们已经能把一段字符串从客户端发给服务器,再把结果发回来。但"程序可以通信"之后,还有一些问题值得追问:
- 同一台服务器运行多个服务,操作系统如何知道数据属于谁?
- 一次发送 100 字节,接收端能不能分十次、每次读 10 字节?
sendto返回 100,是否说明对方已经收到了这 100 字节?- UDP 首部的长度字段是 16 位,为什么发送几千字节也可能失败?
这些问题连起来,才是 UDP 在实际编程中的完整轮廓。我们从端口开始,把报文结构、接收行为和工程约束依次串起来。
文章目录
- 传输层如何把数据交给应用
- [端口范围与 bind 的两个经典问题](#端口范围与 bind 的两个经典问题)
- [拆开固定 8 字节的 UDP 首部](#拆开固定 8 字节的 UDP 首部)
- 无连接与不可靠,具体意味着什么
- 面向数据报:边界、截断与空消息
- [UDP 的缓冲区与双向通信](#UDP 的缓冲区与双向通信)
- [65535、65507 与 1472 分别是什么](#65535、65507 与 1472 分别是什么)
- 用本机实验验证接收行为
- [大消息拆包与基于 UDP 的应用协议](#大消息拆包与基于 UDP 的应用协议)
- 把容易混淆的结论放回正确条件
一、传输层如何把数据交给应用
1.1 IP 地址之后,为什么还需要端口
一台主机可能同时运行 SSH、Web 服务和我们自己写的 UDP 程序。IP 地址用于网络层寻址,但"送到这台主机"和"交给正确的通信端点"是两步。
端口号参与第二步。对于常见的网络编程,可以先把一个本地通信端点理解为"传输层协议 + 本地 IP + 本地端口"。操作系统再根据 socket 的绑定、连接状态和复用规则,把数据交给合适的 socket。
这里要把"端口"和"进程"稍微分开:端口不是 PID,也不是给每个进程永久分配的编号。 一个进程可以创建多个 socket;多个进程也可能通过继承、传递文件描述符等方式使用同一个 socket。

例如,服务器绑定 192.0.2.20:9000/UDP,客户端 A 从 192.0.2.10:51001 发来消息,客户端 B 从 192.0.2.11:51002 发来消息。服务器可以用同一个 UDP socket 接收两位客户端的数据,再通过 recvfrom 返回的发送方地址区分它们。
这些 192.0.2.x 地址只用于文字示例;后面的实验全部使用 127.0.0.1。
1.2 五元组描述一条通信流
常见的五元组写法是:
text
源 IP、源端口、目的 IP、目的端口、传输层协议
192.0.2.10 : 51001 → 192.0.2.20 : 9000 / UDP
192.0.2.11 : 51002 → 192.0.2.20 : 9000 / UDP
目的端点相同,但源地址、源端口不同,所以可以区分这两条流。把协议也写进去,是因为 TCP 和 UDP 使用各自的端口空间:TCP 9000 与 UDP 9000 可以同时存在。
五元组通常按方向描述报文。服务器回复时,源与目的交换,得到反方向的五元组。抓包工具、流量统计或防火墙可能进一步把两个方向关联成同一次对话。
不要由此推导出"UDP 内核为每个五元组建立一条连接"。 五元组可以用来识别通信,普通未连接 UDP socket 仍可与多个对端交换数据。
二、端口范围与 bind 的两个经典问题
2.1 三个范围,要和 Linux 临时端口配置区分开
端口号是 16 位数值,范围为 0~65535。IANA 的分类如下。
| 范围 | 名称 | 理解方式 |
|---|---|---|
| 0~1023 | System Ports,系统端口 | 常见标准服务集中于此,0 号端口有特殊用途 |
| 1024~49151 | User Ports,用户端口 | 可以登记分配,也常供应用服务使用 |
| 49152~65535 | Dynamic / Private Ports,动态 / 私有端口 | 不作固定服务登记,供动态或私有用途使用 |
这张表是端口号的登记分类,不能直接当作 Linux 自动分配临时端口的固定范围。Linux 的相关配置可以这样查看:
bash
cat /proc/sys/net/ipv4/ip_local_port_range
cat /proc/sys/net/ipv4/ip_local_reserved_ports
第一项指定自动分配所使用的本地端口范围,第二项可以保留不参与自动分配的端口。实际配置取决于运行环境,不应在程序里写死为"1024 以上全部随机分配"。分类与配置分别见 IANA 端口登记表 和 Linux 内核 IP 配置文档。
bind 时把端口填为 0,表示让系统选择可用端口,而不是要求其他主机向目的端口 0 发送数据。选中后可以通过 getsockname 查询。
2.2 知名端口是一种约定
SSH 常用 TCP 22,FTP 控制连接常用 TCP 21,Telnet 常用 TCP 23,HTTP 常用 TCP 80,HTTPS 常见于 TCP 443;HTTP/3 则可以使用 UDP 443。这些约定帮助客户端定位服务,但服务可以通过配置使用其他端口。
Linux 中可以查看本机维护的服务名称映射:
bash
less /etc/services
ss -u -a -n
ss -u -l -n -p
/etc/services 是名称与端口的映射表,里面有一项记录,不代表相应服务已经启动。ss 才用于查看当前 socket;-p 的进程信息可能受权限限制。UDP 输出中的 UNCONN 也不等于服务不能工作,它可能只是一个正常等待数据的未连接 UDP socket。
2.3 一个进程可以 bind 多个端口吗
可以。通常做法是创建多个 socket,让它们分别绑定不同端口:
text
同一进程
socket A → bind(127.0.0.1:9000)
socket B → bind(127.0.0.1:9001)
关键在于 bind 的操作对象是 socket。不要对一个已经绑定的普通 socket 反复调用 bind,期待它同时获得 9000、9001 两个本地端口。
2.4 一个端口可以被多个进程 bind 吗
这要补齐地址、协议和选项等条件,不能只看数字。
| 情况 | 应当怎样判断 |
|---|---|
| TCP 9000 与 UDP 9000 | 协议不同,可以并存 |
| 两个 UDP socket 分别绑定不同的具体本地地址、相同端口 | 在地址可用且没有通配绑定等冲突时可以成立 |
| 两个普通 UDP socket 绑定相同地址、相同端口 | 默认通常发生地址占用冲突 |
使用 Linux SO_REUSEPORT |
满足选项、权限和地址等条件时,可以共享本地端点 |
| 多进程继承同一个 socket | 共享已有 socket,与分别创建后重复 bind 是两回事 |

例如 Linux 的 SO_REUSEPORT 需要参与者在 bind 前设置;防止端口劫持的规则还涉及有效用户 ID。它常用于把 UDP 流量分配给多个接收 socket,不能理解成"每份单播报文必定给每个进程各复制一份"。SO_REUSEADDR 与它也不是可随意替换的两个名字。Linux socket(7):复用选项
三、拆开固定 8 字节的 UDP 首部
3.1 四个字段就构成了整个首部
UDP 的首部固定为 8 字节,结构非常紧凑。

| 字段 | 位数 | 含义 |
|---|---|---|
| Source Port | 16 位 | 源端口,普通应用通常使用它作为回包地址的一部分 |
| Destination Port | 16 位 | 目的端口,参与接收端分用 |
| Length | 16 位 | 当前 UDP 数据报总字节数,包含首部和数据 |
| Checksum | 16 位 | 用于差错检测的校验和 |
发送 100 字节应用数据时:
text
UDP Length = 8 + 100 = 108 字节
Length 记录的是这份报文的实际长度,不是"当前还能发多少"的额度,也不是接收方数组的大小。普通 UDP 数据报的最小长度是 8,此时应用正文为空。
端口和长度等多字节字段采用网络字节序。写 socket 地址时经常看到 htons(9000),就是把主机字节序转为网络字节序。使用普通 SOCK_DGRAM 时,我们提交的是应用数据;UDP 首部通常由内核构造,不需要自己在正文前拼这 8 字节。RFC 768:字段定义
3.2 校验和解决什么问题
UDP 校验和覆盖 UDP 首部、数据以及由 IP 信息组成的伪首部。伪首部参与计算,用来帮助发现误投递等问题;它不是额外插在应用正文前的一段 UDP 数据。
如果接收端验证校验和失败,报文通常会被丢弃,不会被 UDP 自动修复后重新递送。由此可以看到:差错检测与可靠传输是不同的能力。 能发现错误,不代表能够追回丢失的数据。
还需要保留两个边界:
- 对普通 IPv4 UDP,线上校验和字段为 0 表示发送方没有生成 UDP 校验和;如果计算结果为 0,则按规则发送全 1。
- 普通 IPv6 UDP 原则上要求使用校验和;不能把 IPv4 中"可以省略"的结论直接搬过去,特殊隧道例外不在本文范围内。
校验和也不能证明"发送者就是它声称的人",更不提供机密性。有认证或加密需求时,需要上层协议解决。RFC 768:校验和规则;RFC 8200:IPv6 上层协议校验和
四、无连接与不可靠,具体意味着什么
4.1 无连接:没有 UDP 层的建立握手
普通 UDP 发送方知道目的 IP 与端口,就可以调用 sendto 发送,不需要先执行 TCP 那样的连接建立过程。服务器也不需要用 listen、accept 为每位客户端生成连接 socket。
不过,UDP socket 可以调用 connect 。它主要在本地设置默认对端,使后续 send、recv 更方便,并限制通常接收的数据来源。这个 API 名字相同,不代表它执行了 TCP 的握手。
即使 connect 成功,也不能证明对端存在一个正在工作的 UDP 服务。若业务协议需要确认对方准备就绪,仍要设计请求与响应。
4.2 不可靠:协议不承诺最终交付
UDP 自身不提供确认、超时重传、有序交付、去重,也没有 TCP 那套内置流量控制和拥塞控制机制。
因此一份数据报可能到达,也可能丢失;多份报文可能乱序或重复。没有发生丢包时,应用照样可以正常工作;"不可靠"描述的是协议保证的范围,并不是说每次发送都会失败。

4.3 sendto 成功,不等于业务处理成功
假设调用返回 100:
cpp
ssize_t n = sendto(fd, data, 100, 0,
reinterpret_cast<const sockaddr*>(&peer), sizeof(peer));
这表明本次提交的 100 字节被本机协议栈接受。接下来仍可能因为网络拥塞、路由问题、接收队列溢出等原因丢失。即使送到了对方主机,也不一定已被应用读取;即使应用读取了,也不一定已完成业务。
如果业务是"提交一份订单",应用需要匹配业务请求的响应、超时策略,以及避免重复执行的机制。一个简单 ACK 最多证明对方在某个时刻确认了某件事,它的含义仍由协议约定。
4.4 不可靠,不等于所有错误都不会报告
本地参数错误、消息太长等情况可以使系统调用直接失败。Linux 还可能把某些网络异步错误报告给应用;例如收到相关 ICMP 错误后,后续操作可能返回 ECONNREFUSED。启用 IP_RECVERR 后,也可以通过错误队列获取更多信息。
但错误报告并不是逐份 UDP 数据报的可靠回执:有的故障不会产生可见错误,有的错误对应更早发送的报文。因此要同时记住"检查系统调用错误"和"没有报错不能证明送达"。Linux udp(7):连接与错误处理
五、面向数据报:边界、截断与空消息
5.1 UDP 保留消息边界
在普通 UDP 收发模式下,一次成功发送形成一份数据报;普通接收调用一次最多消费一份数据报。
text
发送:sendto("ABC") → 数据报 A,正文 3 字节
sendto("DEFG") → 数据报 B,正文 4 字节
接收:recvfrom(足够大的数组) → 一份数据报
recvfrom(足够大的数组) → 下一份数据报
它不会仅仅因为数组很大,就把两份独立数据报自动拼成 ABCDEFG。注意这说明消息边界,不保证 A、B 的网络到达顺序。
与 TCP 对比时最容易理解:TCP 提供字节流,应用必须另外规定消息边界;UDP 原本有数据报边界,应用需要处理的是长度约束、截断以及丢失等问题。
5.2 一份 100 字节数据报,不能分十次读完
假设发送端先发 100 字节的 A,再发正文为 END 的 B。接收端第一次只提供 10 字节数组。
在 Linux 普通接收语义下,A 的前 10 字节被复制到数组中,其余 90 字节被丢弃。下一次读取的是 B,不是 A 的"后半段"。

这也是为什么不能照搬 TCP 的"循环接收,直到累计收够 100 字节"的思路。第一次接收已经消费掉了 A,后续循环可能把其他消息当成 A 的剩余部分,甚至一直等待。
反过来,如果数组容量有 200 字节,而当前数据报只有 100 字节,接收会返回 100,不会为了填满数组而等待下一份数据报。
5.3 用 recvmsg 检查截断
普通 recvfrom 不能通过一个独立的输出参数返回消息标志。若收到的长度刚好等于数组容量,仅凭这一点也无法确定"恰好装满"还是"原报文更长"。
下面是 Linux 接收循环的核心片段,假设 fd 已经创建、绑定,程序也已包含相应的 socket、sys/uio.h、cerrno、cstdio 和 string 头文件:
cpp
char buf[1024];
sockaddr_in peer{};
iovec iov{};
iov.iov_base = buf;
iov.iov_len = sizeof(buf);
msghdr msg{};
msg.msg_name = &peer;
msg.msg_namelen = sizeof(peer);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t n;
do {
n = recvmsg(fd, &msg, 0);
} while (n < 0 && errno == EINTR);
if (n < 0) {
std::perror("recvmsg");
} else if ((msg.msg_flags & MSG_TRUNC) != 0) {
// 整份消息已消费;不能把残缺内容当成完整业务消息。
std::fprintf(stderr, "datagram truncated\n");
} else {
// 按实际字节数构造,允许正文包含 NUL,也允许 n == 0。
std::string payload(buf, static_cast<std::size_t>(n));
// 在这里解析 payload,并按协议校验长度与字段。
}
把这段放进循环时,应为每次调用重新初始化 msghdr、地址容量等输入输出字段。这里作为传入参数的 flags 为 0,检查的是返回结构中的 msg.msg_flags。
Linux 还支持把 MSG_TRUNC 作为接收调用的输入标志,以得到原数据报长度;此时返回长度可能大于真正写入数组的字节数,不能拿它直接访问数组。Linux recv(2):MSG_TRUNC
5.4 返回 0,也可能是收到空数据报
对 TCP 的普通接收,返回 0 常表示对端有序关闭发送方向;对 UDP,零长度正文是一份合法消息,接收也可能返回 0。
所以不能在通用 UDP 接收循环里直接写 if (n == 0) break;,把它当作连接关闭。UDP 本来也没有 TCP 那种对应的连接终止语义。
本文的边界结论针对普通收发路径。Linux 的 UDP_CORK、UDP_SEGMENT、UDP_GRO 等机制会改变调用与报文的组织方式,进行高性能优化时应按对应接口规则处理,不能把本文的一次调用模型不加条件地套用。Linux udp(7):特殊选项
六、UDP 的缓冲区与双向通信
6.1 先区分两种"接收缓冲区"
第一种是内核 socket 的接收队列 。应用还没有调用接收接口时,已经交给该 socket 的数据报可以暂存在这里。它受 SO_RCVBUF 和系统内存限制等因素影响。
第二种是应用本次提供的数组 ,例如刚才的 char buf[1024]。它决定本次最多能复制多少字节。

两种问题要分别解决:
| 问题 | 原因 | 调整方向 |
|---|---|---|
| 数据报已到主机,但队列持续积压并丢包 | 应用处理速度跟不上,队列容量有限 | 及时读取、控制流量、优化处理路径,合理设置接收容量 |
| 应用拿到一份残缺的数据报 | 本次传入的数组太小 | 约定单包上限、提供足够空间、检查截断 |
只把用户数组从 1 KiB 改成 64 KiB,不能解决应用长时间不读取导致的队列积压。反过来,只调大 SO_RCVBUF,也不会自动扩大 recvmsg 中的 iov_len。
内核队列按实际入队的消息组织,并不替应用恢复网络上的发送顺序。即使接收队列容量充足,也不能因此获得 UDP 协议没有承诺的有序交付。
6.2 UDP 不是完全没有发送缓冲和排队
UDP 的发送路径中仍可能存在 socket 发送内存、协议栈排队、网卡队列等资源,SO_SNDBUF 也有意义。它与 TCP 的关键差别是:UDP 不会为了等待接收方确认,按 TCP 的机制保留数据并自动重传。
所以,把"没有 TCP 式确认与重传缓冲机制"简写成"UDP 完全没有发送缓冲区",容易误导后续排查。发送调用也不是总能立即成功;阻塞或非阻塞行为还与可用资源、路由和具体选项有关。Linux socket(7):SO_SNDBUF 与 SO_RCVBUF
6.3 同一个 UDP socket 可以读,也可以写
服务器用 recvfrom 得到一份请求和发送方地址后,可以直接用同一个 fd 调用 sendto 回包。客户端也可以用原 socket 接收回复。
可以把这种双向能力称作全双工收发能力,但它并不意味着必须建立一个 TCP 式连接,也不代表两边一定同时执行发送。客户端、服务器只是应用角色,不限制 socket 只能读或只能写。
七、65535、65507 与 1472 分别是什么
7.1 长度字段的最大值不是 65536
普通 UDP 的 Length 字段为 16 位,最大可表示值是:
text
2^16 - 1 = 65535
它已经包含 8 字节 UDP 首部,所以单看这个字段,普通 UDP 正文不能超过 65527 字节。但 UDP 报文还需要装进 IP 数据包,实际限制必须继续向外计算。
7.2 普通 IPv4 的常见正文上限是 65507
IPv4 总长度字段同样受 65535 字节限制。当 IP 首部没有选项、长度为 20 字节时:
text
最大 IPv4 总长度 65535
减去 IPv4 首部 20
减去 UDP 首部 8
--------------------------------
最大 UDP 应用数据 65507 字节
有 IPv4 选项时,IP 首部更长,应用数据上限还会减少。普通 IPv6 则使用不同的外层长度规则;没有扩展首部时常见 UDP 正文上限为 65527,IPv6 巨型报文另有特殊规则,不能混在普通场景里计算。
这些是格式上限,不是保证任意路径都能成功发送的大小。

7.3 1472 是一个有前提的算式
假设到目标的路径 MTU 是 1500,使用 20 字节 IPv4 首部、8 字节 UDP 首部,并希望不发生 IP 分片:
text
1500 - 20 - 8 = 1472 字节应用数据
这里的 1500 是网络层 IP 数据包可承载的长度,不需要再减一次以太网首部。若有隧道、额外首部或路径上更小的 MTU,结果就会变化。1472 不能当成所有网络上的通用安全常量。
如果 UDP 报文过大,IPv4 在允许分片且满足条件时可能进行 IP 分片,接收端必须先重组完整 IP 数据包,之后才交给 UDP。少了一片,整份 UDP 数据报就无法正常交付。
Linux 默认的 UDP 路径 MTU 发现行为,也可能使超出已知路径 MTU 的发送返回 EMSGSIZE。正确的处理通常是减小单份应用数据,而不是认定"还没超过 65507,所以不可能太大"。尽量避免依赖 IP 分片,有助于提高传输效率与交付机会。RFC 8085:消息大小建议;Linux udp(7):路径 MTU
7.4 IP 分片与应用拆包的区别
IP 分片发生在网络层;对于普通 socket,应用通常看不到每片 IP 分片,只看到重组后的 UDP 正文。
应用拆包则是把一个业务消息拆成多份独立的 UDP 数据报。每份都有自己的 UDP 首部,接收端也会分别收到,需要按应用协议自行重组。两者名称都带"分",负责的层次与恢复方式却不同。
八、用本机实验验证接收行为
8.1 不连接外网,也能观察消息边界
配套脚本 udp_probe.py 使用 Python 标准库创建本机 UDP socket,自动选择端口并设置接收超时。它把结果输出为 JSON,方便核对实际发生了什么。
bash
cd code
python3 udp_probe.py
Windows 可以使用 python udp_probe.py。脚本对 Linux 的 recvmsg 和 Windows Winsock 的超长接收行为分别处理:Linux 可以复制前 10 字节并报告 MSG_TRUNC;Winsock 的相应调用通常报告 WSAEMSGSIZE,错误号为 10040。不要拿一边的返回值规则解释另一边。
脚本覆盖以下七组观察:
| 实验 | 操作 | 应当核对的现象 |
|---|---|---|
| 一份消息 | 发送 100 字节,提供 200 字节数组 | 收到 100 字节,不等待填满数组 |
| 两份消息 | 发送 ABC、DEFG |
两次普通接收分别得到两份消息 |
| 接收截断 | 发送 100 字节、再发 END,第一次只给 10 字节空间 |
截断后,下一份收到 END,没有前份剩余正文 |
| 零长度消息 | 发送空正文,再发送 ALIVE |
空消息可以被接收,后续通信仍继续 |
| UDP connect 与回包 | 设置默认对端,再交换 HELLO、REPLY |
connect 本身不产生应用数据报,同一 socket 可以双向收发 |
| 一个进程多个端口 | 创建两个 UDP socket,各自绑定 0 号端口 | 得到两个实际可用的不同端口 |
| TCP 与 UDP 同号端口 | 两类 socket 绑定同一本地地址与端口数字 | 可以同时存在 |
实验使用回环地址,几份短消息通常会按发送顺序观察到;这是本次本地运行的现象,不能用它证明互联网传输不会乱序、丢失或重复。
8.2 最值得亲手改动的一组参数
可以把实验简化为下面这段接收思路。这里的 receiver 已绑定,sender 把数据发到它的地址:
python
sender.sendto(b'X' * 100, target)
sender.sendto(b'END', target)
# Linux:有 recvmsg 的 Python 环境。
data, ancillary, flags, source = receiver.recvmsg(10)
print(len(data), bool(flags & socket.MSG_TRUNC)) # 10、True
following, source = receiver.recvfrom(100)
print(following) # b'END',不会读出那 90 字节
把第一次接收容量改成 100、200,再观察:前者刚好装下,后者只返回实际 100 字节。然后把第一份正文改为空,确认"返回空字节串"不等于 UDP 对话结束。
8.3 在 Linux 上查看 socket 和报文
若想观察已经运行的 UDP 服务,可以在另一终端使用:
bash
ss -u -a -n -p
sudo tcpdump -i lo -n -vv -X 'udp port 9000'
这里 9000 是示例服务端口,需要替换为实际端口;自动选择端口的脚本执行很快,不适合执行完之后再去抓。可先运行固定端口的 UDP 服务,或者先启动抓包,再执行脚本。
观察时,分别核对源端口、目的端口、UDP 正文长度和消息次数。工具输出中的 UDP, length ... 常描述正文长度,不能只凭界面上的一个 length 就认定它与首部里的 Length 字段含义完全相同。
抓包还会受到校验和卸载等因素影响,发送主机本地看到的校验和提示不一定意味着线上传输了坏包。排查时应结合抓包位置、实际接收结果和网卡设置判断。
九、大消息拆包与基于 UDP 的应用协议
9.1 超过单包大小,需要的不只是循环 sendto
假设要发送一段 200 KiB 的业务数据,即使把它切成几百个小包,每个包都成功提交,接收方仍然需要知道它们属于哪条消息、排列在哪里、是否收齐。
一种教学用的字段设计是:
text
version | message_id | fragment_index | fragment_count | total_length | data
消息 ID 区分不同业务消息;片号与总片数帮助定位;总长度帮助校验重组结果。应用头部同样占空间,计算每片正文容量时,要从计划使用的 UDP 应用数据容量中继续扣除它。

但字段齐全只解决"怎么摆放"。真正可用的接收端还需要限制最大总长度、总片数和并发重组数量,拒绝越界或相互矛盾的字段,识别重复分片,并在超时后清理不完整消息。
若业务还要求恢复缺失分片,就需要确认、重传与退避;如果重复请求可能产生重复操作,还要考虑幂等性。使用 UDP 不会免去可靠性成本,只会让上层按自己的需求承担这些成本。
9.2 没有内置拥塞控制,也要控制发送行为
UDP 本身没有 TCP 那样的拥塞控制,不表示应用可以无限速发送。持续超出网络可用容量,会增加丢包、排队和重传,影响自己的业务,也会影响共享路径的其他流量。
需要持续传输时,应设计合适的速率控制、反馈和拥塞响应;要求可靠传输时,也可以评估已有传输协议,而不是在几个分片字段上不断叠加复杂机制。RFC 8085:UDP 应用设计建议
9.3 哪些应用使用 UDP
| 协议或场景 | 与 UDP 的关系 | 容易忽略的条件 |
|---|---|---|
| DNS | 普通查询经常使用 UDP 53 | DNS 也需要支持 TCP,不能说 DNS 只使用 UDP |
| DHCPv4 | 使用 UDP 67、68 | 客户端获取网络配置的过程有自己的消息规则 |
| TFTP | 使用 UDP,初始请求发向 69 端口 | 数据交换采用协商的传输端点,并自带确认与重传规则 |
| BOOTP | 基于 UDP 的引导协议 | 属于早期网络引导与配置体系 |
| NFS | 某些历史版本或部署支持 UDP | 不能概括为所有 NFS 都基于 UDP;NFSv4 的传输要求不同 |
| QUIC | 在 UDP 上实现传输能力 | 上层仍提供流、可靠性、拥塞控制和安全机制 |
| 实时音视频、游戏状态同步 | 可以采用 UDP 承载适合的数据 | 延迟、丢失恢复与过期处理策略由具体协议决定 |
DNS 的 TCP 要求可参见 RFC 7766;TFTP 的端点与确认机制见 RFC 1350;NFS 版本差异见 NFSv3 与 NFSv4;QUIC 的能力见 RFC 9000。
实时系统常常关心"新状态能不能及时到达",一份过期数据即使重传成功,也可能失去价值。但这不等于实时业务永远不需要可靠性:控制命令、资源文件和状态同步,可以使用不同的传输策略。
十、把容易混淆的结论放回正确条件
| 常见说法 | 更准确的理解 |
|---|---|
| 端口就是进程编号 | 端口参与标识通信端点,进程与 socket 不是一一对应 |
| 1024 以上都是系统随机分配端口 | 登记分类与系统临时端口配置是两回事 |
| UDP 无连接,所以不能 connect | 可以调用,通常设置默认对端,不执行 TCP 握手 |
| sendto 成功就是对方收到 | 只说明本次数据被本机协议栈接受 |
| UDP 不可靠,所以绝不会返回网络错误 | Linux 可以报告部分异步错误,但不保证逐包获知结果 |
| UDP 的大消息可以分多次 recvfrom 读取 | 普通接收消费一份数据报,数组不足时丢弃超出部分 |
| 接收返回 0 就该关闭 socket | UDP 允许零长度消息,不能套用 TCP EOF 判断 |
| UDP 完全没有发送缓冲 | 存在发送内存和队列,只是没有 TCP 式确认重传机制 |
| UDP 最多能发 64 KiB 正文 | 长度字段、IP 首部与路径 MTU 都要考虑 |
| 1472 字节对所有网络都安全 | 它依赖路径 MTU 1500、IPv4 无选项等前提 |
| 给 UDP 加 ACK 就有完整可靠传输 | 还要定义超时、重传、去重、顺序、资源与拥塞策略 |
写 UDP 程序时,最实用的做法,是先明确一份业务消息最多多大、丢失之后该怎么办,再安排接收数组、超时和处理流程。这样 sendto 与 recvfrom 才不只是两个能让字符串来回移动的函数,而是有明确边界和失败策略的通信接口。
🌟 写在最后
UDP 用很小的首部保留了消息边界,也把大量交付策略留给上层。理解这一点之后,端口、截断、队列与 MTU 就能放在同一条收发路径中解释。
接下来学习 TCP 时,可以继续带着这些问题比较:它怎样确认数据,怎样重传,怎样保证顺序,又为什么提供的是字节流。把两种协议承担的责任对照起来,往往比单独背一张特点表更容易记住。
如果这篇文章帮你厘清了某个细节,欢迎点赞、收藏,也欢迎交流你在 Linux 网络编程中遇到的实际现象。我们下一篇见。