Internet Checksum 把数据按 16 位分组,用反码加法折叠进位,再对结果取反。本文用"账本溢出回卷"类比解释算法,区分检错与纠错能力,并给出 Java 完整实现、奇数字节补零和两个已知测试向量。
先把十六位账本摊开
把每两个字节看成账本上的一格,每格最多记 16 位。普通加法溢出的进位若直接丢掉,前后两种数据可能更容易碰出同一结果;Internet Checksum 的做法是把溢出的 1 绕回最低位继续相加,最后把整本账逐位取反。接收方把数据和校验值按同样规则相加,若得到全 1,就说明这份账至少通过了本次检验。
溢出的进位绕回来
数据按网络字节序组成 16 位无符号字。所有字使用一补码加法累加:每当和超过 0xFFFF,就把高位进位加回低 16 位。处理完后再重复折叠可能残留的进位,最后按位取反并截为 16 位。奇数字节长度在计算时在末尾补一个零字节,但这个补位只是数学约定,不一定进入实际传输内容。算法能发现许多常见位翻转,却不是密码学摘要。
把主样例走一遍
测试向量由 0x0001、0xF203、0xF4F5、0xF6F7 四个字组成。普通和超过 16 位,把高位 2 回卷到低位后得到 0xDDF2,逐位取反为 0x220D。字符串 123456789 有九个字节,末尾的字符 9 与补零组成 0x3900,结果为 0xF62A。两个向量都写成断言,可同时检查字节序、进位折叠和奇数长度。
账目一致不等于不可伪造
循环处理到偏移 i 时,sum 等价于此前所有 16 位字的一补码和,且高位进位已经折叠。读取字节时必须先 & 0xFF,否则 Java 的 byte 符号扩展会污染高位。第二字节不存在时使用 0。最终 ~sum & 0xFFFF 明确把结果限制到无符号 16 位范围。
完整可运行代码
下面的程序只使用语言标准库,主函数直接包含可复制测试。Python 运行 python main.py,Java 使用 javac Main.java 后执行 java -ea Main,C++ 使用支持 C++17 的编译器。断言开启时,只要实现或预期结果不一致,进程就会以失败状态结束。
java
import java.nio.charset.StandardCharsets;
public class Main {
static int checksum(byte[] data) {
long sum = 0;
for (int i = 0; i < data.length; i += 2) {
int high = data[i] & 0xFF;
int low = (i + 1 < data.length) ? (data[i + 1] & 0xFF) : 0;
sum += (high << 8) | low;
sum = (sum & 0xFFFF) + (sum >>> 16);
}
while ((sum >>> 16) != 0) {
sum = (sum & 0xFFFF) + (sum >>> 16);
}
return (int) (~sum) & 0xFFFF;
}
public static void main(String[] args) {
byte[] vector = {0x00, 0x01, (byte)0xF2, 0x03,
(byte)0xF4, (byte)0xF5, (byte)0xF6, (byte)0xF7};
assert checksum(vector) == 0x220D;
assert checksum("123456789".getBytes(StandardCharsets.US_ASCII)) == 0xF62A;
assert checksum(new byte[0]) == 0xFFFF;
System.out.printf("0x%04X%n", checksum(vector));
}
}
复杂度分析
设字节数为 n,每两个字节执行常数次操作,时间 O(n),额外空间 O(1)。算法适合边读边累加,不必复制报文。若数据分块到达,需要保留跨块的单个尾字节,不能分别给每块补零后再把校验值相加。
复杂度描述必须和代码的实际数据结构一致。只看最内层语句的常数时间,或把"通常很快"当成平均复杂度证明,都会误导选型。测试规模翻倍时应同时观察耗时和峰值内存,再判断渐进结论是否已经在当前数据范围显现。
边界条件
空数据的校验值为 0xFFFF;奇数字节需逻辑补零;字节序必须固定为网络序。Java byte 是有符号类型,拼字时必须掩码。若校验字段本身包含在报文中,发送端计算前要先把该字段置零,接收端验证时则连同收到的校验值一起计算,协议位置要说清楚。
边界不是正文末尾的附属清单,而是输入契约的一部分。调用方需要知道非法输入是抛异常、返回空结果还是给出状态码;同一项目中的测试、日志和监控也应使用相同语义。本文代码选择尽早失败,避免错误值继续流入后续计算。
常见错误
最常见错误是只保留低 16 位而丢弃进位,或只折叠一次却允许累加器积累多个高位。把 Checksum 描述成能保证数据未被篡改也不成立:不同报文可能碰撞,攻击者还能构造补偿变化。另一个误区是逐字节相加,这与协议规定的 16 位网络序分组不是同一算法。
排查时不要只打印最终答案。应记录首次破坏不变量的轮次、当前输入、辅助结构和刚执行的分支。这样错误能落到一个具体更新动作,而不是在整段代码里盲目改条件。对优化版本保留一个慢但直观的小规模基线,往往比增加更多手写样例更有效。
可复制的测试用例
代码断言 RFC 风格四字向量得到 0x220D,奇数字符串得到 0xF62A,并检查空数组。还应把任意偶数字节数据与其校验值拼接后验证全 1;奇数数据则要按协议字段位置处理补位,不能简单尾随两个字节。随机翻转单个位,统计被检出的比例可以帮助理解能力,但不能据此宣称绝对保证。
本地验证会从本文代码块提取源码,在独立临时目录编译或执行。测试结果只报告真实标准输出,不伪造时间数据。读者复制到自己的环境后,建议先保持相同断言,再替换业务输入;若接口有所调整,应同步修改异常约定和预期值。
从正确到可用
实现放进网络栈时,要明确硬件卸载是否已经计算校验和,否则抓包中看到的占位值可能被误判为故障。分片、重组和伪首部各有协议规则,不能只校验负载。指标应区分本地计算失败、接收校验失败和上游主动丢弃,避免把所有异常汇总成一个"网络错误"。
验收时再走一遍
验账时先把原始字节以十六进制两两分组,旁边写出每轮累加值、溢出高位和回卷后的低位,字节序错误会立刻显现。偶数字节向量验证正常分组,奇数字节向量验证逻辑补零,空输入确认约定值。再设计一个只翻转单个位的样例,检查接收端不再得到全 1;随后主动构造两处补偿变化,理解校验和存在碰撞而非误以为代码失效。分块输入测试要让块边界落在一个 16 位字中间,确认实现会保留尾字节。接入协议前把校验字段位置、伪首部范围与硬件卸载行为写入测试夹具,同一个函数只有在相同报文定义下才可比较。
什么时候该用,什么时候换方案
协议评审要把"发现偶然传输错误"和"抵抗主动篡改"分成两栏。前者可使用 Internet Checksum 并依赖现有协议字段,后者应选择带密钥的消息认证码或数字签名,不能靠扩大校验和位数冒充安全。若链路已经有更强的 CRC,仍要遵守上层协议是否要求本字段。计算函数、字段布局和验证流程分别测试,才能区分算法错误、序列化错误与抓包卸载假象。
针对本文的校验算法方案,评审记录还应同时写明输入所有权、异常返回、可重复性和资源上限。以主样例为起点,再加入最小输入、重复值、极端值和无解输入;每一项不仅保存期望输出,也保存推导理由。只有代码、复杂度说明与测试契约三者一致,才可以把一次演示视为可复用实现。
进一步上线前,应记录输入规模分布、失败类型、超时和资源峰值,而不是只统计成功请求的平均耗时。若实现含随机性,要保存种子;若依赖排序或哈希,要固定平局规则;若涉及数值累计,要为溢出和精度损失设置可观测指标。这样一次异常才能被复现,而不是被一句"偶发"带过。
总结
Internet Checksum 的价值在于便宜、可流式、容易由硬件实现,而不是不可碰撞。理解回卷进位、网络字节序和奇数补位后,代码很短;理解它的能力边界,才不会把检错工具误当成安全证明。