mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3590 字
10 分钟
为什么 UDP 头只有 8 个字节
2023-04-22

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 Port16 位发送方端口(可选填 0)
Destination Port16 位接收方端口(必须填写)
Length16 位UDP 头 + 数据的总长度(字节)
Checksum16 位校验和(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 Number4字节流编号,保证顺序
Acknowledgment4确认收到的数据
Data Offset + Flags1.5头部长度 + 控制标志
Window2流量控制:接收窗口大小
Urgent Pointer0.5紧急数据指针(已过时)

TCP 用 12 个额外的字节(最小头部差值)换来了可靠的字节流传输。但如果你不需要这些功能,这 12 字节就是纯粹的浪费。

3.3 头部开销的实际影响#

以一个典型的 DNS 查询为例:

协议IP 头传输层头数据总计传输层开销占比
UDP20B8B50B78B10.3%
TCP20B20B50B90B22.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#

flowchart LR subgraph 设计目标 S[简单性] --> M[最小 overhead] M --> F[快速传输] end

核心原则:如果功能不需要,就不要添加。这不是偷懒,而是一种克制。

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 个 RTT

DNS 原始设计(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 的协议栈#

flowchart TB subgraph 应用层 A[HTTP/3] end subgraph QUIC 传输层 R[可靠传输] F[流量控制] C[拥塞控制] S[加密 TLS 1.3] M[多路复用] end subgraph UDP U[8 字节头部] end A --> M M --> S S --> R R --> F F --> C C --> U

7.3 QUIC vs TCP + TLS 的详细对比#

特性TCP + TLSQUIC为什么有差异
连接建立3-4 RTT0-1 RTTQUIC 将握手和加密合并
队头阻塞有(一个流阻塞所有)无(流间独立)QUIC 多路复用在传输层
流量控制连接级连接级 + 流级QUIC 更细粒度
拥塞控制内核实现用户态可插拔QUIC 可以快速迭代算法
连接迁移不支持(4 元组绑定)支持(Connection ID)QUIC 用 CID 标识连接
协议升级需要升级内核只需升级应用QUIC 在用户态实现

7.4 QUIC 头部的权衡#

QUIC 在 UDP 之上添加了自己的头部。一个 QUIC 包 = UDP 头(8B)+ QUIC 头(变长,约 20-40B)+ 加密后的有效载荷。

看起来 QUIC 的总开销比裸 TCP 还大。但 QUIC 的价值不在于减少头部开销,而在于:

  • 合并握手,减少 RTT
  • 消除队头阻塞
  • 支持连接迁移
  • 可快速迭代

八、其他传输层协议的头部对比#

协议头部大小年代设计目标
UDP8 字节1980最小开销的数据报传输
TCP20+ 字节1981可靠的字节流传输
SCTP12+ 字节2000多流、多宿、部分可靠
DCCP16 字节2006有拥塞控制但不保证可靠
UDP-Lite8 字节2004允许部分校验(容忍位错误)

SCTP 和 DCCP 试图在 UDP 和 TCP 之间找到中间地带,但因为需要内核支持和中间设备兼容,它们始终没有大规模部署。UDP 的简洁反而成了最大的优势:因为头部太简单,几乎没有中间设备会干扰 UDP 包。

九、总结#

9.1 UDP 头部只有 8 字节的设计哲学#

设计决策理由
最小 overhead适合小数据包、高频率场景
无连接状态服务器可服务更多客户端
无可靠性机制应用层按需实现,避免过度设计
可选校验和在可靠链路上节省 CPU 开销
固定长度头部解析简单,无需复杂的选项处理

9.2 8 字节的代价与回报#

维度收益代价
性能头部开销小,延迟低无拥塞控制可能危害网络
可靠性应用层灵活定制应用层必须自己实现
部署穿透中间设备能力强部分网络限制 UDP 流量
可扩展性QUIC 等协议可在其上构建缺少特性需要重新实现

UDP 的简洁不是偷懒,而是一种深思熟虑的克制。它只提供传输层最核心的功能(复用/分用),其余一切交给上层。正是这种克制,让 UDP 在 40 多年后仍然是新协议的最佳载体。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 UDP 头只有 8 个字节
https://blog.souloss.com/posts/why-the-design/why-udp-header-is-only-8-bytes/
作者
Souloss
发布于
2023-04-22
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时