UDP校验和全解析:原理、作用与计算方法
在当今高度互联的世界中,网络通信协议是信息交换的基石。用户数据报协议(UDP,User Datagram Protocol)作为传输层协议之一,以其简单、高效、低开销的特点,在许多应用场景中扮演着重要角色,例如域名系统(DNS)、动态主机配置协议(DHCP)、流媒体传输(如在线视频、游戏)和许多实时应用。然而,UDP的“不可靠”特性也广为人知,它不保证数据包的顺序、不保证不丢失、也不保证不重复。为了在一定程度上弥补这种不可靠性,UDP引入了校验和(Checksum)机制,用于检测数据在传输过程中是否发生了错误。本文将深入探讨UDP校验和的定义、工作原理、具体作用以及详细的计算方法。
一、什么是UDP校验和?
UDP校验和是一个16位的字段,位于UDP报文头的第5和第6个字节(从0开始计数)。它的主要目的是检测UDP报文(包括UDP头部和UDP数据部分)在从源端发送到目的端的过程中是否发生了比特错误(bit errors)。
值得注意的是,在IPv4协议栈中,UDP校验和的使用是可选的。如果发送方没有计算校验和,则该字段应置为全0(16个0比特)。然而,如果计算出的校验和恰好为0,则它必须以全1(即0xFFFF,反码表示的0)的形式存储,以区别于“未使用校验和”的情况。在IPv6中,UDP校验和则是强制性的,不能被禁用。这是因为IPv6头部不再包含对上层协议(如UDP或TCP)有效载荷的校验和计算,因此这个责任完全落在了传输层协议本身。
UDP校验和是一种简单的错误检测机制,它不能纠正错误,也不能检测所有类型的错误(例如,多个比特错误恰好相互抵消,导致校验和不变的情况,虽然概率较低)。如果接收方计算出的校验和与报文中携带的校验和不匹配,通常会默默地丢弃该UDP数据报,而不会发送任何错误通知给发送方,这符合UDP“尽力而为”的传输特性。
二、UDP校验和的作用
UDP校验和的核心作用在于数据完整性校验。具体来说,它帮助接收端判断接收到的UDP数据报在传输过程中是否被意外修改或损坏。这些损坏可能由多种因素引起,例如:
- 网络设备故障:路由器、交换机等网络设备在转发数据包时,由于硬件故障、软件bug或电磁干扰,可能导致数据包中的比特发生翻转。
- 传输介质干扰:无论是铜缆、光纤还是无线信道,都可能受到外部环境的干扰,导致信号失真,进而造成数据错误。
- 主机内存错误:在数据包被操作系统内核处理、在网卡缓冲区暂存或在应用程序内存中操作时,主机自身的内存(RAM)错误也可能导致数据损坏。
尽管UDP本身是“不可靠”的,不提供重传、流量控制等机制,但基本的错误检测对于许多应用仍然至关重要。例如:
- DNS查询与响应:错误的DNS记录可能导致用户访问错误的网站或服务。
- 简单网络管理协议(SNMP):错误的配置信息或监控数据可能导致网络管理混乱。
- 实时音视频流:虽然少量错误可以容忍(通过编解码器的纠错能力或直接忽略),但严重的、未被检测到的错误可能导致画面撕裂、声音异常等问题。UDP校验和可以帮助过滤掉一些已损坏的数据包。
通过校验和,接收方可以增加对所接收数据可信度的判断。如果校验和匹配,则数据有较大概率是完好的;如果不匹配,则数据几乎肯定是损坏的,应当被丢弃,以避免上层应用处理错误的数据。
三、UDP校验和的原理:反码求和
UDP校验和的计算基于一种称为“反码求和”(One’s Complement Sum)的算法。其基本思想是将需要校验的数据分割成一系列16位的字(word),然后对这些字进行反码算术加法运算,最后将得到的和取反码,结果即为校验和。
反码算术(One’s Complement Arithmetic):
在反码算术中,当两个16位数相加产生溢出(即结果超出了16位所能表示的范围,有进位到第17位)时,这个溢出的进位需要被回卷(wrap around)加到结果的最低位。
例如,计算 A + B (A, B 均为16位数):
1. Sum = A + B
2. 如果 Sum
大于 0xFFFF
(即产生了进位),则 Sum = (Sum & 0xFFFF) + (Sum >> 16)
。这个操作将高16位(即进位部分)加到低16位上。这个回卷操作可能需要重复进行,直到没有新的进位产生(尽管对于两个16位数相加,一次回卷就足够了)。
校验和的生成(发送端):
1. 将UDP报文中校验和字段置为0。
2. 将所有参与校验的数据(包括一个特殊的“伪头部”、UDP头部和UDP数据)视为一个连续的16位字序列。
3. 对这些16位字进行反码求和。
4. 将最终得到的和按位取反(即0变1,1变0),得到的结果就是UDP校验和。
5. 如果计算出的校验和值为0x0000,则实际存入校验和字段的是0xFFFF。
校验和的验证(接收端):
1. 将接收到的UDP报文中的所有参与校验的数据(包括伪头部、UDP头部(包含接收到的校验和字段本身)和UDP数据)视为一个连续的16位字序列。
2. 对这些16位字进行反码求和。
3. 如果传输过程中没有发生错误,那么最终得到的和(在取反之前)应该是全1(即0xFFFF)。如果对这个0xFFFF再取反,结果就是0x0000。
为什么是反码求和?
选择反码求和而不是更常见的补码求和,部分原因在于其历史背景和硬件实现的简便性。反码运算的一个特性是,如果所有数据段(包括原始校验和)相加得到的反码和是全1(0xFFFF),则表明数据未出错。这使得验证过程非常直接。
四、UDP校验和的计算方法详解
UDP校验和的计算范围不仅包括UDP头部和UDP数据,还包括一个从IP头部提取部分信息构成的“伪头部”(Pseudo Header)。引入伪头部的目的是为了双重检查,确保UDP数据报不仅内容没有损坏,而且没有被错误地路由到当前主机,或者IP头部中的关键信息(如源/目标IP地址、协议类型)没有在传输中被篡改导致数据报被错误地交付给UDP。
计算步骤:
-
构造UDP伪头部 (Pseudo Header)
伪头部是一个虚拟的数据结构,仅用于校验和计算,并不会实际在网络上传输。其结构如下(对于IPv4):- 源IP地址 (Source IP Address):32位 (4字节)
- 目的IP地址 (Destination IP Address):32位 (4字节)
- 预留字段 (Reserved/Zeroes):8位 (1字节),必须填充为0。
- 协议号 (Protocol):8位 (1字节),对于UDP,此值为17 (0x11)。
- UDP长度 (UDP Length):16位 (2字节),表示UDP头部和UDP数据的总长度(字节数)。这个长度与UDP头部中的长度字段值相同。
因此,IPv4下的伪头部总共是12字节。
对于IPv6,伪头部的结构有所不同:
* 源IP地址 (Source IP Address):128位 (16字节)
* 目的IP地址 (Destination IP Address):128位 (16字节)
* UDP长度 (UDP Length):32位 (4字节),表示UDP头部和UDP数据的总长度。注意这里是32位,与IPv4不同。
* 预留字段 (Zeroes):24位 (3字节),必须填充为0。
* 下一报头 (Next Header):8位 (1字节),对于UDP,此值为17 (0x11)。IPv6下的伪头部总共是40字节。
-
准备UDP头部
UDP头部共8字节:- 源端口号 (Source Port):16位
- 目的端口号 (Destination Port):16位
- UDP长度 (UDP Length):16位 (与伪头部中的UDP长度值一致)
- 校验和 (Checksum):16位。在计算校验和时,此字段必须暂时置为0。
-
准备UDP数据 (Payload)
这是UDP报文的实际承载内容。 -
数据对齐与填充
校验和计算要求所有数据按16位字(2字节)对齐进行累加。- 伪头部、UDP头部本身都是偶数字节,不需要特殊处理。
- UDP数据部分:如果UDP数据的字节数为奇数,则需要在末尾逻辑上追加一个全0的字节(padding byte),使其长度变为偶数,以便凑成16位的字。这个填充字节仅用于校验和计算,并不会实际添加到UDP数据报中进行传输。 UDP头部中的“UDP长度”字段仍然是原始数据的长度。
-
执行反码求和
将伪头部、置零校验和字段的UDP头部、以及(可能已逻辑填充的)UDP数据,看作一个连续的16位字序列。
初始化一个32位的累加器(或一个能处理溢出的16位累加器)为0。
遍历这个16位字序列,将每个16位字加到累加器中。“`
uint32_t sum = 0;// 累加伪头部 (以IPv4为例)
sum += (pseudo_header.source_ip >> 16) & 0xFFFF; // IP源地址高16位
sum += pseudo_header.source_ip & 0xFFFF; // IP源地址低16位
sum += (pseudo_header.dest_ip >> 16) & 0xFFFF; // IP目的地址高16位
sum += pseudo_header.dest_ip & 0xFFFF; // IP目的地址低16位
sum += (uint16_t)pseudo_header.protocol; // 协议号 (高字节为0,低字节为协议号)
sum += pseudo_header.udp_length; // UDP长度// 累加UDP头部 (校验和字段已置0)
sum += udp_header.source_port;
sum += udp_header.destination_port;
sum += udp_header.udp_length;
sum += 0; // Checksum field (already set to 0)// 累加UDP数据
uint16_t data_ptr = (uint16_t )udp_data;
int data_len_words = udp_payload_length / 2; // 完整的16位字的数量
for (int i = 0; i < data_len_words; i++) {
sum += data_ptr++;
}
// 处理奇数长度的数据
if (udp_payload_length % 2 != 0) {
sum += (((uint8_t *)data_ptr)) << 8; // 取最后一个字节,放到高8位,低8位为0
}// 处理溢出:将高16位加到低16位,直到高16位为0
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
“` -
取反码
将上一步得到的16位和(sum & 0xFFFF
)按位取反。
uint16_t checksum = (uint16_t)~sum;
-
处理特殊情况:校验和为0
如果计算得到的checksum
值为0x0000
,则实际写入UDP头部校验和字段的值应为0xFFFF
。
if (checksum == 0x0000) { checksum = 0xFFFF; }
这个checksum
值就是最终要填入UDP头部校验和字段的值。
接收端的校验过程:
接收端执行几乎相同的计算步骤:
1. 从接收到的IP包中提取源IP、目标IP、协议号,并从UDP头中获取UDP长度,构造伪头部。
2. 获取UDP头部(包括其中接收到的校验和字段)和UDP数据。
3. 如果UDP数据长度为奇数,同样在末尾逻辑填充一个0字节。
4. 将伪头部、UDP头部(此时校验和字段是接收到的值,不是0)、以及(可能填充的)UDP数据,视为16位字序列,进行反码求和。
5. uint32_t recv_sum = 0;
// … 累加伪头部 …
// … 累加UDP头部 (包括接收到的校验和) …
// … 累加UDP数据 …
while (recv_sum >> 16) { recv_sum = (recv_sum & 0xFFFF) + (recv_sum >> 16); }
- 验证结果:
如果数据在传输过程中没有发生错误,并且发送端正确计算了校验和,那么接收端计算得到的recv_sum
(在最终取反之前) 应该等于0xFFFF
。
if ((uint16_t)recv_sum == 0xFFFF) { // 数据包很可能完好 } else { // 数据包已损坏,丢弃 }
另一种等价的判断是,对recv_sum
取反,如果结果是0x0000
,则数据包完好。
if ((uint16_t)~recv_sum == 0x0000) { // 数据包很可能完好 }
一个简化的计算示例:
假设我们有以下数据(均为16进制):
* 伪头部贡献值(已预加):0x1000
* UDP头部(校验和为0)贡献值:0x2000
* UDP数据贡献值:0x3000
, 0x4000
计算:
1. Sum = 0x1000 + 0x2000 + 0x3000 + 0x4000 = 0xA000
2. 无溢出,Sum
仍为 0xA000
。
3. 取反:Checksum = ~0xA000 = 0x5FFF
。
这个 0x5FFF
将被填入UDP头部的校验和字段。
接收端验证:
接收到包含校验和 0x5FFF
的UDP包。
1. 将所有部分(包括校验和字段本身)相加:
RecvSum = 0x1000 (pseudo) + 0x2000 (UDP hdr part1) + 0x5FFF (checksum) + 0x3000 (data1) + 0x4000 (data2)
RecvSum = 0x1000 + 0x2000 + 0x5FFF + 0x3000 + 0x4000 = 0xFFFF
(计算过程注意进位)
0x1000 + 0x2000 = 0x3000
0x3000 + 0x5FFF = 0x8FFF
0x8FFF + 0x3000 = 0xBFFF
0xBFFF + 0x4000 = 0xFFFF
RecvSum
为0xFFFF
。这表明数据很可能未损坏。
如果传输中 0x3000
变成了 0x3001
:
1. RecvSum = 0x1000 + 0x2000 + 0x5FFF + 0x3001 + 0x4000 = 0x10000
2. 发生溢出,回卷:RecvSum = (0x10000 & 0xFFFF) + (0x10000 >> 16) = 0x0000 + 0x0001 = 0x0001
3. RecvSum
为 0x0001
,不等于 0xFFFF
。校验失败,数据包将被丢弃。
五、UDP校验和的局限性
虽然UDP校验和提供了一层基本的保护,但它并非万无一失:
- 不能检测所有错误:反码和校验算法本身存在一些弱点。例如,如果数据中两个16位字发生对称错误(一个增加X,另一个减少X),或者多个比特的错误恰好相互抵消,使得最终的和不变,校验和就无法检测出这类错误。
- 不能纠正错误:UDP校验和只能检测错误,不能纠正错误。一旦检测到错误,唯一的处理方式通常是丢弃数据包。
- 可选性 (IPv4):在IPv4中,如果发送方选择不计算校验和(将校验和字段置0),那么接收方也就无法进行错误检测。这在某些性能敏感或错误容忍度高的局域网环境中可能被接受,但在广域网或不可靠网络中会增加数据损坏的风险。
- 性能开销:虽然计算相对简单,但在高速网络环境下,对每个数据包进行校验和计算和验证仍然会消耗一定的CPU资源。现代CPU通常有指令集优化,网卡也常支持校验和卸载(Checksum Offloading),将计算任务交给硬件完成,以减轻CPU负担。
六、总结
UDP校验和是UDP协议中一个简单而有效的错误检测机制。它通过对UDP伪头部、UDP头部和UDP数据执行反码求和运算,并对结果取反来生成一个16位的校验值。接收端通过重复此计算(包含接收到的校验和本身)来验证数据的完整性。如果计算结果不符合预期(通常是反码和为0xFFFF),则表明数据在传输过程中可能已损坏,应予以丢弃。
尽管UDP校验和存在局限性,不能保证100%的错误检测,也不能纠正错误,但它在UDP“尽力而为”的服务模型下,为数据传输提供了一层重要的基础保护,特别是在IPv6中其使用已成为强制要求。理解UDP校验和的原理和计算方法,对于网络编程、故障排查以及深入学习计算机网络协议都具有重要意义。它体现了在协议设计中,如何在简单性、效率和可靠性之间进行权衡。