TCP/IP卷1学习: 5.2 IPv4 与 IPv6 头部字段详解

头部字段总览(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{ 字节} 最大值=2161=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} 2321=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 数据报的载荷是什么协议的数据(解复用)。

常见值协议
1ICMP
4IPv4-in-IPv4(隧道封装)
6TCP
17UDP

IPv6 对应字段:叫做下一头部(Next Header),作用相同,但还可以指向 IPv6 扩展头的类型,更加通用灵活。

8. 首部校验和字段(Header Checksum)—— 16 位(仅 IPv4)

作用:对 IPv4 头部做完整性校验,确保头部没有在传输中损坏。
重要:校验和只覆盖头部,不覆盖数据部分。数据的完整性需要 TCP/UDP 等上层协议自己负责。
每过一个路由器都要重新计算,因为 TTL 每次都变了。
IPv6 没有校验和字段,理由是:

  1. 光纤传输误码率极低
  2. 上层协议(TCP/UDP)已有自己的校验和
  3. 省掉这个字段可以让路由器转发更快
    校验和计算方法(互联网校验和算法):
    checksum = ∼ ( ∑ i 16位字 i ) \text{checksum} = \sim\left(\sum_{i} \text{16位字}_i\right) checksum=∼(i16位字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,29643 亿
IPv6:各 128 位,地址空间极其巨大:
IPv6 最大地址数 = 2 128 ≈ 3.4 × 10 38 \text{IPv6 最大地址数} = 2^{128} \approx 3.4 \times 10^{38} IPv6 最大地址数=21283.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(可变)         扩展头(链式)

数据报大小限制关系图

IPv4 数据报

最大理论值
65535 字节

以太网MTU限制
1500 字节

主机最小接收保证
576 字节

UDP应用惯用上限
512字节数据

IPv6 数据报

基本最大载荷
65535 字节

最小链路MTU
1280 字节

Jumbogram选项
最大约4GB载荷

IPv6 下一头部链式结构

IPv6基本头
Next Header=43

路由扩展头
Next Header=44

分片扩展头
Next Header=6

TCP头部+数据

完整可运行 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
头部字段

版本字段

IPv4填4

IPv6填6

不可互通

双栈同时支持

长度相关

IHL仅IPv4

正常值5即20字节

最大60字节

IPv6固定40字节

服务质量

原ToS字段

现DS字段6位

ECN拥塞通知2位

数据大小

总长度16位

IPv4最大65535字节

IPv6载荷最大65535字节

IPv6 Jumbogram达4GB

分片相关

标识符唯一编号

Flags控制分片

片偏移记录位置

IPv6移入扩展头

TTL跳数

初始推荐64

每跳减1

归零即丢弃

IPv6改名Hop Limit

协议字段

6代表TCP

17代表UDP

1代表ICMP

IPv6为Next Header

校验和

仅覆盖头部

IPv6无此字段

过路由器重算

地址字段

IPv4为32位

IPv6为128位

43亿vs3.4×10^38

小结


字段IPv4IPv6说明
Version4位,值=44位,值=6两者位置相同,值不同
IHL4位IPv6头部长度固定40字节
ToS/Traffic Class8位(DS+ECN)8位(DS+ECN)定义相同
长度字段Total LengthPayload LengthIPv6不算基本头
标识符16位在分片扩展头分片重组用
TTL8位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)

发送方准备 IP 头部

将 Checksum 字段设为 0x0000

对整个头部做 16
位反码累加求和

对求和结果取反码

将结果填入 Checksum 字段

发送数据包

接收方收到数据包

对整个头部(含 Checksum)做 16 位反码累加求和

求和结果取反后是否为 0x0000?

头部正确,继续处理

头部出错,丢弃数据包
不发送错误消息

七、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
缺点: 只能发现奇数个比特翻转,无法发现字节顺序错误(ABBA 校验和相同)。

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除法示例

计算 110111 的 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 fileLinuxMD5
sha256sum fileLinuxSHA-256
sha512sum fileLinuxSHA-512
md5 filemacOSMD5
shasum -a 256 filemacOSSHA-256
certutil -hashfile file SHA256WindowsSHA-256
Get-FileHash file -Algorithm SHA256PowerShellSHA-256

八、各类校验和对比


