头部字段总览(ASCII 示意)
IPv4 头部(20字节,无选项)
0 8 16 24 31
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 版本(4) |IHL(4)| DS字段(6) |ECN(2)| 总长度(16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 标识符(16) |标志(3)| 片偏移(13) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TTL(8) | 协议(8) | 首部校验和(16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源IP地址(32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 目的IP地址(32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
IPv6 头部(40字节,固定)
0 8 16 24 31
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 版本(4) | 流量类(8) | 流标签(20) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 有效载荷长度(16) |下一头部(8)| 跳数限制(8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 源地址(128位) |
| |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 目的地址(128位) |
| |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
各字段逐一详解
1. 版本字段(Version)—— 4 位
作用:标识这个数据报用的是哪个版本的 IP 协议。
- IPv4 填写
4 - IPv6 填写
6
关键点:IPv4 和 IPv6 头部只有版本字段的位置相同,其他字段完全不同。因此这两种协议不能直接互通,路由器或主机必须分别处理。
支持同时运行两种协议的叫做双栈(Dual Stack)。
版本字段示意(4位):
IPv4: 0100 (十进制 4)
IPv6: 0110 (十进制 6)
2. 首部长度字段(IHL, Internet Header Length)—— 4 位(仅 IPv4)
作用:告诉接收方 IPv4 头部有多长,单位是 32 位字(4字节)。
正常情况下(无选项)值为 5,即:
5
×
4
=
20
字节
5 \times 4 = 20 \text{ 字节}
5×4=20 字节
由于字段只有 4 位,最大值为 15,所以:
IPv4 头部最大长度
=
15
×
4
=
60
字节
\text{IPv4 头部最大长度} = 15 \times 4 = 60 \text{ 字节}
IPv4 头部最大长度=15×4=60 字节
IPv6 没有这个字段,因为 IPv6 头部固定是 40 字节,不需要说明长度。
3. 服务类型字节 / 流量类字节(ToS / Traffic Class)—— 8 位
这个字段经历了重新定义,现在被拆成两部分:
bit: 7 6 5 4 3 2 1 0
[ DS字段(6位) ][ECN(2位)]
- DS 字段(Differentiated Services Field,6位):用于区分服务,例如让语音通话的数据包比普通文件传输更优先转发。
- ECN 字段(Explicit Congestion Notification,2位):显式拥塞通知,路由器可以用它告诉两端"网络快堵了,请减速",而不必真的丢包。
这两个字段的定义同时适用于 IPv4 和 IPv6。
4. 总长度字段(Total Length)—— 16 位(仅 IPv4)
作用:整个 IPv4 数据报的字节总数,包含头部和数据。
最大值
=
2
16
−
1
=
65535
字节
\text{最大值} = 2^{16} - 1 = 65535 \text{ 字节}
最大值=216−1=65535 字节
为什么必须有这个字段?
以太网有最小帧长度限制(64字节),如果 IP 数据报比较小,以太网会在后面补零填充。如果没有总长度字段,接收方就无法区分哪些是真实数据、哪些是填充的零。
以太网帧示意:
+-------------------+---------------------+----------+
| 以太网头(14字节) | IPv4数据报(20字节) | 填充(26字节) |
+-------------------+---------------------+----------+
<-- 总长度字段=20 -->
没有这个字段就不知道后面26字节是填充
实际使用限制:
- 虽然理论最大 65535 字节,但大多数链路层(如以太网)无法一次承载这么大的包,需要分片
- IPv4 主机至少要能接收 576 字节的数据报
- 很多基于 UDP 的应用(DNS、DHCP 等)限制数据在 512 字节以内,就是为了避免分片
IPv6 对应字段:叫做有效载荷长度(Payload Length),只计算头部之后的数据(不含 40 字节基本头部),扩展头包含在内。同样 16 位,最大 65535 字节的载荷。
IPv6 还支持**超大数据报(Jumbogram)**选项,理论上载荷可达:
2 32 − 1 = 4,294,967,295 字节 ≈ 4 GB 2^{32} - 1 = 4{,}294{,}967{,}295 \text{ 字节} \approx 4 \text{ GB} 232−1=4,294,967,295 字节≈4 GB
5. 标识符字段(Identification)—— 16 位(仅 IPv4 基本头)
作用:给每个发出去的 IPv4 数据报贴上唯一编号,用于分片重组。
发送方每发一个数据报,内部计数器 +1,把值填入此字段。当一个大数据报被切成多个分片时,所有分片都有相同的标识符,接收方就知道它们属于同一个原始数据报。
原始数据报(标识符=1000)被分片:
分片1:标识符=1000, 偏移=0
分片2:标识符=1000, 偏移=185
分片3:标识符=1000, 偏移=370
^--- 相同标识符,说明同属一个数据报
在 IPv6 中,这个字段移到了分片扩展头里,基本头部没有。
6. 生存时间字段(TTL / Hop Limit)—— 8 位
作用:防止数据报在网络中无限循环。每经过一个路由器,TTL 值 减 1,减到 0 就丢弃并通知发送方。
推荐初始值为 64,常见的还有 128 或 255。
最多经过路由器数
=
初始TTL值
\text{最多经过路由器数} = \text{初始TTL值}
最多经过路由器数=初始TTL值
历史趣事:TTL 字段最初设计是以秒为单位的最大生存时间,但实际路由器处理包的时间远小于 1 秒,这个语义早就名存实亡。IPv6 干脆将其改名为跳数限制(Hop Limit),更贴合实际用途。
TTL 递减过程:
发送方(TTL=64) -> 路由器A(TTL=63) -> 路由器B(TTL=62) -> ... -> TTL=0 -> 丢弃+ICMP通知
7. 协议字段(Protocol)—— 8 位(仅 IPv4)
作用:告诉接收方,IP 数据报的载荷是什么协议的数据(解复用)。
| 常见值 | 协议 |
|---|---|
| 1 | ICMP |
| 4 | IPv4-in-IPv4(隧道封装) |
| 6 | TCP |
| 17 | UDP |
IPv6 对应字段:叫做下一头部(Next Header),作用相同,但还可以指向 IPv6 扩展头的类型,更加通用灵活。
8. 首部校验和字段(Header Checksum)—— 16 位(仅 IPv4)
作用:对 IPv4 头部做完整性校验,确保头部没有在传输中损坏。
重要:校验和只覆盖头部,不覆盖数据部分。数据的完整性需要 TCP/UDP 等上层协议自己负责。
每过一个路由器都要重新计算,因为 TTL 每次都变了。
IPv6 没有校验和字段,理由是:
- 光纤传输误码率极低
- 上层协议(TCP/UDP)已有自己的校验和
- 省掉这个字段可以让路由器转发更快
校验和计算方法(互联网校验和算法):
checksum = ∼ ( ∑ i 16位字 i ) \text{checksum} = \sim\left(\sum_{i} \text{16位字}_i\right) checksum=∼(i∑16位字i)
即把头部所有 16 位字相加(带进位回绕),最后取反。
9. 源/目的 IP 地址字段
IPv4:各 32 位,通常标识网络上的一个接口。
IPv4 最大地址数
=
2
32
=
4,294,967,296
≈
43
亿
\text{IPv4 最大地址数} = 2^{32} = 4{,}294{,}967{,}296 \approx 43 \text{ 亿}
IPv4 最大地址数=232=4,294,967,296≈43 亿
IPv6:各 128 位,地址空间极其巨大:
IPv6 最大地址数
=
2
128
≈
3.4
×
10
38
\text{IPv6 最大地址数} = 2^{128} \approx 3.4 \times 10^{38}
IPv6 最大地址数=2128≈3.4×1038
这意味着地球表面每平方米可以分配约
3.9
×
10
18
3.9 \times 10^{18}
3.9×1018 个地址,完全不用担心耗尽。
字段对照表(IPv4 vs IPv6)
字段功能 IPv4 字段名 IPv6 字段名
---------------------------------------------------------
协议版本 Version(4位) Version(4位)
头部长度 IHL(4位) 无(固定40字节)
服务类型 ToS -> DS+ECN(8位) Traffic Class(8位)
数据报总长度 Total Length(16位) Payload Length(16位)
分片标识 Identification(16位) 在分片扩展头中
分片控制 Flags+Fragment Offset 在分片扩展头中
存活跳数 TTL(8位) Hop Limit(8位)
上层协议 Protocol(8位) Next Header(8位)
头部校验和 Header Checksum(16位) 无
源地址 Source IP(32位) Source IP(128位)
目的地址 Dest IP(32位) Dest IP(128位)
选项 Options(可变) 扩展头(链式)
数据报大小限制关系图
IPv6 下一头部链式结构
完整可运行 C++ 演示代码
#include <iostream>
#include <cstdint>
#include <cstring>
#include <arpa/inet.h> // htons, htonl, ntohs, ntohl, inet_addr, inet_ntoa
#include <netinet/in.h> // struct in_addr
// ============================================================
// IPv4 头部结构(不含选项,标准20字节)
// __attribute__((packed)) 防止编译器插入对齐填充字节
// ============================================================
struct __attribute__((packed)) IPv4Header {
uint8_t ver_ihl; // 高4位=版本(4), 低4位=IHL(首部长度,单位4字节)
uint8_t tos; // 原服务类型,现为 DS字段(6位) + ECN(2位)
uint16_t total_len; // 整个数据报总字节数(头+数据),网络字节序
uint16_t id; // 标识符,分片重组用,网络字节序
uint16_t flags_offset; // 高3位=标志(Flags), 低13位=片偏移,网络字节序
uint8_t ttl; // 生存时间/跳数限制,每过一个路由器减1
uint8_t protocol; // 上层协议号:6=TCP, 17=UDP, 1=ICMP
uint16_t checksum; // 仅覆盖头部的校验和,网络字节序
uint32_t src_ip; // 源IP地址,网络字节序
uint32_t dst_ip; // 目的IP地址,网络字节序
};
// ============================================================
// 计算互联网校验和(Internet Checksum)
// 原理:将数据按16位分组累加,进位回绕,最后取反
// 参数:buf=要计算的数据指针, len=字节数
// ============================================================
uint16_t internet_checksum(const uint8_t* buf, int len) {
uint32_t sum = 0;
// 每次取2字节(16位)累加
while (len > 1) {
uint16_t word;
std::memcpy(&word, buf, 2); // 避免未对齐访问
sum += word;
buf += 2;
len -= 2;
}
// 如果数据长度是奇数,补一个0字节再加
if (len == 1) {
uint16_t last = (*buf) << 8; // 高字节放实际数据,低字节补0
sum += last;
}
// 将32位结果的高16位(进位)加回低16位(回绕进位)
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
// 取反得到校验和
return static_cast<uint16_t>(~sum);
}
// ============================================================
// 打印 IPv4 头部所有字段(以人类可读方式)
// 注意:从网络字节序读取时需要用 ntohs/ntohl 转换
// ============================================================
void print_ipv4_header(const IPv4Header& h) {
std::cout << "=== IPv4 头部字段 ===" << std::endl;
// 从 ver_ihl 字节中拆出版本和IHL
int version = h.ver_ihl >> 4; // 高4位
int ihl = h.ver_ihl & 0x0F; // 低4位
std::cout << "版本(Version): " << version << std::endl;
std::cout << "首部长度(IHL): " << ihl << " * 4 = " << ihl*4 << " 字节" << std::endl;
// DS字段和ECN从ToS字节拆出
int ds_field = (h.tos >> 2) & 0x3F; // 高6位
int ecn = h.tos & 0x03; // 低2位
std::cout << "DS字段: " << ds_field << std::endl;
std::cout << "ECN: " << ecn << std::endl;
std::cout << "总长度(Total Len): " << ntohs(h.total_len) << " 字节" << std::endl;
std::cout << "标识符(ID): " << ntohs(h.id) << std::endl;
// 从 flags_offset 拆出标志位和片偏移
uint16_t fo = ntohs(h.flags_offset);
int flag_df = (fo >> 14) & 1; // DF位:Don't Fragment
int flag_mf = (fo >> 13) & 1; // MF位:More Fragments
int offset = fo & 0x1FFF; // 低13位:片偏移(单位8字节)
std::cout << "DF标志(不分片): " << flag_df << std::endl;
std::cout << "MF标志(更多分片): " << flag_mf << std::endl;
std::cout << "片偏移: " << offset << " * 8 = " << offset*8 << " 字节" << std::endl;
std::cout << "TTL: " << (int)h.ttl << std::endl;
std::cout << "协议(Protocol): " << (int)h.protocol;
if (h.protocol == 6) std::cout << " (TCP)";
else if (h.protocol == 17) std::cout << " (UDP)";
else if (h.protocol == 1) std::cout << " (ICMP)";
std::cout << std::endl;
std::cout << "首部校验和: 0x" << std::hex << ntohs(h.checksum) << std::dec << std::endl;
// 将网络字节序IP转为点分十进制字符串
struct in_addr src, dst;
src.s_addr = h.src_ip;
dst.s_addr = h.dst_ip;
std::cout << "源IP: " << inet_ntoa(src) << std::endl;
std::cout << "目的IP: " << inet_ntoa(dst) << std::endl;
}
// ============================================================
// 演示 TTL 递减(模拟经过多个路由器)
// ============================================================
void demo_ttl_decrement() {
std::cout << "\n=== TTL 递减演示(模拟经过路由器)===" << std::endl;
uint8_t ttl = 5; // 初始TTL=5,只允许经过5个路由器
int hop = 0;
while (ttl > 0) {
std::cout << "第" << hop << "跳: TTL=" << (int)ttl;
if (hop == 0) std::cout << " (发送方)";
std::cout << std::endl;
ttl--;
hop++;
}
std::cout << "第" << hop << "跳: TTL=0 -> 数据报被丢弃,发送ICMP超时消息" << std::endl;
}
// ============================================================
// 演示地址空间大小对比
// ============================================================
void demo_address_space() {
std::cout << "\n=== IP地址空间对比 ===" << std::endl;
// IPv4: 2^32
uint64_t ipv4_count = 1ULL << 32;
std::cout << "IPv4地址数量(2^32): " << ipv4_count << " 约43亿" << std::endl;
// IPv6: 2^128 太大,用科学计数法表示
// 2^128 = 2^10 * 2^10 * 2^10 * 2^10 * ...
// 约等于 3.4 × 10^38
std::cout << "IPv6地址数量(2^128): 约 3.4 × 10^38" << std::endl;
std::cout << "地球表面每平方米可分配约 3.9 × 10^18 个IPv6地址" << std::endl;
}
int main() {
// ---- 构造一个 IPv4 头部 ----
IPv4Header hdr;
std::memset(&hdr, 0, sizeof(hdr));
hdr.ver_ihl = (4 << 4) | 5; // 版本=4, IHL=5(20字节)
hdr.tos = 0; // 普通服务,DS=0, ECN=0
hdr.total_len = htons(20 + 512); // 头部20 + UDP数据512
hdr.id = htons(54321); // 标识符
hdr.flags_offset = htons(0x4000); // DF=1(不允许分片), offset=0
hdr.ttl = 64; // 推荐初始值
hdr.protocol = 17; // UDP
hdr.checksum = 0; // 先置0再计算
hdr.src_ip = inet_addr("192.168.0.1");
hdr.dst_ip = inet_addr("8.8.8.8");
// 计算首部校验和
hdr.checksum = internet_checksum(
reinterpret_cast<const uint8_t*>(&hdr),
sizeof(hdr)
);
// 打印头部
print_ipv4_header(hdr);
// 验证校验和(对正确的头部再算一次应该得0)
uint16_t verify = internet_checksum(
reinterpret_cast<const uint8_t*>(&hdr),
sizeof(hdr)
);
std::cout << "校验和验证(应为0x0000): 0x"
<< std::hex << verify << std::dec << std::endl;
// TTL 演示
demo_ttl_decrement();
// 地址空间演示
demo_address_space();
return 0;
}
https://godbolt.org/z/qqj6znb9c
编译运行
g++ -o ipv4_fields ipv4_fields.cpp
./ipv4_fields
预期输出
=== IPv4 头部字段 ===
版本(Version): 4
首部长度(IHL): 5 * 4 = 20 字节
DS字段: 0
ECN: 0
总长度(Total Len): 532 字节
标识符(ID): 54321
DF标志(不分片): 1
MF标志(更多分片): 0
片偏移: 0 * 8 = 0 字节
TTL: 64
协议(Protocol): 17 (UDP)
首部校验和: 0x????
源IP: 192.168.0.1
目的IP: 8.8.8.8
校验和验证(应为0x0000): 0x0
=== TTL 递减演示(模拟经过路由器)===
第0跳: TTL=5 (发送方)
第1跳: TTL=4
第2跳: TTL=3
第3跳: TTL=2
第4跳: TTL=1
第5跳: TTL=0 -> 数据报被丢弃,发送ICMP超时消息
=== IP地址空间对比 ===
IPv4地址数量(2^32): 4294967296 约43亿
IPv6地址数量(2^128): 约 3.4 × 10^38
地球表面每平方米可分配约 3.9 × 10^18 个IPv6地址
校验和算法图解
头部字节流(以2字节为单位):
[0x4500] [0x0214] [0xD431] [0x4000] [0x4011] [0x0000] [0xC0A8] [0x0001] [0x0808] [0x0808]
| | | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+
全部相加(带进位回绕)
|
取反
|
= checksum
互联网校验和数学表达
设头部分成
n
n
n 个 16 位字
w
1
,
w
2
,
…
,
w
n
w_1, w_2, \ldots, w_n
w1,w2,…,wn,求和时进位回绕(反码加法):
S
=
w
1
⊕
+
w
2
⊕
+
⋯
⊕
+
w
n
S = w_1 \oplus_+ w_2 \oplus_+ \cdots \oplus_+ w_n
S=w1⊕+w2⊕+⋯⊕+wn
其中
⊕
+
\oplus_+
⊕+ 表示反码加法(进位加回最低位)。校验和为:
checksum
=
∼
S
\text{checksum} = \sim S
checksum=∼S
验证时,将包含校验和字段的头部再做一次同样的求和,结果应为:
∼
0
=
0
xFFFF
→
等价
0
\sim 0 = 0\text{xFFFF}\ \xrightarrow{\text{等价}} 0
∼0=0xFFFF 等价0
(因为
S
+
∼
S
=
0
xFFFF
S + \sim S = 0\text{xFFFF}
S+∼S=0xFFFF,再取反为
0
0
0)
本节知识点总结
小结
| 字段 | IPv4 | IPv6 | 说明 |
|---|---|---|---|
| Version | 4位,值=4 | 4位,值=6 | 两者位置相同,值不同 |
| IHL | 4位 | 无 | IPv6头部长度固定40字节 |
| ToS/Traffic Class | 8位(DS+ECN) | 8位(DS+ECN) | 定义相同 |
| 长度字段 | Total Length | Payload Length | IPv6不算基本头 |
| 标识符 | 16位 | 在分片扩展头 | 分片重组用 |
| TTL | 8位 | Hop Limit(8位) | IPv6改了名字 |
| 协议 | Protocol(8位) | Next Header(8位) | IPv6还指向扩展头 |
| 校验和 | 16位(仅头部) | 无 | IPv6依赖上层校验 |
| 源地址 | 32位 | 128位 | IPv6地址空间大得多 |
| 目的地址 | 32位 | 128位 | 同上 |
5.2.2 互联网校验和(Internet Checksum)
一、什么是校验和?
互联网校验和(Internet Checksum)是一个 16 位的数学求和值,用来判断接收到的数据包是否和发送时的一致。
注意:互联网校验和 不是 循环冗余校验(CRC)。CRC 的错误检测能力更强,而互联网校验和计算更简单、开销更低,适合在 IP 层快速使用。
二、核心概念:反码(One’s Complement)
在理解校验和之前,先搞清楚两个概念:
2.1 二进制反码(One’s Complement)
把一个二进制数的每一位取反(0 变 1,1 变 0),就是它的反码。
∼
x
=
按位取反
(
x
)
\sim x = \text{按位取反}(x)
∼x=按位取反(x)
例如:
∼
(
1110
0101
0000
0000
)
2
=
(
0001
1010
1111
1111
)
2
\sim (1110\ 0101\ 0000\ 0000)_2 = (0001\ 1010\ 1111\ 1111)_2
∼(1110 0101 0000 0000)2=(0001 1010 1111 1111)2
2.2 反码加法(One’s Complement Addition)
反码加法和普通加法的区别:如果最高位产生了进位(carry),要把这个进位加回到结果的最低位,称为"端回进位(End-Around Carry)"。
A
+
反码
B
=
(
A
+
B
)
的普通和,若有进位则加回
A +_{\text{反码}} B = (A + B)\ \text{的普通和,若有进位则加回}
A+反码B=(A+B) 的普通和,若有进位则加回
例如:
E500
+ 1AFF
------
FFFF ← 没有进位,直接得到 FFFF
再例如有进位的情况:
E3 4F
+ 23 96
-------
1 07 E5 ← 产生了进位 1
+1 ← 把进位加回
-------
07 E6
三、发送方:如何计算校验和?
步骤如下:
1. 将 IP 头部的 Checksum 字段暂时设为 0x0000
2. 把整个头部按 16 位(2字节)为一组,做反码累加求和
3. 对求和结果再取一次反码(按位取反)
4. 把最终结果填入 Checksum 字段
用公式表示:
Checksum
=
∼
(
∑
反码
每个 16 位字
)
\text{Checksum} = \sim \left( \sum_{\text{反码}} \text{每个 16 位字} \right)
Checksum=∼(反码∑每个 16 位字)
四、图 5-3 发送端详细演算
原始消息(Checksum 字段设为 00 00):
E3 4F | 23 96 | 44 27 | 99 F3 | 00 00
第一步:做普通(二进制补码)累加
E34F
+ 2396
------
1 06E5 ← 进位 1,加回 → 06E6
+ 4427
------
4B0D
+ 99F3
------
E500 (注意:E500 < FFFF,无进位)
+ 0000 (Checksum 字段为 0)
------
E500
所以反码累加和(One’s Complement Sum)= E500 \text{E500} E500
原文中写 Two’s Complement Sum 为 1E4FF,是指没有处理进位前的中间值。
处理进位后: E4FF + 1 = E500 \text{E4FF} + 1 = \text{E500} E4FF+1=E500(One’s Complement Sum)
第二步:对 E500 取反码
∼
(
1110
0101
0000
0000
)
2
=
(
0001
1010
1111
1111
)
2
=
1AFF
\sim (1110\ 0101\ 0000\ 0000)_2 = (0001\ 1010\ 1111\ 1111)_2 = \text{1AFF}
∼(1110 0101 0000 0000)2=(0001 1010 1111 1111)2=1AFF
所以 Checksum 字段 = 0x1AFF
五、接收方:如何验证校验和?
接收方把收到的**整个头部(包括 Checksum 字段)**再做一遍同样的反码求和:
验证值
=
∼
(
∑
反码
头部所有 16 位字(含 Checksum)
)
\text{验证值} = \sim \left( \sum_{\text{反码}} \text{头部所有 16 位字(含 Checksum)} \right)
验证值=∼(反码∑头部所有 16 位字(含 Checksum))
如果没有错误,验证值一定是 0x0000。
原理:
原始数据之和
+
Checksum
=
FFFF(反码全 1)
\text{原始数据之和} + \text{Checksum} = \text{FFFF(反码全 1)}
原始数据之和+Checksum=FFFF(反码全 1)
∼
FFFF
=
0000
\sim \text{FFFF} = \text{0000}
∼FFFF=0000
图 5-3 接收端验证演算
E34F
+ 2396
+ 4427
+ 99F3
+ 1AFF ← 这是 Checksum 字段
--------
FFFF (所有 16 位字的反码和)
~(FFFF) = 0000 ← 验证通过!
六、流程图(Mermaid)
七、ASCII 文本:数据包头部校验和字段位置
+--------+--------+--------+--------+
| Version| IHL | DSCP | ECN | Total Length |
+--------+--------+--------+--------+
| Identification | Flags | Fragment Offset |
+--------+--------+--------+--------+
| TTL | Protocol | Header Checksum <-- 这里 |
+--------+--------+--------+--------+
| Source IP Address |
+--------+--------+--------+--------+
| Destination IP Address |
+--------+--------+--------+--------+
八、关键规则与特殊情况
| 情况 | 说明 |
|---|---|
| Checksum 字段 = 0xFFFF | 理论上不可能出现在合法 IP 头部中。若 Checksum 为 FFFF,则发送前的求和值必须为 0,但全 0 的合法头部不存在 |
| 检验结果非零 | 说明头部有错误,数据包被直接丢弃,不发送任何错误消息 |
| 字节数为奇数 | 末尾补一个 0x00 字节,再做 16 位求和 |
| 每跳都要重算 | IP 头部的 TTL 字段每经过一个路由器就减 1,所以 Checksum 每跳都要重新计算 |
九、C++ 完整可运行代码
/**
* 互联网校验和(Internet Checksum)演示程序
* 演示 IPv4 头部校验和的计算与验证过程
*
* 编译:g++ -std=c++17 -o checksum checksum.cpp
*/
#include <iostream>
#include <vector>
#include <iomanip>
#include <cstdint>
#include <cassert>
/**
* 计算反码累加和(One's Complement Sum)
*
* 算法:
* 1. 将所有 16 位字做普通加法累加(用 32 位保存,防止溢出)
* 2. 把高 16 位(进位)加回到低 16 位(端回进位)
* 3. 重复直到没有进位
*
* @param data 待校验的字节数组
* @param length 字节数(若为奇数,末尾补 0x00)
* @return 16 位反码累加和
*/
uint16_t ones_complement_sum(const uint8_t* data, size_t length) {
uint32_t sum = 0;
// 按 2 字节(16 位)一组累加
size_t i = 0;
for (; i + 1 < length; i += 2) {
// 大端拼接:高字节在前,低字节在后
uint16_t word = (static_cast<uint16_t>(data[i]) << 8) | data[i + 1];
sum += word;
}
// 若字节数为奇数,末尾补 0x00 后再加
if (i < length) {
uint16_t word = static_cast<uint16_t>(data[i]) << 8; // 低字节补 0
sum += word;
}
// 端回进位(End-Around Carry):把高 16 位的进位加回低 16 位
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
return static_cast<uint16_t>(sum);
}
/**
* 计算互联网校验和
*
* 校验和 = ~(反码累加和)
*
* @param data 待校验的字节数组(Checksum 字段应预先置 0)
* @param length 字节数
* @return 16 位校验和
*/
uint16_t internet_checksum(const uint8_t* data, size_t length) {
return ~ones_complement_sum(data, length);
}
/**
* 验证校验和是否正确
*
* 原理:若数据正确,则对包含 Checksum 字段的完整数据
* 做反码求和后取反,结果应为 0x0000
*
* @param data 含 Checksum 字段的完整数据
* @param length 字节数
* @return true 表示校验通过(无错误)
*/
bool verify_checksum(const uint8_t* data, size_t length) {
// 对整个数据(包含 Checksum 字段)做反码求和再取反
uint16_t result = ~ones_complement_sum(data, length);
return result == 0x0000;
}
/**
* 打印字节数组(十六进制格式)
*/
void print_hex(const char* label, const uint8_t* data, size_t length) {
std::cout << label << ": ";
for (size_t i = 0; i < length; ++i) {
std::cout << std::hex << std::uppercase
<< std::setw(2) << std::setfill('0')
<< static_cast<int>(data[i]);
if (i + 1 < length) std::cout << " ";
}
std::cout << std::dec << "\n";
}
int main() {
std::cout << "========================================\n";
std::cout << " 互联网校验和(Internet Checksum)演示\n";
std::cout << "========================================\n\n";
// ------------------------------------------------
// 演示 1:教材图 5-3 的例子
// 消息:E3 4F 23 96 44 27 99 F3,Checksum 位设为 00 00
// ------------------------------------------------
std::cout << "--- 演示 1:图 5-3 教材例子 ---\n\n";
// 发送方数据(Checksum 字段 = 00 00)
uint8_t message[] = {0xE3, 0x4F, 0x23, 0x96,
0x44, 0x27, 0x99, 0xF3,
0x00, 0x00}; // 最后两字节是 Checksum 字段
print_hex("原始消息(Checksum=0000)", message, sizeof(message));
// 计算校验和
uint16_t cksum = internet_checksum(message, sizeof(message));
std::cout << "计算得到的 Checksum: 0x"
<< std::hex << std::uppercase
<< std::setw(4) << std::setfill('0') << cksum
<< std::dec << "\n";
// 断言:应该是 0x1AFF(与教材一致)
assert(cksum == 0x1AFF && "校验和计算结果与教材不符!");
std::cout << "(与教材 0x1AFF 一致 - 断言通过)\n\n";
// 填入校验和字段(大端存储)
message[8] = (cksum >> 8) & 0xFF; // 高字节
message[9] = cksum & 0xFF; // 低字节
print_hex("填入 Checksum 后的消息", message, sizeof(message));
std::cout << "\n";
// ------------------------------------------------
// 接收方验证
// ------------------------------------------------
std::cout << "--- 接收方验证 ---\n\n";
print_hex("收到的完整数据(含 Checksum)", message, sizeof(message));
bool ok = verify_checksum(message, sizeof(message));
std::cout << "校验结果:" << (ok ? "通过(0x0000)- 数据正确" : "失败 - 数据有误") << "\n\n";
// ------------------------------------------------
// 演示 2:模拟数据损坏
// ------------------------------------------------
std::cout << "--- 演示 2:模拟数据损坏 ---\n\n";
uint8_t corrupted[] = {0xE3, 0x4F, 0x23, 0x96,
0x44, 0x27, 0x99, 0xF4, // 第 8 字节从 F3 改为 F4(1 位翻转)
0x1A, 0xFF}; // Checksum 不变
print_hex("损坏的消息", corrupted, sizeof(corrupted));
bool ok2 = verify_checksum(corrupted, sizeof(corrupted));
std::cout << "校验结果:" << (ok2 ? "通过 - 未检出错误" : "失败 - 检测到数据损坏!") << "\n\n";
// ------------------------------------------------
// 演示 3:奇数字节长度(末尾补 0x00)
// ------------------------------------------------
std::cout << "--- 演示 3:奇数字节数据 ---\n\n";
// 5 字节数据,末尾补 0 后计算
uint8_t odd_data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x00}; // 手动补 0 看效果
print_hex("数据(6字节含补0)", odd_data, sizeof(odd_data));
uint8_t odd_data2[] = {0x01, 0x02, 0x03, 0x04, 0x05}; // 5 字节,内部自动补 0
uint16_t cksum3 = internet_checksum(odd_data2, sizeof(odd_data2));
std::cout << "5字节数据校验和(自动补0): 0x"
<< std::hex << std::uppercase
<< std::setw(4) << std::setfill('0') << cksum3
<< std::dec << "\n\n";
std::cout << "========================================\n";
std::cout << " 演示结束\n";
std::cout << "========================================\n";
return 0;
}
https://godbolt.org/z/7daqv6Mc3
十、运行示例输出
========================================
互联网校验和(Internet Checksum)演示
========================================
--- 演示 1:图 5-3 教材例子 ---
原始消息(Checksum=0000): E3 4F 23 96 44 27 99 F3 00 00
计算得到的 Checksum: 0x1AFF
(与教材 0x1AFF 一致 - 断言通过)
填入 Checksum 后的消息: E3 4F 23 96 44 27 99 F3 1A FF
--- 接收方验证 ---
收到的完整数据(含 Checksum): E3 4F 23 96 44 27 99 F3 1A FF
校验结果:通过(0x0000)- 数据正确
--- 演示 2:模拟数据损坏 ---
损坏的消息: E3 4F 23 96 44 27 99 F4 1A FF
校验结果:失败 - 检测到数据损坏!
--- 演示 3:奇数字节数据 ---
数据(6字节含补0): 01 02 03 04 05 00
5字节数据校验和(自动补0): 0xFAF7
========================================
演示结束
========================================
十一、总结
发送方:
Checksum = ~( 反码累加和(头部数据,Checksum位置为0) )
接收方:
~( 反码累加和(完整头部,含Checksum) ) == 0x0000 → 正确
!= 0x0000 → 丢弃,不通知上层
互联网校验和的优点是计算简单、速度快;缺点是检错能力较弱,不如 CRC,无法纠错,也无法检测出所有错误模式(例如两个字节同时出错但互相抵消的情况)。
从零理解校验和(Checksum)
校验和是数据完整性验证的基石,广泛用于网络传输、文件校验、密码学等领域。
一、什么是校验和?
校验和(Checksum) 是对一段数据进行某种数学运算后得到的一个固定长度的值,用于验证数据在传输或存储过程中是否发生了错误或篡改。
核心思想
原始数据 ──[算法]──► 校验和(一串数字或字母)
修改数据 ──[算法]──► 不同的校验和
就像一本书的"指纹"——同一本书算出来的指纹永远相同,内容变了指纹就变了。
二、为什么需要校验和?
| 场景 | 问题 | 校验和的作用 |
|---|---|---|
| 网络传输 | 数据包在传输中可能出错 | 接收方验证数据完整性 |
| 文件下载 | 文件可能被篡改或损坏 | 对比发布方提供的校验和 |
| 数据库存储 | 磁盘故障导致数据损坏 | 检测静默数据错误 |
| 密码学 | 消息可能被中间人篡改 | 确保消息未被修改 |
三、从最简单的校验和开始
3.1 累加校验和(最原始的形式)
把所有字节的值加在一起,取低8位(即对256取模)。
示例: 数据为 [72, 101, 108, 108, 111](即 “Hello”)
H = 72
e = 101
l = 108
l = 108
o = 111
─────────
和 = 500
对256取模: 500 % 256 = 244
校验和 = 244 (0xF4)
C++ 实现:
#include <iostream>
#include <string>
#include <numeric>
uint8_t simple_checksum(const std::string& data) {
uint32_t sum = 0;
for (unsigned char c : data) {
sum += c;
}
return static_cast<uint8_t>(sum % 256);
}
int main() {
std::string data = "Hello";
std::cout << static_cast<int>(simple_checksum(data)) << std::endl; // 输出: 244
return 0;
}
https://godbolt.org/z/P4bTbeY38
缺点: 只能发现奇数个比特翻转,无法发现字节顺序错误(AB 和 BA 校验和相同)。
3.2 奇偶校验(最基础的错误检测)
在数据后追加一个比特,使得所有比特中 1 的个数为偶数(偶校验)或奇数(奇校验)。
数据: 1011001 (四个1,偶数个1)
偶校验位: 0 (已经是偶数,补0)
发送: 10110010
数据: 1011101 (五个1,奇数个1)
偶校验位: 1 (补1使变成偶数)
发送: 10111011
局限: 只能检测奇数个比特错误,无法纠错,两个比特同时翻转则无法察觉。
四、CRC 循环冗余校验
CRC(Cyclic Redundancy Check)是目前最常用的校验算法之一,以太网、ZIP、PNG 等都在使用。
4.1 核心原理
把数据当作一个巨大的二进制数,用一个"生成多项式"去除它,余数就是 CRC 值。
数据(被除数) ÷ 生成多项式(除数) = 商 余 CRC值
这里的"除法"是模2除法(XOR 运算,没有进位/借位)。
4.2 模2除法示例
计算 1101 对 11 的 CRC(CRC-1,实际中不用,仅演示):
被除数: 1 1 0 1
1 1 ← 对齐最高位,XOR
─────
0 0
0 0 ← 下移
─────
0 1
余数(CRC)= 01
4.3 实际的 CRC-32
CRC-32 使用的生成多项式为:
x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1
十六进制表示:0x04C11DB7(或反转版 0xEDB88320)
C++ 实现(手写,不依赖库):
#include <iostream>
#include <string>
#include <iomanip>
uint32_t crc32_manual(const std::string& data) {
uint32_t crc = 0xFFFFFFFF; // 初始值全1
for (unsigned char byte : data) {
crc ^= byte; // 当前字节与CRC低8位异或
for (int i = 0; i < 8; ++i) { // 处理每一位
if (crc & 1) {
crc = (crc >> 1) ^ 0xEDB88320; // 生成多项式(反转)
} else {
crc >>= 1;
}
}
}
return crc ^ 0xFFFFFFFF; // 最终异或(取反)
}
int main() {
std::string data = "Hello";
std::cout << std::hex << std::uppercase
<< "CRC-32: 0x" << crc32_manual(data) << std::endl;
// 输出: CRC-32: 0xF7D18982
return 0;
}
https://godbolt.org/z/eTnfchfPc
使用查找表加速(实际系统的做法):
#include <iostream>
#include <string>
#include <array>
#include <iomanip>
// 预计算256个字节对应的CRC值(查表法,比逐位计算快8倍)
std::array<uint32_t, 256> make_crc_table() {
std::array<uint32_t, 256> table;
for (uint32_t i = 0; i < 256; ++i) {
uint32_t crc = i;
for (int j = 0; j < 8; ++j) {
if (crc & 1) {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
table[i] = crc;
}
return table;
}
const auto CRC_TABLE = make_crc_table();
uint32_t crc32_fast(const std::string& data) {
uint32_t crc = 0xFFFFFFFF;
for (unsigned char byte : data) {
crc = (crc >> 8) ^ CRC_TABLE[(crc ^ byte) & 0xFF];
}
return crc ^ 0xFFFFFFFF;
}
int main() {
std::string data = "Hello";
std::cout << std::hex << std::uppercase
<< "CRC-32 (fast): 0x" << crc32_fast(data) << std::endl;
return 0;
}
五、哈希算法(加密级校验和)
CRC 能检测随机错误,但无法防止有意篡改。哈希算法(Hash)提供了更强的保证。
5.1 哈希的核心特性
| 特性 | 说明 |
|---|---|
| 确定性 | 相同输入永远产生相同输出 |
| 雪崩效应 | 输入改变一个比特,输出大约一半的比特发生变化 |
| 单向性 | 无法从哈希值推算出原始数据 |
| 抗碰撞性 | 极难找到两个不同数据有相同哈希值 |
5.2 MD5(已不安全,了解即可)
输出128位(32个十六进制字符)。
C++ 标准库不内置 MD5/SHA,通常使用 OpenSSL 库。以下示例均基于 OpenSSL。
编译命令:g++ -o demo demo.cpp -lssl -lcrypto
#include <iostream>
#include <string>
#include <iomanip>
#include <openssl/md5.h>
std::string md5_hash(const std::string& data) {
unsigned char digest[MD5_DIGEST_LENGTH];
MD5(reinterpret_cast<const unsigned char*>(data.c_str()),
data.size(), digest);
std::ostringstream oss;
for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(digest[i]);
}
return oss.str();
}
int main() {
std::string data = "Hello, World!";
std::cout << "MD5: " << md5_hash(data) << std::endl;
// 输出: 65a8e27d8879283831b664bd8b7f0ad4
return 0;
}
⚠️ 注意: MD5 已被证明存在碰撞漏洞,不应用于安全场景。
5.3 SHA-256(当前主流)
输出256位(64个十六进制字符)。
#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
#include <openssl/sha.h>
std::string sha256_hash(const std::string& data) {
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(data.c_str()),
data.size(), digest);
std::ostringstream oss;
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(digest[i]);
}
return oss.str();
}
int main() {
std::string data = "Hello, World!";
std::cout << "SHA-256: " << sha256_hash(data) << std::endl;
// 输出: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986d
return 0;
}
SHA-256 内部流程(简化):
1. 消息填充 → 长度填充到512的倍数
2. 初始化哈希值 → 8个32位常量(由质数平方根推导)
3. 压缩函数 → 对每个512位数据块进行64轮运算
4. 输出 → 8个32位值拼接 = 256位哈希
5.4 雪崩效应演示
#include <iostream>
#include <string>
#include <openssl/sha.h>
#include <sstream>
#include <iomanip>
std::string sha256_hash(const std::string& data) {
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(data.c_str()),
data.size(), digest);
std::ostringstream oss;
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i)
oss << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(digest[i]);
return oss.str();
}
int main() {
std::string msg1 = "Hello";
std::string msg2 = "hello"; // 只改了大小写!
std::cout << "msg1: " << sha256_hash(msg1) << std::endl;
// 185f8db32921bd46d35fce1130a7df32c50...
std::cout << "msg2: " << sha256_hash(msg2) << std::endl;
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b1...
// 完全不同!
return 0;
}
六、互联网中的校验和:IP/TCP/UDP
网络协议中的校验和相对简单(需要硬件快速计算)。
6.1 IP 校验和算法
1. 把 IP 头部按16位(2字节)分组
2. 将所有16位数相加(进位加回低位,即反卷加法)
3. 对结果取反码(每一位取反)
4. 结果放入头部的 Checksum 字段
C++ 实现:
#include <iostream>
#include <vector>
#include <cstdint>
#include <iomanip>
uint16_t ip_checksum(const std::vector<uint8_t>& data) {
std::vector<uint8_t> buf = data;
if (buf.size() % 2 == 1) {
buf.push_back(0x00); // 奇数字节则补零
}
uint32_t total = 0;
for (size_t i = 0; i < buf.size(); i += 2) {
uint16_t word = (static_cast<uint16_t>(buf[i]) << 8) + buf[i + 1];
total += word;
}
// 处理进位(高16位加到低16位)
while (total >> 16) {
total = (total & 0xFFFF) + (total >> 16);
}
return static_cast<uint16_t>(~total & 0xFFFF); // 取反码
}
int main() {
// 构造一个简单的 IP 头部(校验和字段置0)
std::vector<uint8_t> ip_header = {
0x45, 0x00, // 版本+IHL, 服务类型
0x00, 0x3C, // 总长度 = 60
0x1C, 0x46, // 标识
0x40, 0x00, // 标志+分片偏移
0x40, 0x06, // TTL=64, 协议=TCP
0x00, 0x00, // 校验和(待计算)
0xC0, 0xA8, 0x00, 0x01, // 源IP: 192.168.0.1
0xC0, 0xA8, 0x00, 0xC8 // 目标IP: 192.168.0.200
};
uint16_t checksum = ip_checksum(ip_header);
std::cout << "IP校验和: 0x" << std::hex << std::uppercase
<< std::setw(4) << std::setfill('0') << checksum << std::endl;
return 0;
}
七、文件完整性校验实战
7.1 生成文件的 SHA-256
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <iomanip>
#include <openssl/evp.h>
// 计算文件的 SHA-256,支持大文件(分块读取)
std::string file_sha256(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file) {
throw std::runtime_error("Cannot open file: " + filepath);
}
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr);
const size_t CHUNK_SIZE = 65536; // 每次读 64KB
std::vector<char> buffer(CHUNK_SIZE);
while (file.read(buffer.data(), CHUNK_SIZE) || file.gcount() > 0) {
EVP_DigestUpdate(ctx, buffer.data(), file.gcount());
}
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digest_len = 0;
EVP_DigestFinal_ex(ctx, digest, &digest_len);
EVP_MD_CTX_free(ctx);
std::ostringstream oss;
for (unsigned int i = 0; i < digest_len; ++i) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(digest[i]);
}
return oss.str();
}
int main() {
std::cout << file_sha256("example.iso") << std::endl;
// 输出类似: 3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e...
return 0;
}
7.2 验证下载文件
# Linux / macOS
sha256sum ubuntu-22.04.iso
# 对比官网提供的校验和
# Windows PowerShell
Get-FileHash ubuntu-22.04.iso -Algorithm SHA256
7.3 命令行工具速查
| 命令 | 系统 | 算法 |
|---|---|---|
md5sum file | Linux | MD5 |
sha256sum file | Linux | SHA-256 |
sha512sum file | Linux | SHA-512 |
md5 file | macOS | MD5 |
shasum -a 256 file | macOS | SHA-256 |
certutil -hashfile file SHA256 | Windows | SHA-256 |
Get-FileHash file -Algorithm SHA256 | PowerShell | SHA-256 |
八、各类校验和对比
| 算法 | 输出长度 | 速度 | 抗碰撞 | 用途 |
|---|---|---|---|---|
| 累加和 | 8~16位 | 极快 | 极弱 | 嵌入式简单校验 |
| CRC-16 | 16位 | 很快 | 弱 | 串口通信(Modbus等) |
| CRC-32 | 32位 | 快 | 弱 | 以太网、ZIP、PNG |
| Adler-32 | 32位 | 快 | 弱 | zlib(HTTP压缩) |
| MD5 | 128位 | 较快 | 已破解 | 非安全场景(遗留系统) |
| SHA-1 | 160位 | 中等 | 弱 | 已淘汰(Git内部仍用) |
| SHA-256 | 256位 | 中等 | 强 | 文件校验、TLS、比特币 |
| SHA-512 | 512位 | 中等 | 更强 | 高安全要求 |
| SHA3-256 | 256位 | 较慢 | 强 | 新系统(抗量子准备) |
| BLAKE3 | 256位+ | 极快 | 强 | 现代高性能场景 |
九、常见误区
❌ 误区1:校验和相同 = 文件完全相同
对于 CRC-32 等短校验和,理论上存在不同文件碰撞的可能。但对于 SHA-256,碰撞的概率约为 1/2²⁵⁶,实际上不可能发生。
❌ 误区2:MD5 校验和可以防篡改
MD5 已被证明可以人工构造碰撞,攻击者可以创造出两个不同内容但 MD5 相同的文件。不要用 MD5 做安全验证。
❌ 误区3:校验和可以纠错
普通校验和(CRC、SHA 等)只能检测错误,不能纠正错误。纠错需要使用 Reed-Solomon、Hamming Code 等纠错码。
十、完整综合示例
#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
#include <array>
#include <openssl/md5.h>
#include <openssl/sha.h>
// ── 累加校验和 ──────────────────────────────────────────
uint8_t simple_checksum(const std::string& data) {
uint32_t sum = 0;
for (unsigned char c : data) sum += c;
return static_cast<uint8_t>(sum % 256);
}
// ── CRC-32 查表法 ────────────────────────────────────────
std::array<uint32_t, 256> make_crc_table() {
std::array<uint32_t, 256> table;
for (uint32_t i = 0; i < 256; ++i) {
uint32_t crc = i;
for (int j = 0; j < 8; ++j)
crc = (crc & 1) ? (crc >> 1) ^ 0xEDB88320 : crc >> 1;
table[i] = crc;
}
return table;
}
uint32_t crc32(const std::string& data) {
static const auto TABLE = make_crc_table();
uint32_t crc = 0xFFFFFFFF;
for (unsigned char byte : data)
crc = (crc >> 8) ^ TABLE[(crc ^ byte) & 0xFF];
return crc ^ 0xFFFFFFFF;
}
// ── Adler-32 ─────────────────────────────────────────────
uint32_t adler32(const std::string& data) {
const uint32_t MOD = 65521;
uint32_t a = 1, b = 0;
for (unsigned char c : data) {
a = (a + c) % MOD;
b = (b + a) % MOD;
}
return (b << 16) | a;
}
// ── OpenSSL 哈希辅助 ──────────────────────────────────────
template<size_t N>
std::string to_hex(const unsigned char (&digest)[N]) {
std::ostringstream oss;
for (size_t i = 0; i < N; ++i)
oss << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(digest[i]);
return oss.str();
}
// ── 综合分析 ──────────────────────────────────────────────
void analyze_data(const std::string& data) {
std::cout << "数据: \"" << data << "\"\n";
std::cout << "大小: " << data.size() << " 字节\n\n";
// 累加校验和
uint8_t simple = simple_checksum(data);
std::cout << "累加校验和 (8位): " << static_cast<int>(simple)
<< " (0x" << std::hex << std::uppercase
<< std::setw(2) << std::setfill('0')
<< static_cast<int>(simple) << ")\n" << std::dec;
// CRC-32
std::cout << "CRC-32: 0x" << std::hex << std::uppercase
<< std::setw(8) << std::setfill('0') << crc32(data) << "\n" << std::dec;
// Adler-32
std::cout << "Adler-32: 0x" << std::hex << std::uppercase
<< std::setw(8) << std::setfill('0') << adler32(data) << "\n" << std::dec;
// MD5
unsigned char md5_digest[MD5_DIGEST_LENGTH];
MD5(reinterpret_cast<const unsigned char*>(data.c_str()), data.size(), md5_digest);
std::cout << "MD5: " << to_hex(md5_digest) << "\n";
// SHA-1
unsigned char sha1_digest[SHA_DIGEST_LENGTH];
SHA1(reinterpret_cast<const unsigned char*>(data.c_str()), data.size(), sha1_digest);
std::cout << "SHA-1: " << to_hex(sha1_digest) << "\n";
// SHA-256
unsigned char sha256_digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(data.c_str()), data.size(), sha256_digest);
std::cout << "SHA-256: " << to_hex(sha256_digest) << "\n";
// SHA-512
unsigned char sha512_digest[SHA512_DIGEST_LENGTH];
SHA512(reinterpret_cast<const unsigned char*>(data.c_str()), data.size(), sha512_digest);
std::string sha512_hex = to_hex(sha512_digest);
std::cout << "SHA-512: " << sha512_hex.substr(0, 64) << "...\n";
}
int main() {
analyze_data("Hello, Checksum World!");
return 0;
}
编译命令:
g++ -std=c++17 -O2 -o checksum_demo checksum_demo.cpp -lssl -lcrypto
./checksum_demo
输出示例:
数据: "Hello, Checksum World!"
大小: 22 字节
累加校验和 (8位): 107 (0x6B)
CRC-32: 0x4B4B6E9A
Adler-32: 0x4CC60A18
MD5: e3d9e02908526c4c9f5e0b6e3f8c1b5a
SHA-1: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
SHA-256: 7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069
SHA-512: 861844d6704e8573fec34d967e20bcfe...
参考资料
- RFC 1071 – Computing the Internet Checksum
- FIPS PUB 180-4 – Secure Hash Standard (SHA)
- CRC Catalogue: https://crccalc.com
- OpenSSL 文档: https://docs.openssl.org
- C++
<bitset>/<numeric>文档: https://en.cppreference.com
互联网校验和的数学原理
一、先从直觉出发:校验和在干什么?
在网络中传输数据时,我们需要一种方法验证数据有没有损坏。
IP/TCP/UDP 的校验和算法非常简单:
把所有 16 位(2字节)的数据段加起来,
进位回卷到低位(反卷加法),
最后取反码,
结果就是校验和。
接收方做同样的运算,如果结果全是 1(即 FFFF),说明数据没有损坏。
这个算法背后藏着一个优美的数学结构——阿贝尔群(Abelian Group)。
二、什么是群(Group)?
群是抽象代数中最基本的结构,由一个集合加上一个运算组成。
要成为群,必须满足 4 个条件:
集合 S + 运算 * = 群
| 条件 | 含义 | 通俗解释 |
|---|---|---|
| 封闭性 | a ∗ b ∈ S a * b \in S a∗b∈S | 运算结果还在集合里,不会"跑出去" |
| 结合律 | ( a ∗ b ) ∗ c = a ∗ ( b ∗ c ) (a * b) * c = a * (b * c) (a∗b)∗c=a∗(b∗c) | 先算哪对括号结果一样 |
| 单位元 | 存在 e e e 使 e ∗ a = a ∗ e = a e * a = a * e = a e∗a=a∗e=a | 有个"不改变任何元素"的特殊值 |
| 逆元 | 对每个 a a a 存在 a ′ a' a′ 使 a ∗ a ′ = e a * a' = e a∗a′=e | 每个元素都有"配对的反元素" |
如果还满足交换律: a ∗ b = b ∗ a a * b = b * a a∗b=b∗a,就叫阿贝尔群(Abelian Group),也叫交换群。
三、反卷加法是什么?
普通加法中,
0xAB12
+
0x6000
\text{0xAB12} + \text{0x6000}
0xAB12+0x6000 会产生进位,结果超出 16 位。
反卷加法(One’s Complement Sum)的规则是:把超出的进位加回到低位。
0xAB12
+ 0x6000
---------
1 0B12 <-- 产生了进位(第17位为1)
+1 <-- 把进位加回低位
---------
0x0B13 <-- 最终结果
用数学表示:对于两个 16 位数
X
X
X 和
Y
Y
Y,
X
⊕
Y
=
{
X
+
Y
若
X
+
Y
≤
0xFFFF
(
X
+
Y
)
−
0xFFFF
若
X
+
Y
>
0xFFFF
X \oplus Y = \begin{cases} X + Y & \text{若 } X + Y \leq \text{0xFFFF} \\ (X + Y) - \text{0xFFFF} & \text{若 } X + Y > \text{0xFFFF} \end{cases}
X⊕Y={X+Y(X+Y)−0xFFFF若 X+Y≤0xFFFF若 X+Y>0xFFFF
其中
⊕
\oplus
⊕ 表示反卷加法(区别于普通加法)。
注意:减去 0xFFFF 等价于把进位的 1 加回到低 16 位,因为 0x10000 − 0xFFFF = 1 \text{0x10000} - \text{0xFFFF} = 1 0x10000−0xFFFF=1。
四、集合 V 的定义
文中定义:
V
=
{
0x0001
,
0x0002
,
…
,
0xFFFF
}
V = \{\text{0x0001},\ \text{0x0002},\ \ldots,\ \text{0xFFFF}\}
V={0x0001, 0x0002, …, 0xFFFF}
注意:0x0000 被故意排除在外。
这是一个包含 65535 个元素的集合(所有非零的 16 位十六进制数)。
五、逐条验证群的性质
5.1 封闭性
命题: 对任意
X
,
Y
∈
V
X, Y \in V
X,Y∈V,有
X
⊕
Y
∈
V
X \oplus Y \in V
X⊕Y∈V。
验证:
- 两个非零 16 位数的反卷和,最小是 0x0001 ⊕ 0x0001 = 0x0002 \text{0x0001} \oplus \text{0x0001} = \text{0x0002} 0x0001⊕0x0001=0x0002,最大是 0xFFFF ⊕ 0xFFFF \text{0xFFFF} \oplus \text{0xFFFF} 0xFFFF⊕0xFFFF。
- 0xFFFF ⊕ 0xFFFF \text{0xFFFF} \oplus \text{0xFFFF} 0xFFFF⊕0xFFFF:普通加法得 0x1FFFE \text{0x1FFFE} 0x1FFFE,进位回卷后 0x1FFFE − 0xFFFF = 0xFFFF \text{0x1FFFE} - \text{0xFFFF} = \text{0xFFFF} 0x1FFFE−0xFFFF=0xFFFF。
- 结果永远在 [ 0x0001 , 0xFFFF ] [\text{0x0001}, \text{0xFFFF}] [0x0001,0xFFFF] 范围内,不会出现 0x0000,所以封闭性成立。
ASCII 示意:
输入两个 V 中的值
|
v
反卷加法
|
v
结果还在 V 中?
[0x0001 ~ 0xFFFF]
|
v
YES <-- 封闭性成立
5.2 结合律
命题:
X
⊕
(
Y
⊕
Z
)
=
(
X
⊕
Y
)
⊕
Z
X \oplus (Y \oplus Z) = (X \oplus Y) \oplus Z
X⊕(Y⊕Z)=(X⊕Y)⊕Z
验证思路:
反卷加法的本质是:把所有数当成整数相加,超出的进位不断回卷,最终结果都落在
[
0x0001
,
0xFFFF
]
[\text{0x0001}, \text{0xFFFF}]
[0x0001,0xFFFF]。
由于整数加法本身满足结合律,进位回卷操作不改变最终结果的顺序无关性,因此反卷加法也满足结合律。
例:
0x1234
⊕
(
0xABCD
⊕
0x9999
)
\text{0x1234} \oplus (\text{0xABCD} \oplus \text{0x9999})
0x1234⊕(0xABCD⊕0x9999)
=
0x1234
⊕
0x4566
= \text{0x1234} \oplus \text{0x4566}
=0x1234⊕0x4566
=
0x579A
= \text{0x579A}
=0x579A
(
0x1234
⊕
0xABCD
)
⊕
0x9999
(\text{0x1234} \oplus \text{0xABCD}) \oplus \text{0x9999}
(0x1234⊕0xABCD)⊕0x9999
=
0xBE01
⊕
0x9999
= \text{0xBE01} \oplus \text{0x9999}
=0xBE01⊕0x9999
=
0x579A
= \text{0x579A}
=0x579A
两种顺序结果相同。
5.3 单位元(Identity Element)
命题: 存在
e
∈
V
e \in V
e∈V 使得对所有
X
∈
V
X \in V
X∈V 有
e
⊕
X
=
X
⊕
e
=
X
e \oplus X = X \oplus e = X
e⊕X=X⊕e=X。
答案:
e
=
0xFFFF
e = \text{0xFFFF}
e=0xFFFF
验证:
对任意
X
∈
V
X \in V
X∈V(即
X
≠
0
X \neq 0
X=0):
0xFFFF
⊕
X
=
X
+
0xFFFF
\text{0xFFFF} \oplus X = X + \text{0xFFFF}
0xFFFF⊕X=X+0xFFFF
由于
X
≥
1
X \geq 1
X≥1,所以
X
+
0xFFFF
≥
0x10000
X + \text{0xFFFF} \geq \text{0x10000}
X+0xFFFF≥0x10000,发生进位回卷:
(
X
+
0xFFFF
)
−
0xFFFF
=
X
(X + \text{0xFFFF}) - \text{0xFFFF} = X
(X+0xFFFF)−0xFFFF=X
结果就是
X
X
X 本身,单位元条件成立。
直觉理解:0xFFFF 在反卷加法中扮演"零"的角色,就像普通加法里的 0。
5.4 逆元(Inverse Element)
命题: 对每个
X
∈
V
X \in V
X∈V,存在
X
′
∈
V
X' \in V
X′∈V 使得
X
⊕
X
′
=
0xFFFF
X \oplus X' = \text{0xFFFF}
X⊕X′=0xFFFF。
答案:
X
′
=
0xFFFF
−
X
X' = \text{0xFFFF} - X
X′=0xFFFF−X(即
X
X
X 的按位取反,即反码)
验证:
X
⊕
(
0xFFFF
−
X
)
X \oplus (\text{0xFFFF} - X)
X⊕(0xFFFF−X)
由于
X
≥
1
X \geq 1
X≥1,所以
0xFFFF
−
X
≤
0xFFFE
\text{0xFFFF} - X \leq \text{0xFFFE}
0xFFFF−X≤0xFFFE,即
X
′
≥
1
X' \geq 1
X′≥1,
X
′
∈
V
X' \in V
X′∈V。
普通加法:
X
+
(
0xFFFF
−
X
)
=
0xFFFF
X + (\text{0xFFFF} - X) = \text{0xFFFF}
X+(0xFFFF−X)=0xFFFF,没有进位,结果恰好是
0xFFFF
\text{0xFFFF}
0xFFFF(单位元)。
例:
X
=
0x1234
X = \text{0x1234}
X=0x1234,则
X
′
=
0xFFFF
−
0x1234
=
0xEDCB
X' = \text{0xFFFF} - \text{0x1234} = \text{0xEDCB}
X′=0xFFFF−0x1234=0xEDCB,
0x1234
⊕
0xEDCB
=
0xFFFF
\text{0x1234} \oplus \text{0xEDCB} = \text{0xFFFF}
0x1234⊕0xEDCB=0xFFFF
这正是为什么校验和算法用"取反码":把发送方的校验和与数据一起发送,接收方把所有数(含校验和)反卷相加,结果恰好是 0xFFFF(全1),表示正确。
5.5 交换律(Commutativity)
命题:
X
⊕
Y
=
Y
⊕
X
X \oplus Y = Y \oplus X
X⊕Y=Y⊕X
验证: 普通整数加法满足交换律,反卷操作不依赖顺序,因此反卷加法也满足交换律。
六、为什么不能包含 0x0000?
这是整个数学结构中最微妙的地方,我们来详细拆解。
6.1 两个"零"的问题
如果把 0x0000 \text{0x0000} 0x0000 加入集合 V V V,变成 V ′ = { 0x0000 , 0x0001 , … , 0xFFFF } V' = \{\text{0x0000}, \text{0x0001}, \ldots, \text{0xFFFF}\} V′={0x0000,0x0001,…,0xFFFF},那么:
- 0xFFFF ⊕ X = X \text{0xFFFF} \oplus X = X 0xFFFF⊕X=X(原来的单位元,如上验证)
-
0x0000
⊕
X
=
?
\text{0x0000} \oplus X = ?
0x0000⊕X=?
对于 0x0000 ⊕ X \text{0x0000} \oplus X 0x0000⊕X:
0x0000 + X = X \text{0x0000} + X = X 0x0000+X=X(无进位,结果为 X X X)
所以 0x0000 ⊕ X = X \text{0x0000} \oplus X = X 0x0000⊕X=X!
0x0000 和 0xFFFF 都表现得像单位元!
0xFFFF + 0xAB12 --> 0x1AB11 -->(回卷)--> 0xAB12 [正确]
0x0000 + 0xAB12 --> 0x0AB12 -->(无进位)-> 0xAB12 [也正确!]
6.2 群中单位元必须唯一
在群的定义中,单位元必须是唯一的。证明如下:
假设
e
1
e_1
e1 和
e
2
e_2
e2 都是单位元,则:
e
1
=
e
1
⊕
e
2
=
e
2
e_1 = e_1 \oplus e_2 = e_2
e1=e1⊕e2=e2
所以两个单位元必须相等,即单位元唯一。
但在
V
′
V'
V′ 中,
0x0000
\text{0x0000}
0x0000 和
0xFFFF
\text{0xFFFF}
0xFFFF 都满足单位元的定义,这与群的要求矛盾。
6.3 0x0000 导致逆元不存在
更致命的问题:如果我们选
0x0000
\text{0x0000}
0x0000 作为单位元(即"零"),那么对于任意
X
∈
V
′
X \in V'
X∈V′,
我们需要找到
X
′
X'
X′ 使得:
X
⊕
X
′
=
0x0000
X \oplus X' = \text{0x0000}
X⊕X′=0x0000
但是,反卷加法的结果永远
≥
0x0001
\geq \text{0x0001}
≥0x0001(当两个操作数不都是0时),
不可能等于
0x0000
\text{0x0000}
0x0000。
例如
X
=
0x12AB
X = \text{0x12AB}
X=0x12AB,找不到任何
X
′
X'
X′ 使
0x12AB
⊕
X
′
=
0x0000
\text{0x12AB} \oplus X' = \text{0x0000}
0x12AB⊕X′=0x0000。
所以 0x0000 作为单位元时,逆元不存在,群结构崩溃。
6.4 选 0xFFFF 作为单位元,排除 0x0000
若选 0xFFFF \text{0xFFFF} 0xFFFF 作为单位元,逆元是 X ′ = 0xFFFF − X X' = \text{0xFFFF} - X X′=0xFFFF−X:
- 当 X = 0x0001 X = \text{0x0001} X=0x0001 时, X ′ = 0xFFFE X' = \text{0xFFFE} X′=0xFFFE,没问题
- 当
X
=
0xFFFF
X = \text{0xFFFF}
X=0xFFFF 时,
X
′
=
0x0000
X' = \text{0x0000}
X′=0x0000,但 0x0000 不在 V 中!
这意味着 0xFFFF \text{0xFFFF} 0xFFFF 的逆元是 0x0000 \text{0x0000} 0x0000,但 0x0000 \text{0x0000} 0x0000 被排除了,
所以如果不排除 0x0000 \text{0x0000} 0x0000, 0xFFFF \text{0xFFFF} 0xFFFF 就没有逆元。
换句话说:排除 0x0000,正是为了让群结构完整自洽。
七、结构总结
集合 V = {0x0001, 0x0002, ..., 0xFFFF}
运算 (+) = 反卷加法(一's complement sum)
群的五大性质
|
----------------+----------------
| | | | |
封闭性 结合律 单位元 逆元 交换律
| | | | |
结果仍 顺序无 e = X' = 加法
在 V 中 关紧要 0xFFFF FFFF-X 可换序
=> (V, +) 构成阿贝尔群(交换群)
八、为什么这对网络协议有用?
群结构的实际意义:
- 交换律 -> 数据包的 16 位字段顺序可以任意,校验和不变
- 结合律 -> 可以分块计算校验和,最后合并,结果一致(适合硬件流水线)
- 逆元(取反码) -> 接收方把数据和校验和一起加,结果是 0xFFFF(全1),用于判断正确性
发送方:
data_sum = D1 + D2 + ... + Dn (反卷加法)
checksum = ~data_sum (取反码,即逆元)
发送:[D1, D2, ..., Dn, checksum]
接收方:
total = D1 + D2 + ... + Dn + checksum
= data_sum + (~data_sum)
= 0xFFFF (单位元,表示"无误")
九、C++ 完整演示代码
// internet_checksum.cpp
// 演示互联网校验和(反卷加法)的数学性质
// 编译:g++ -std=c++17 -O2 -o internet_checksum internet_checksum.cpp
#include <iostream>
#include <cstdint>
#include <iomanip>
#include <vector>
#include <string>
#include <cassert>
// ============================================================
// 核心:16 位反卷加法(One's Complement Sum)
// 规则:普通加法后,若有进位,把进位加回低 16 位
// ============================================================
uint16_t ones_complement_add(uint16_t x, uint16_t y) {
// 用 32 位存储,避免溢出
uint32_t sum = static_cast<uint32_t>(x) + static_cast<uint32_t>(y);
// 如果有进位(第 17 位为 1),把进位加回低 16 位
// 等价于 sum - 0xFFFF(当 sum > 0xFFFF 时)
if (sum > 0xFFFF) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
// 注意:返回值可能是 0x0000(当 x = y = 0x0000 时)
// 但在群 V 中,我们不处理 0x0000 的情况
return static_cast<uint16_t>(sum);
}
// ============================================================
// 辅助:格式化输出 16 进制数
// ============================================================
std::string hex16(uint16_t v) {
std::ostringstream oss;
oss << "0x" << std::hex << std::uppercase
<< std::setw(4) << std::setfill('0') << v;
return oss.str();
}
// ============================================================
// 验证 1:封闭性
// 对 V 中任意 X, Y,X (+) Y 仍在 V 中(结果 != 0x0000)
// ============================================================
void demo_closure() {
std::cout << "=== 1. 封闭性验证 ===\n";
// 测试一些边界值
std::vector<std::pair<uint16_t, uint16_t>> cases = {
{0x0001, 0x0001}, // 最小值 + 最小值
{0xFFFF, 0xFFFF}, // 最大值 + 最大值(测试进位回卷)
{0x0001, 0xFFFE}, // 和恰好是 0xFFFF
{0x1234, 0xABCD}, // 普通值
};
for (auto [x, y] : cases) {
uint16_t result = ones_complement_add(x, y);
bool in_V = (result != 0x0000); // V 中不包含 0x0000
std::cout << " " << hex16(x) << " (+) " << hex16(y)
<< " = " << hex16(result)
<< (in_V ? " [在 V 中]" : " [不在 V 中!]") << "\n";
// 对于 V 中的元素(非零输入),结果应该在 V 中
if (x != 0 && y != 0) {
assert(in_V && "封闭性违反!");
}
}
std::cout << "\n";
}
// ============================================================
// 验证 2:结合律
// X (+) (Y (+) Z) == (X (+) Y) (+) Z
// ============================================================
void demo_associativity() {
std::cout << "=== 2. 结合律验证 ===\n";
std::vector<std::tuple<uint16_t, uint16_t, uint16_t>> cases = {
{0x1234, 0xABCD, 0x9999},
{0x0001, 0x0001, 0xFFFE},
{0x1111, 0x2222, 0x3333},
};
for (auto [x, y, z] : cases) {
// 先算 Y (+) Z,再算 X (+) 结果
uint16_t left = ones_complement_add(x, ones_complement_add(y, z));
// 先算 X (+) Y,再算结果 (+) Z
uint16_t right = ones_complement_add(ones_complement_add(x, y), z);
std::cout << " X=" << hex16(x) << " Y=" << hex16(y)
<< " Z=" << hex16(z) << "\n";
std::cout << " X+(Y+Z) = " << hex16(left)
<< ",(X+Y)+Z = " << hex16(right)
<< (left == right ? " [相等,结合律成立]" : " [不等!]") << "\n";
assert(left == right && "结合律违反!");
}
std::cout << "\n";
}
// ============================================================
// 验证 3:单位元
// 对任意 X in V,0xFFFF (+) X == X
// ============================================================
void demo_identity() {
std::cout << "=== 3. 单位元验证(e = 0xFFFF)===\n";
// 单位元
const uint16_t IDENTITY = 0xFFFF;
std::vector<uint16_t> cases = {0x0001, 0x1234, 0xABCD, 0xFFFE, 0xFFFF};
for (uint16_t x : cases) {
uint16_t left = ones_complement_add(IDENTITY, x); // e (+) X
uint16_t right = ones_complement_add(x, IDENTITY); // X (+) e
std::cout << " 0xFFFF (+) " << hex16(x) << " = " << hex16(left);
std::cout << "," << hex16(x) << " (+) 0xFFFF = " << hex16(right);
std::cout << (left == x && right == x ? " [等于 X,单位元成立]" : " [不等!]") << "\n";
assert(left == x && right == x && "单位元性质违反!");
}
std::cout << "\n";
}
// ============================================================
// 验证 4:逆元
// 对任意 X in V,X' = 0xFFFF - X,使 X (+) X' = 0xFFFF
// 注意:X' = ~X(按位取反)在 16 位范围内等价于 0xFFFF - X
// ============================================================
void demo_inverse() {
std::cout << "=== 4. 逆元验证(X' = 0xFFFF - X = ~X)===\n";
std::vector<uint16_t> cases = {0x0001, 0x1234, 0xABCD, 0xFFFE};
for (uint16_t x : cases) {
// 逆元:按位取反(16 位),等价于 0xFFFF - X
uint16_t inv = static_cast<uint16_t>(~x); // 0xFFFF - x
uint16_t result = ones_complement_add(x, inv);
std::cout << " X=" << hex16(x)
<< ",X'=" << hex16(inv)
<< ",X (+) X' = " << hex16(result)
<< (result == 0xFFFF ? " [= 0xFFFF,逆元成立]" : " [不等!]") << "\n";
assert(result == 0xFFFF && "逆元性质违反!");
}
// 特殊情况:X = 0xFFFF,其逆元是 0xFFFF - 0xFFFF = 0x0000
// 但 0x0000 不在 V 中!这正是排除 0x0000 的原因之一。
std::cout << " X=0xFFFF 的逆元 = 0xFFFF - 0xFFFF = 0x0000\n";
std::cout << " -> 0x0000 不在 V 中,因此 0xFFFF 需要特殊处理\n";
std::cout << " -> 实际上 0xFFFF (+) 0xFFFF = 0xFFFF(它是自身的逆元)\n";
uint16_t self_inv = ones_complement_add(0xFFFF, 0xFFFF);
std::cout << " 验证:0xFFFF (+) 0xFFFF = " << hex16(self_inv) << "\n\n";
}
// ============================================================
// 验证 5:交换律
// X (+) Y == Y (+) X
// ============================================================
void demo_commutativity() {
std::cout << "=== 5. 交换律验证 ===\n";
std::vector<std::pair<uint16_t, uint16_t>> cases = {
{0x1234, 0xABCD},
{0x0001, 0xFFFE},
{0xFFFF, 0x1234},
};
for (auto [x, y] : cases) {
uint16_t xy = ones_complement_add(x, y);
uint16_t yx = ones_complement_add(y, x);
std::cout << " " << hex16(x) << " (+) " << hex16(y) << " = " << hex16(xy);
std::cout << "," << hex16(y) << " (+) " << hex16(x) << " = " << hex16(yx);
std::cout << (xy == yx ? " [相等,交换律成立]" : " [不等!]") << "\n";
assert(xy == yx && "交换律违反!");
}
std::cout << "\n";
}
// ============================================================
// 实际应用:IP 校验和的计算与验证
// ============================================================
// 计算一段数据的互联网校验和(标准算法)
uint16_t internet_checksum(const std::vector<uint8_t>& data) {
// 补零到偶数长度
std::vector<uint8_t> buf = data;
if (buf.size() % 2 == 1) {
buf.push_back(0x00);
}
uint32_t sum = 0;
for (size_t i = 0; i < buf.size(); i += 2) {
// 每两字节合成一个 16 位字(大端序)
uint16_t word = (static_cast<uint16_t>(buf[i]) << 8) | buf[i + 1];
sum += word;
}
// 处理所有进位(可能需要多次)
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
// 取反码,得到校验和
return static_cast<uint16_t>(~sum);
}
void demo_ip_checksum() {
std::cout << "=== 6. 实际应用:IP 校验和 ===\n";
// 一个简化的 IP 头部(校验和字段置 0)
std::vector<uint8_t> ip_header = {
0x45, 0x00, // 版本(4)+IHL(5), 服务类型
0x00, 0x3C, // 总长度 = 60
0x1C, 0x46, // 标识
0x40, 0x00, // 标志 + 分片偏移
0x40, 0x06, // TTL=64, 协议=TCP(6)
0x00, 0x00, // 校验和字段(先置为 0)
0xC0, 0xA8, 0x00, 0x01, // 源 IP:192.168.0.1
0xC0, 0xA8, 0x00, 0xC8 // 目的 IP:192.168.0.200
};
uint16_t cksum = internet_checksum(ip_header);
std::cout << " 计算得到的校验和:" << hex16(cksum) << "\n";
// 把校验和写入头部(大端序)
ip_header[10] = static_cast<uint8_t>(cksum >> 8);
ip_header[11] = static_cast<uint8_t>(cksum & 0xFF);
// 接收方验证:把含校验和的完整头部再做一次校验和
uint16_t verify = internet_checksum(ip_header);
std::cout << " 接收方验证结果:" << hex16(verify)
<< (verify == 0xFFFF ? " [= 0xFFFF,数据完整]" : " [不是 0xFFFF,数据损坏!]")
<< "\n\n";
// 演示数据损坏的情况
std::cout << " 模拟数据损坏(修改 TTL 字段):\n";
ip_header[8] ^= 0xFF; // 破坏 TTL 字段
uint16_t corrupted = internet_checksum(ip_header);
std::cout << " 损坏后验证结果:" << hex16(corrupted)
<< (corrupted == 0xFFFF ? " [= 0xFFFF,未检测到错误]" : " [不是 0xFFFF,检测到错误]")
<< "\n\n";
}
// ============================================================
// 主函数
// ============================================================
int main() {
std::cout << "==================================================\n";
std::cout << " 互联网校验和的阿贝尔群数学性质演示\n";
std::cout << " 集合 V = {0x0001, ..., 0xFFFF}\n";
std::cout << " 运算 (+) = 反卷加法(One's Complement Sum)\n";
std::cout << "==================================================\n\n";
demo_closure(); // 封闭性
demo_associativity(); // 结合律
demo_identity(); // 单位元
demo_inverse(); // 逆元
demo_commutativity(); // 交换律
demo_ip_checksum(); // 实际应用
std::cout << "所有群性质验证通过,(V, +) 构成阿贝尔群。\n";
return 0;
}
https://godbolt.org/z/faaM3eMTc
十、运行结果示例
==================================================
互联网校验和的阿贝尔群数学性质演示
集合 V = {0x0001, ..., 0xFFFF}
运算 (+) = 反卷加法(One's Complement Sum)
==================================================
=== 1. 封闭性验证 ===
0x0001 (+) 0x0001 = 0x0002 [在 V 中]
0xFFFF (+) 0xFFFF = 0xFFFF [在 V 中]
0x0001 (+) 0xFFFE = 0xFFFF [在 V 中]
0x1234 (+) 0xABCD = 0xBE01 [在 V 中]
=== 2. 结合律验证 ===
X=0x1234 Y=0xABCD Z=0x9999
X+(Y+Z) = 0x579A,(X+Y)+Z = 0x579A [相等,结合律成立]
...
=== 3. 单位元验证(e = 0xFFFF)===
0xFFFF (+) 0x0001 = 0x0001,0x0001 (+) 0xFFFF = 0x0001 [等于 X,单位元成立]
...
=== 4. 逆元验证(X' = 0xFFFF - X = ~X)===
X=0x0001,X'=0xFFFE,X (+) X' = 0xFFFF [= 0xFFFF,逆元成立]
...
=== 5. 交换律验证 ===
0x1234 (+) 0xABCD = 0xBE01,0xABCD (+) 0x1234 = 0xBE01 [相等,交换律成立]
...
=== 6. 实际应用:IP 校验和 ===
计算得到的校验和:0xB861
接收方验证结果:0xFFFF [= 0xFFFF,数据完整]
模拟数据损坏(修改 TTL 字段):
损坏后验证结果:0xB7C1 [不是 0xFFFF,检测到错误]
所有群性质验证通过,(V, +) 构成阿贝尔群。
十一、群结构与 0x0000 关系的可视化
十二、一句话总结
互联网校验和使用反卷加法,在集合 V = { 0x0001 , … , 0xFFFF } V = \{\text{0x0001}, \ldots, \text{0xFFFF}\} V={0x0001,…,0xFFFF} 上构成阿贝尔群,单位元是 0xFFFF \text{0xFFFF} 0xFFFF,每个元素的逆元是它的按位取反(即反码)。0x0000 被排除,是因为它的加入会破坏单位元的唯一性,进而导致逆元无法定义,群结构崩溃。
参考资料
- RFC 1071 – Computing the Internet Checksum
- Pinter, C. C. A Book of Abstract Algebra. Dover Publications, 1990.
- Kurose & Ross, Computer Networking: A Top-Down Approach, 8th Edition, §5.2

2856

被折叠的 条评论
为什么被折叠?



