UDP(User Datagram Protocol)是互联网中最简单的传输层协议之一。它的头部只有 8 字节,而 TCP 头部最少 20 字节。为什么 UDP 这么简洁?这 8 个字节够用吗?少一点行不行?多一点又如何?
一、UDP 的诞生背景
1.1 RFC 768:一页纸的协议
UDP 定义在 1980 年发布的 RFC 768 中。这份 RFC 整个只有不到两页纸,是互联网历史上最短的协议标准之一。相比之下,TCP 的定义(RFC 793)有几十页。
这种差距并非偶然。1970 年代末,ARPANET 正在从 NCP(Network Control Protocol)迁移到 TCP/IP 协议族。在这一过程中,人们意识到并非所有应用都需要 TCP 那样复杂的可靠性保证。一些应用只想要一个”发出去就行”的传输服务,TCP 的三次握手、确认重传、流量控制反而成了累赘。
UDP 就是这个需求的产物:提供一个最简单的复用/分用机制,其他一切交给应用层。
1.2 端到端原则
UDP 的极简设计体现了网络架构中的一个核心哲学:端到端原则(End-to-End Principle)。1984 年的论文 “End-to-End Arguments in System Design” 指出,可靠传输等功能应该在通信的端点实现,而不是在网络的每一层重复实现。
TCP 把可靠性塞进了传输层,UDP 则选择了另一条路:传输层只做最低限度的复用(端口号),可靠性由应用按需实现。这种设计在当时看起来”偷懒”,但正是这种克制,让 UDP 在 40 多年后焕发了第二春(QUIC 就建在 UDP 之上)。
二、UDP 头部结构:逐字段分析
2.1 头部详解
0 7 8 15 16 23 24 31+--------+--------+--------+--------+| Source | Destination || Port | Port |+--------+--------+--------+--------+| Length | Checksum |+--------+--------+--------+--------+| data octets ... |+-----------------------------------+ 每行 32 位,共 2 行 = 8 字节| 字段 | 长度 | 说明 |
|---|---|---|
| Source Port | 16 位 | 发送方端口(可选填 0) |
| Destination Port | 16 位 | 接收方端口(必须填写) |
| Length | 16 位 | UDP 头 + 数据的总长度(字节) |
| Checksum | 16 位 | 校验和(IPv4 下可选) |
2.2 为什么是这四个字段?
源端口(Source Port):实现多路复用。没有端口号,内核收到数据后就不知道该交给哪个进程。16 位提供 65536 个端口,对绝大多数场景足够。源端口可以填 0,表示不需要回复。
目标端口(Destination Port):这是 UDP 头中最不可或缺的字段。没有它,数据包到了目的主机后,内核无法分用给正确的进程。
长度(Length):为什么需要 Length 字段?IP 头已经有一个 Total Length 字段了。原因在于 IP 层的长度包含 IP 头自身,而 UDP 的 Length 字段可以直接算出数据部分的长度,不需要减去 IP 头长度。此外,UDP Length 还能检测 IP 层可能出现的截断错误。
校验和(Checksum):这是唯一一个”可选”的字段。在 IPv4 下,发送方可以将校验和置为 0,表示不计算校验和。但在 IPv6 下(RFC 2460),校验和是必须的,因为 IPv6 去掉了 IP 层的校验和。
2.3 能不能更少?
假设去掉源端口,省 2 字节,变成 6 字节头部。问题是:很多协议依赖源端口来回复(比如 DNS 请求发到 53 端口,响应要回到源端口标识的客户端)。去掉源端口,UDP 就只能做单向通知,实用性大打折扣。
假设去掉 Length,省 2 字节。那接收方就得从 IP 头推算数据长度,但 IP 层可能有填充(padding),导致长度计算不准确。Length 字段是 UDP 自包含的必要条件。
假设去掉 Checksum,省 2 字节。实际上,在 IPv4 下确实可以这么做,但那就完全没有错误检测了。8 字节已经是最小可行方案。
2.4 用 C 语言表示 UDP 头
// Linux 内核中的 UDP 头部定义(include/uapi/linux/udp.h)struct udphdr { __be16 source; // 源端口 __be16 dest; // 目标端口 __be16 len; // 长度 __be16 check; // 校验和};// 4 个 __be16 = 8 字节8 字节的头部意味着一个 UDP 数据包的最小开销极低。结合以太网帧头(14 字节)和 IP 头(20 字节),一个最小的 UDP 包总共有 42 字节的协议开销。
三、UDP 与 TCP 头部的深度对比
3.1 结构对比
TCP 头部(至少 20 字节):┌────────┬────────┬────────────────────┬────────┐│Src Port│Dst Port│ Sequence │Ack Num │├────────┼────────┼────────────────────┼────────┤│ Offset │ Flags │ Window │Chksum │├────────┼────────┼────────────────────┼────────┤│ Urgent Pointer (可选) │ │├────────┴────────┴────────────────────┴────────┤│ Options (可变长度) │└──────────────────────────────────────────────┘3.2 TCP 多出的 12+ 字节都做了什么
| TCP 字段 | 字节数 | UDP 有吗 | 存在的意义 |
|---|---|---|---|
| Sequence Number | 4 | 字节流编号,保证顺序 | |
| Acknowledgment | 4 | 确认收到的数据 | |
| Data Offset + Flags | 1.5 | 头部长度 + 控制标志 | |
| Window | 2 | 流量控制:接收窗口大小 | |
| Urgent Pointer | 0.5 | 紧急数据指针(已过时) |
TCP 用 12 个额外的字节(最小头部差值)换来了可靠的字节流传输。但如果你不需要这些功能,这 12 字节就是纯粹的浪费。
3.3 头部开销的实际影响
以一个典型的 DNS 查询为例:
| 协议 | IP 头 | 传输层头 | 数据 | 总计 | 传输层开销占比 |
|---|---|---|---|---|---|
| UDP | 20B | 8B | 50B | 78B | 10.3% |
| TCP | 20B | 20B | 50B | 90B | 22.2% |
DNS 查询的请求数据通常只有 50 字节左右。TCP 的头部开销占比是 UDP 的两倍多。如果算上 TCP 三次握手的额外开销(SYN + SYN-ACK + ACK = 3 个包,共 180 字节纯协议开销),差距更大。
# 用 tcpdump 观察 DNS 查询的 UDP 包sudo tcpdump -i eth0 -n -vvv port 53
# 输出示例:# IP 192.168.1.100.54321 > 8.8.8.8.53: UDP, length 45# IP 8.8.8.8.53 > 192.168.1.100.54321: UDP, length 61# 整个查询只需 2 个包,1 个 RTT四、UDP 简洁设计的原因
4.1 设计哲学:最小化 overhead
核心原则:如果功能不需要,就不要添加。这不是偷懒,而是一种克制。
1970 年代末的网络环境带宽极其有限(ARPANET 的骨干链路只有 56 Kbps)。每一个字节的开销都是真金白银。在这个背景下,UDP 把头部压缩到 8 字节是有实际经济意义的。
4.2 无状态设计
TCP 维护连接状态:序列号、窗口大小、拥塞窗口、重传定时器等。每个连接在内核中需要几 KB 的控制块(TCB,Transmission Control Block)。
UDP 不维护任何连接状态。一个 UDP 套接字可以同时和成千上万个对端通信,而内核不需要为每个对端分配状态。这意味着:
# TCP 服务器:每个连接占用内核内存# 假设 10 万个并发连接,每个 TCB 约 4KB# 总内存开销 = 100,000 × 4KB ≈ 400MB
# UDP 服务器:无连接状态# 不管和多少个对端通信,内核开销几乎不变这也是为什么 DNS 服务器、游戏服务器、流媒体服务器倾向于使用 UDP 的原因之一。
4.3 延迟敏感型应用
UDP 适合的应用场景:
| 应用 | 特点 | 为什么用 UDP | 不用 TCP 的后果 |
|---|---|---|---|
| DNS | 查询量巨大,低延迟 | 无需连接建立,1 RTT 完成 | 三次握手 + 查询 = 2+ RTT |
| 视频流 | 容忍丢包 | 快速传输比可靠更重要 | 重传导致卡顿,体验更差 |
| VoIP | 实时性要求高 | 重传反而增加延迟 | 200ms 后重传的数据已无意义 |
| 游戏 | 状态更新频繁 | 只需最新状态 | 等待重传旧状态毫无价值 |
4.4 DNS 查询:UDP 的经典场景
# DNS 查询通常很小# 请求:约 50 字节# 响应:通常 < 512 字节(传统限制)
# 如果用 TCP:# SYN → SYN-ACK → ACK → DNS 请求 → DNS 响应 → FIN → FIN-ACK → ACK# = 8 个包,至少 2 个 RTT
# 如果用 UDP:# DNS 请求 → DNS 响应# = 2 个包,1 个 RTTDNS 原始设计(RFC 1035)规定 UDP 响应不超过 512 字节。超过这个限制时,DNS 会自动回退到 TCP(通过设置 TC 标志位)。这种混合策略是 UDP 简洁性的经典利用:小查询走 UDP 快速完成,大响应走 TCP 保证可靠。
五、UDP 校验和:为什么是可选的
5.1 校验和的计算方式
UDP 校验和的计算范围比较特殊:它不仅覆盖 UDP 头和数据,还覆盖一个”伪头部”(Pseudo Header):
伪头部(不实际传输,仅用于计算校验和):┌──────────────────────────────────┐│ Source IP Address │ (4 字节)├──────────────────────────────────┤│ Destination IP Address │ (4 字节)├────────┬─────────┬───────────────┤│ Zero │Protocol │ UDP Length │ (4 字节)└────────┴─────────┴───────────────┘伪头部把 IP 地址信息纳入校验,确保数据包不会因为 IP 层的错误而被误投。这是一种跨层的保护机制。
5.2 为什么 IPv4 下可选
在 IPv4 下,IP 头自身有一个校验和(Header Checksum)。UDP 的设计者认为,既然 IP 层已经做了校验,UDP 再做一遍可能是冗余的。对于计算能力有限的早期设备,跳过 UDP 校验和可以节省 CPU 时间。
但在 IPv6 下,IP 层去掉了 Header Checksum(因为链路层通常有 CRC 校验),所以 UDP 校验和变成了必须的。这是一个重要的细节:UDP 的简洁有时以牺牲安全性为代价。
// 计算UDP校验和的伪代码uint16_t udp_checksum(struct udphdr *uh, size_t len, uint32_t src_ip, uint32_t dst_ip) { struct pseudo_header ph; ph.src_ip = src_ip; ph.dst_ip = dst_ip; ph.zero = 0; ph.protocol = IPPROTO_UDP; // 17 ph.udp_len = htons(len);
// 将伪头部 + UDP头 + 数据一起计算校验和 return compute_checksum(&ph, sizeof(ph), uh, len);}六、UDP 缺少的功能及其影响
6.1 UDP 不提供的能力
| TCP 功能 | UDP 支持吗 | 如何弥补 | 不弥补的后果 |
|---|---|---|---|
| 可靠传输 | 应用层实现(ACK/重传) | 数据丢失无感知 | |
| 顺序保证 | 应用层序号 | 数据乱序到达 | |
| 流量控制 | 应用层限速 | 接收方缓冲区溢出 | |
| 拥塞控制 | 应用层实现或 QUIC | 网络拥塞崩溃 | |
| 连接管理 | 无连接 | 无法区分连接 |
6.2 拥塞控制的缺失:一个严重问题
UDP 没有拥塞控制,这是它最被诟病的问题。如果大量应用无节制地使用 UDP 发送数据,网络可能发生拥塞崩溃。
1986 年 10 月,ARPANET 曾因缺乏拥塞控制而发生了严重的网络崩溃事件,吞吐量从 32 Kbps 暴跌到 40 bps。这直接推动了 TCP 拥塞控制算法(Jacobson 算法,1988 年)的诞生。
现代的 UDP 应用如果不实现拥塞控制,会与 TCP 流量不公平竞争。这就是为什么 QUIC 在 UDP 之上重新实现了拥塞控制。
6.3 在 UDP 上实现可靠传输
# 应用层实现可靠 UDP 的核心逻辑class ReliableUDP: def __init__(self): self.unacked = {} # 未确认的数据包 self.timeout = 0.5 # 重传超时(秒) self.next_seq = 0 # 下一个序列号 self.recv_buffer = {} # 接收缓冲区(处理乱序)
def send(self, data): seq = self.next_seq self.unacked[seq] = { 'data': data, 'time': time.time(), 'retries': 0 } self.sock.sendto(self._pack(seq, data), self.addr) self.next_seq += len(data)
def recv(self): while True: raw, addr = self.sock.recvfrom(4096) seq, payload = self._unpack(raw) self._ack(seq) # 发送确认 self.recv_buffer[seq] = payload # 按序交付 if seq == self.next_expected: return self._deliver_ordered()
def _retransmit(self): """定期检查超时并重传""" now = time.time() for seq, pkt in list(self.unacked.items()): if now - pkt['time'] > self.timeout: pkt['retries'] += 1 pkt['time'] = now self.sock.sendto(self._pack(seq, pkt['data']), self.addr)这段代码实现了可靠传输的基本框架,但它缺少拥塞控制、流量控制等关键机制。一个完整的实现基本上是在重新发明 TCP。
七、QUIC:在 UDP 上构建现代传输
7.1 为什么 QUIC 选择 UDP
QUIC(Quick UDP Internet Connections)是 HTTP/3 的传输层基础。Google 设计 QUIC 时,面临一个选择:修改 TCP 还是新建协议?
修改 TCP 的问题:
- TCP 的很多行为被中间设备(防火墙、NAT、负载均衡器)硬编码,非标准 TCP 选项会被丢弃
- 操作系统内核的 TCP 实现升级缓慢,无法快速迭代
- TCP 的队头阻塞问题在协议层面无法解决
选择 UDP 的好处:
- UDP 包可以穿透大多数中间设备
- 用户态实现,不需要修改内核
- 可以在不升级操作系统的情况下部署新功能
7.2 QUIC 的协议栈
7.3 QUIC vs TCP + TLS 的详细对比
| 特性 | TCP + TLS | QUIC | 为什么有差异 |
|---|---|---|---|
| 连接建立 | 3-4 RTT | 0-1 RTT | QUIC 将握手和加密合并 |
| 队头阻塞 | 有(一个流阻塞所有) | 无(流间独立) | QUIC 多路复用在传输层 |
| 流量控制 | 连接级 | 连接级 + 流级 | QUIC 更细粒度 |
| 拥塞控制 | 内核实现 | 用户态可插拔 | QUIC 可以快速迭代算法 |
| 连接迁移 | 不支持(4 元组绑定) | 支持(Connection ID) | QUIC 用 CID 标识连接 |
| 协议升级 | 需要升级内核 | 只需升级应用 | QUIC 在用户态实现 |
7.4 QUIC 头部的权衡
QUIC 在 UDP 之上添加了自己的头部。一个 QUIC 包 = UDP 头(8B)+ QUIC 头(变长,约 20-40B)+ 加密后的有效载荷。
看起来 QUIC 的总开销比裸 TCP 还大。但 QUIC 的价值不在于减少头部开销,而在于:
- 合并握手,减少 RTT
- 消除队头阻塞
- 支持连接迁移
- 可快速迭代
八、其他传输层协议的头部对比
| 协议 | 头部大小 | 年代 | 设计目标 |
|---|---|---|---|
| UDP | 8 字节 | 1980 | 最小开销的数据报传输 |
| TCP | 20+ 字节 | 1981 | 可靠的字节流传输 |
| SCTP | 12+ 字节 | 2000 | 多流、多宿、部分可靠 |
| DCCP | 16 字节 | 2006 | 有拥塞控制但不保证可靠 |
| UDP-Lite | 8 字节 | 2004 | 允许部分校验(容忍位错误) |
SCTP 和 DCCP 试图在 UDP 和 TCP 之间找到中间地带,但因为需要内核支持和中间设备兼容,它们始终没有大规模部署。UDP 的简洁反而成了最大的优势:因为头部太简单,几乎没有中间设备会干扰 UDP 包。
九、总结
9.1 UDP 头部只有 8 字节的设计哲学
| 设计决策 | 理由 |
|---|---|
| 最小 overhead | 适合小数据包、高频率场景 |
| 无连接状态 | 服务器可服务更多客户端 |
| 无可靠性机制 | 应用层按需实现,避免过度设计 |
| 可选校验和 | 在可靠链路上节省 CPU 开销 |
| 固定长度头部 | 解析简单,无需复杂的选项处理 |
9.2 8 字节的代价与回报
| 维度 | 收益 | 代价 |
|---|---|---|
| 性能 | 头部开销小,延迟低 | 无拥塞控制可能危害网络 |
| 可靠性 | 应用层灵活定制 | 应用层必须自己实现 |
| 部署 | 穿透中间设备能力强 | 部分网络限制 UDP 流量 |
| 可扩展性 | QUIC 等协议可在其上构建 | 缺少特性需要重新实现 |
UDP 的简洁不是偷懒,而是一种深思熟虑的克制。它只提供传输层最核心的功能(复用/分用),其余一切交给上层。正是这种克制,让 UDP 在 40 多年后仍然是新协议的最佳载体。
参考资料
- RFC 768 - User Datagram Protocol — UDP 协议原始定义
- RFC 793 - Transmission Control Protocol — TCP 协议定义
- RFC 2460 - IPv6 Specification — IPv6 下 UDP 校验和强制要求
- End-to-End Arguments in System Design — 端到端原则论文
- RFC 9000 - QUIC: A UDP-Based Multiplexed and Secure Transport — QUIC 协议标准
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