算法输出长度速度抗碰撞用途
累加和8~16位极快极弱嵌入式简单校验
CRC-1616位很快串口通信(Modbus等)
CRC-3232位以太网、ZIP、PNG
Adler-3232位zlib(HTTP压缩)
MD5128位较快已破解非安全场景(遗留系统)
SHA-1160位中等已淘汰(Git内部仍用)
SHA-256256位中等文件校验、TLS、比特币
SHA-512512位中等更强高安全要求
SHA3-256256位较慢新系统(抗量子准备)
BLAKE3256位+极快现代高性能场景

九、常见误区

❌ 误区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 abS运算结果还在集合里,不会"跑出去"
结合律 ( a ∗ b ) ∗ c = a ∗ ( b ∗ c ) (a * b) * c = a * (b * c) (ab)c=a(bc)先算哪对括号结果一样
单位元存在 e e e 使 e ∗ a = a ∗ e = a e * a = a * e = a ea=ae=a有个"不改变任何元素"的特殊值
逆元对每个 a a a 存在 a ′ a' a 使 a ∗ a ′ = e a * a' = e aa=e每个元素都有"配对的反元素"

如果还满足交换律 a ∗ b = b ∗ a a * b = b * a ab=ba,就叫阿贝尔群(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} XY={X+Y(X+Y)0xFFFF X+Y0xFFFF X+Y>0xFFFF
其中 ⊕ \oplus 表示反卷加法(区别于普通加法)。

注意:减去 0xFFFF 等价于把进位的 1 加回到低 16 位,因为 0x10000 − 0xFFFF = 1 \text{0x10000} - \text{0xFFFF} = 1 0x100000xFFFF=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,YV,有 X ⊕ Y ∈ V X \oplus Y \in V XYV
验证:

  • 两个非零 16 位数的反卷和,最小是 0x0001 ⊕ 0x0001 = 0x0002 \text{0x0001} \oplus \text{0x0001} = \text{0x0002} 0x00010x0001=0x0002,最大是 0xFFFF ⊕ 0xFFFF \text{0xFFFF} \oplus \text{0xFFFF} 0xFFFF0xFFFF
  • 0xFFFF ⊕ 0xFFFF \text{0xFFFF} \oplus \text{0xFFFF} 0xFFFF0xFFFF:普通加法得 0x1FFFE \text{0x1FFFE} 0x1FFFE,进位回卷后 0x1FFFE − 0xFFFF = 0xFFFF \text{0x1FFFE} - \text{0xFFFF} = \text{0xFFFF} 0x1FFFE0xFFFF=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(YZ)=(XY)Z
验证思路:
反卷加法的本质是:把所有数当成整数相加,超出的进位不断回卷,最终结果都落在 [ 0x0001 , 0xFFFF ] [\text{0x0001}, \text{0xFFFF}] [0x0001,0xFFFF]
由于整数加法本身满足结合律,进位回卷操作不改变最终结果的顺序无关性,因此反卷加法也满足结合律。
例:
0x1234 ⊕ ( 0xABCD ⊕ 0x9999 ) \text{0x1234} \oplus (\text{0xABCD} \oplus \text{0x9999}) 0x1234(0xABCD0x9999)
= 0x1234 ⊕ 0x4566 = \text{0x1234} \oplus \text{0x4566} =0x12340x4566
= 0x579A = \text{0x579A} =0x579A
( 0x1234 ⊕ 0xABCD ) ⊕ 0x9999 (\text{0x1234} \oplus \text{0xABCD}) \oplus \text{0x9999} (0x12340xABCD)0x9999
= 0xBE01 ⊕ 0x9999 = \text{0xBE01} \oplus \text{0x9999} =0xBE010x9999
= 0x579A = \text{0x579A} =0x579A
两种顺序结果相同。

5.3 单位元(Identity Element)

命题: 存在 e ∈ V e \in V eV 使得对所有 X ∈ V X \in V XV e ⊕ X = X ⊕ e = X e \oplus X = X \oplus e = X eX=Xe=X
答案: e = 0xFFFF e = \text{0xFFFF} e=0xFFFF
验证:
对任意 X ∈ V X \in V XV(即 X ≠ 0 X \neq 0 X=0):
0xFFFF ⊕ X = X + 0xFFFF \text{0xFFFF} \oplus X = X + \text{0xFFFF} 0xFFFFX=X+0xFFFF
由于 X ≥ 1 X \geq 1 X1,所以 X + 0xFFFF ≥ 0x10000 X + \text{0xFFFF} \geq \text{0x10000} X+0xFFFF0x10000,发生进位回卷:
( 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 XV,存在 X ′ ∈ V X' \in V XV 使得 X ⊕ X ′ = 0xFFFF X \oplus X' = \text{0xFFFF} XX=0xFFFF
答案: X ′ = 0xFFFF − X X' = \text{0xFFFF} - X X=0xFFFFX(即 X X X 的按位取反,即反码)
验证:
X ⊕ ( 0xFFFF − X ) X \oplus (\text{0xFFFF} - X) X(0xFFFFX)
由于 X ≥ 1 X \geq 1 X1,所以 0xFFFF − X ≤ 0xFFFE \text{0xFFFF} - X \leq \text{0xFFFE} 0xFFFFX0xFFFE,即 X ′ ≥ 1 X' \geq 1 X1 X ′ ∈ V X' \in V XV
普通加法: X + ( 0xFFFF − X ) = 0xFFFF X + (\text{0xFFFF} - X) = \text{0xFFFF} X+(0xFFFFX)=0xFFFF,没有进位,结果恰好是 0xFFFF \text{0xFFFF} 0xFFFF(单位元)。
例: X = 0x1234 X = \text{0x1234} X=0x1234,则 X ′ = 0xFFFF − 0x1234 = 0xEDCB X' = \text{0xFFFF} - \text{0x1234} = \text{0xEDCB} X=0xFFFF0x1234=0xEDCB
0x1234 ⊕ 0xEDCB = 0xFFFF \text{0x1234} \oplus \text{0xEDCB} = \text{0xFFFF} 0x12340xEDCB=0xFFFF

这正是为什么校验和算法用"取反码":把发送方的校验和与数据一起发送,接收方把所有数(含校验和)反卷相加,结果恰好是 0xFFFF(全1),表示正确。

5.5 交换律(Commutativity)

命题: X ⊕ Y = Y ⊕ X X \oplus Y = Y \oplus X XY=YX
验证: 普通整数加法满足交换律,反卷操作不依赖顺序,因此反卷加法也满足交换律。

六、为什么不能包含 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 0xFFFFX=X(原来的单位元,如上验证)
  • 0x0000 ⊕ X = ? \text{0x0000} \oplus X = ? 0x0000X=?
    对于 0x0000 ⊕ X \text{0x0000} \oplus X 0x0000X
    0x0000 + X = X \text{0x0000} + X = X 0x0000+X=X(无进位,结果为 X X X
    所以 0x0000 ⊕ X = X \text{0x0000} \oplus X = X 0x0000X=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=e1e2=e2
所以两个单位元必须相等,即单位元唯一。
但在 V ′ V' V 中, 0x0000 \text{0x0000} 0x0000 0xFFFF \text{0xFFFF} 0xFFFF 都满足单位元的定义,这与群的要求矛盾。

6.3 0x0000 导致逆元不存在

更致命的问题:如果我们选 0x0000 \text{0x0000} 0x0000 作为单位元(即"零"),那么对于任意 X ∈ V ′ X \in V' XV
我们需要找到 X ′ X' X 使得:
X ⊕ X ′ = 0x0000 X \oplus X' = \text{0x0000} XX=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} 0x12ABX=0x0000
所以 0x0000 作为单位元时,逆元不存在,群结构崩溃

6.4 选 0xFFFF 作为单位元,排除 0x0000

若选 0xFFFF \text{0xFFFF} 0xFFFF 作为单位元,逆元是 X ′ = 0xFFFF − X X' = \text{0xFFFF} - X X=0xFFFFX

  • 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, +) 构成阿贝尔群(交换群)

八、为什么这对网络协议有用?

群结构的实际意义:

  1. 交换律 -> 数据包的 16 位字段顺序可以任意,校验和不变
  2. 结合律 -> 可以分块计算校验和,最后合并,结果一致(适合硬件流水线)
  3. 逆元(取反码) -> 接收方把数据和校验和一起加,结果是 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 关系的可视化

选 0x0000

选 0xFFFF

集合 V' = {0x0000, 0x0001, ..., 0xFFFF}
(包含 0x0000)

选哪个作单位元?

需要找 X' 使 X (+) X' = 0x0000
但反卷加法结果 >= 0x0001
逆元不存在

0xFFFF 的逆元 = 0xFFFF - 0xFFFF = 0x0000
但 0x0000 也满足单位元条件
单位元不唯一

群公理违反,V' 不构成群

解决方案:排除 0x0000
集合 V = {0x0001, ..., 0xFFFF}

单位元唯一 = 0xFFFF
每个元素的逆元 = 按位取反
且逆元在 V 中

(V, +) 构成阿贝尔群

十二、一句话总结

互联网校验和使用反卷加法,在集合 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值