3. UDP 校验和计算:看似简单,坑却不少
UDP 校验和,很多人觉得就是算个和,没什么大不了的。说实话,我刚接触网络协议时也这么想。直到有一次,我在一个嵌入式项目里排查了整整两天,才发现是校验和覆盖范围搞错了——嗯,从那以后我再也不敢小看这个字段了。
这一节,我们就把 UDP 校验和掰开揉碎。从它到底覆盖哪些数据,到那个让人迷惑的“伪首部”,再到发送端和接收端各自怎么算,最后聊聊那个“可选性”到底是怎么回事。
3.1 校验和覆盖范围:不只是 UDP 报文
很多人以为 UDP 校验和只算 UDP 头部 + 数据。错。它其实覆盖了三部分:伪首部 + UDP 头部 + UDP 数据。
你想想看,如果只算 UDP 报文本身,那 IP 地址错了怎么办?数据包送错了主机,UDP 层根本不知道。伪首部的作用,就是把 IP 层的关键信息“借”过来,让 UDP 也能校验源 IP 和目的 IP 是否正确。
校验和覆盖范围总结:
- 伪首部(12 字节):源 IP、目的 IP、协议类型、UDP 长度
- UDP 头部(8 字节):源端口、目的端口、长度、校验和本身
- UDP 数据(可变长度):应用层载荷
注意:校验和字段本身在校验和计算时视为 0。
我在项目中遇到过一个问题:某款硬件网卡在接收 UDP 广播包时,偶尔会丢弃正常数据。排查到最后,发现是驱动层在校验和时没有包含伪首部,导致源 IP 校验失败。说白了,就是覆盖范围没对齐。
3.2 伪首部的概念与构造
伪首部(Pseudo Header)是个虚拟结构。它不参与网络传输,只在计算校验和时临时拼出来。接收端收到报文后,也会用同样的方式构造伪首部,重新计算校验和。
伪首部的结构很简单,就 12 个字节:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 源 IP 地址 | 4 | IPv4 头部中的源地址 |
| 目的 IP 地址 | 4 | IPv4 头部中的目的地址 |
| 零 | 1 | 固定为 0x00 |
| 协议类型 | 1 | UDP 为 0x11(17) |
| UDP 长度 | 2 | UDP 头部 + 数据的字节数 |
构造伪首部的代码,我习惯这样写:
// 伪首部结构体(仅用于校验和计算)
struct pseudo_header {
uint32_t src_ip; // 源 IP
uint32_t dst_ip; // 目的 IP
uint8_t zero; // 固定 0
uint8_t protocol; // 0x11
uint16_t udp_length; // UDP 头部 + 数据长度
};
小技巧:伪首部中的 UDP 长度字段,和 UDP 头部里的长度字段是同一个值。计算时直接从 UDP 头部取就行,不用重新算。
3.3 校验和计算步骤
UDP 校验和用的是 16 位二进制反码求和。名字听着唬人,其实步骤很清晰。
发送端计算步骤
- 拼装数据:把伪首部、UDP 头部、UDP 数据按顺序拼成一个连续的字节流。
- 填充零:如果总字节数是奇数,末尾补一个 0x00 字节(仅用于计算)。
- 校验和字段置零:UDP 头部中的校验和字段在计算时视为 0。
- 16 位累加:每 2 个字节作为一个 16 位整数,进行二进制反码求和。
- 取反:将累加结果按位取反,得到最终的校验和。
- 填入字段:把校验和写入 UDP 头部的校验和字段。
我曾经在调试一个 VoIP 设备时,发现发送的 UDP 包校验和总是算不对。查了半天,原来是伪首部里的协议类型写成了 0x06(TCP),而不是 0x11。这种低级错误,排查起来真的很痛苦。
接收端计算步骤
- 拼装数据:同样构造伪首部,加上收到的 UDP 头部和数据。
- 填充零:奇数长度补 0x00。
- 16 位累加:对全部数据进行二进制反码求和。
- 判断结果:如果结果为 0xFFFF(全 1),说明校验通过;否则丢弃报文。
注意:接收端计算时,UDP 头部中的校验和字段是实际值,不是 0。因为反码求和的特性,正确的校验和累加后正好是 0xFFFF。
避坑指南:我曾经遇到过接收端校验和总是失败的情况,最后发现是网卡硬件已经帮我们算了一遍校验和,并且把错误的校验和替换掉了。驱动层如果再算一次,反而会冲突。所以,一定要确认硬件是否已经处理了校验和。
3.4 校验和的可选性
UDP 校验和是可选的。你没看错,IPv4 下的 UDP 允许校验和为 0,表示发送端没有计算校验和。接收端如果收到校验和为 0 的 UDP 报文,可以选择不校验,直接交给上层。
但这里有个大坑:IPv6 下的 UDP 校验和是强制的,不能为 0。如果 IPv6 UDP 报文的校验和为 0,接收端必须丢弃。
| 场景 | 校验和是否可选 | 说明 |
|---|---|---|
| IPv4 UDP | 可选 | 校验和为 0 表示未计算 |
| IPv6 UDP | 强制 | 校验和不能为 0 |
| IPv4 UDP(隧道/封装) | 通常强制 | 避免数据静默损坏 |
为什么 IPv4 允许关闭校验和?说白了,是为了性能。在一些高速网络场景下,上层协议(比如应用层)已经做了完整性校验,UDP 层再算一遍就是浪费 CPU。但我不建议你这么做——网络环境复杂,中间设备可能篡改数据,没有校验和,数据坏了你都不知道。
我的建议:除非你明确知道上层协议已经做了端到端校验,否则永远不要关闭 UDP 校验和。我在一个金融交易系统中见过因为关闭校验和导致的数据静默损坏,后果很严重。
3.5 核心逻辑流程图
下面这张图展示了 UDP 校验和从发送端到接收端的完整流程。我特意把伪首部的构造和校验和的判断逻辑都画出来了,方便你对照理解。
这张图把发送端和接收端的流程并列展示。你可以看到,两边都先构造伪首部,然后做 16 位反码求和。区别在于:发送端把结果取反后填入校验和字段,接收端直接判断累加结果是否为 0xFFFF。
嗯,到这里 UDP 校验和的核心内容就讲完了。记住三点:伪首部不能漏、反码求和步骤不能乱、IPv6 下校验和不能关。下次调试网络问题时,希望你能少踩几个坑。