介绍
校验和(Checksum)参考文献RFC1071:https://www.rfc-editor.org/rfc/rfc1071.html
计算校验和、验证校验和
发送端计算校验和的步骤
1)待计算校验和的数据的字节数如果偶数,后面不需要补字节;如果字节数是奇数,在后面补一字节(0x00)。(备注:补的全为0的这个字节仅仅是计算校验和使用,并不发送出去)
2)将每两个相邻的字节作为一组,组成一个16位的整数。
3)将组成的16位的整数系列计算1的补码和—就是先求二进制的和,然后再将超过最高有效位的进位(carries)加到结果的最低有效位上。
4)将上边计算的和取1的补码,即二进制中的1变0,0变1,结果就是最终的校验和。
5)校验和字段清零,存入上一步计算出来的校验。
接收端验证校验和的步骤
1)如果接收到的数据(包含校验和字段)是偶数个字节,不需要考虑补一个字节的事情,直接跳到第3步。
2)如果接收到的数据(包含校验和字段)是奇数个字节,在校验和两个字节的前面插入一个字节(0x00),即在纯数据和校验和之间补一字节(0x00)。
3)每两个字节组成一个16位的整数,对这个整数系列计算1的补码和—就是先求二进制的和,然后再将超过最高有效位的进位(carries)加到结果的最低有效位上。
4)如果计算得到的结果各位全部是1(在1的补码中表示-0),那么就表示结果正确。
原理示意
要计算校验和的纯数据是偶数个字节
计算校验和
例如计算0xF6, 0xF7, 0x00, 0x01, 0xF4, 0xF5, 0xF2, 0x03这个字节序列的校验和。
因为要计算校验和的纯数据是偶数个字节,所以不需要考虑补1个字节的事情。
1) F6F7 + 0001 = F6F8
2) F6F8 + F4F5 = 1 EBED, 其中前面的1是超过最高有效位的进位
3) 1 EBED + F203 = 2 DDF0, 其中前面的2是超过最高有效位的进位
4) 将超过最高有效位的进位丢弃,然后将该丢弃的进位加到DDF0的最低有效位上,即DDF0 + 2 = DDF2
5)将DDF2求1的补码,即0变1,1变0,得到最终的校验和为220D,将这个填到校验和字段中。
6)最后发送出去的字节序列是(包含校验和):0xF6, 0xF7, 0x00, 0x01, 0xF4, 0xF5, 0xF2, 0x03, 0x22, 0x0D
验证校验和
此时要验证的字节系列为0xF6, 0xF7, 0x00, 0x01, 0xF4, 0xF5, 0xF2, 0x03, 0x22, 0x0D,其中后面的220D是发送端发送过来的校验和字段的值。
因为接收到的数据(包括校验和)是偶数个字节,所以不需要考虑补1个字节的事情。
1) F6F7 + 0001 = F6F8
2) F6F8 + F4F5 = 1 EBED, 其中前面的1是超过最高有效位的进位
3) 1 EBED + F203 = 2 DDF0 , 其中前面的2是超过最高有效位的进位
4)2 DDF0 + 220D= 2 FFFD, 其中前面的2是超过最高有效位的进位
5) 将超过最高有效位的进位丢弃,然后将该丢弃的进位加到FFFD的最低有效位上,即FFFD+ 2 = FFFF。结果各位全部为1,正确。
要计算校验和的纯数据是奇数个字节
计算校验和
假设计算0xF6, 0xF7, 0x01这个字节系列的校验和。
1)因为要计算校验和的纯数据是奇数个字节,所以要在后面补一个字节0x00,补充后的字节序列是0xF6, 0xF7, 0x01, 0x00。
2)计算上面字节序列的1的补码和:F6F7 + 0100 = F7F7
3)将F7F7取1的补码,即0变1,1变0,得到最终的校验和为0x0808,将这个填到校验和字段中。
4)最后发送出去的字节序列是(包含校验和):0xF6, 0xF7, 0x01, 0x08, 0x08。
验证校验和
假设收到的要验证的字节系列为0xF6, 0xF7, 0x01, 0x08, 0x08,其中后面的0x0808是发送端发送过来的校验和字段的值。
1) 因为接收到的数据(包括校验和)是奇数个字节,所以要在纯数据和校验和之间补一个字节的0x00,补充后的字节序列是0xF6, 0xF7, 0x01, 0x00, 0x08, 0x08。
2) 计算上面字节序列的1的补码和:F6F7 + 0100 + 0808 = FFFF。结果各位全部为1,正确。
Java代码举例
计算校验和、验证校验和的功能类
package com.thb;
/**
* 该类提供校验和的功能,计算校验和、检查校验和、计算1的补码和
* @author thb
*
*/
public class ChecksumUtil {
/**
* 根据输入的字节数组数据计算校验和。校验和就是RFC1071描述的校验和
* @param sourceData 输入数据
* @return 16比特位的校验和
*/
public static short calculateChecksum(byte[] sourceData) {
short checksum;
// 计算1的补码和
checksum = calculateOnesComplementSum(sourceData);
// 将1的补码和取反,即0变1,1变0,就是校验和
checksum = (short)(~checksum);
return checksum;
}
/**
* 验证校验和
* 因为接收到的数据中包含了发送端填写的校验,所以接收端对接收到的数据计算
* 1的补码和,如果结果全为1,校验正确;否则,校验错误。
* @param receivedData 接收端收到的数据,其中最后两个字节是校验和
* @return true:检查成功, false:检查失败
*/
public static boolean checkChecksum(byte[] receivedData) {
short onesComplementSum;
if ((receivedData.length % 2) == 0) {
// 如果传入的数据是偶数个字节,那么直接计算1的补码和
onesComplementSum = calculateOnesComplementSum(receivedData);
} else {
// 如果传入的数据是奇数个字节,要在数据后面补一个字节0x00,后面再跟两个字节的校验和
// 构造一个数据数组,偶数个字节
byte[] data = new byte[receivedData.length + 1];
// 将原数据(除校验和两个字段外)先拷贝到data中
System.arraycopy(receivedData, 0, data, 0, receivedData.length - 2);
// 数据后面最后一个字节填充0x00
data[data.length - 3] = (byte)0x00;
// 将校验和两个字节拷贝到data中
System.arraycopy(receivedData, receivedData.length - 2 , data, data.length - 2, 2);
// 计算1的补码和
onesComplementSum = calculateOnesComplementSum(data);
}
return onesComplementSum == (short)0xFFFF;
}
/**
* 根据输入的字节数组数据计算16比特位的1的补码和(1's complement sum)。
* 就是将输入数据每两个字节组成一个16比特位的整数,然后将该整数系列二进制相加,
* 并经超过最高有效位的进位丢弃,并经该丢弃的进位加到最低有效位上。
* @param data 输入数据
* @return 16比特位的1的补码和
*/
public static short calculateOnesComplementSum(byte[] data) {
// 考虑到进位,所以中间结果用32比特位的整数存放,高位两个字节存储的是进位
int middleSum = 0;
for (int i = 0; i < data.length; i += 2) {
if (data.length % 2 == 0) { // 输入数据的总字节数是偶数
// 将两个相邻的字节组合成一个整数,并和前面的整数和相加
middleSum += ((0xFF00 & (data[i] << 8)) | (0x00FF & data[i + 1]));
} else { // 输入数据的总字节数是奇数
// 如果已经到了数据的最后一个字节,后面要补一个各bit位全为0的字节
if (i == (data.length - 1)) {
middleSum += ((0xFF00 & (data[i] << 8)) | (0x00FF & 0x00));
} else {
middleSum += ((0xFF00 & (data[i] << 8)) | (0x00FF & data[i + 1]));
}
}
}
// 定义进位变量
short carries;
// 将进位取出来
carries = (short)((middleSum & 0xFFFF0000) >> 16);
// 因为将进位加到后面两个字节上,可能又产生了进位,所以要用循环判断处理,
// 直到不产生进位了为止
while (carries != 0) {
// 将sum的前面两个字节清零,准备下面的计算
middleSum = (middleSum & 0x0000FFFF);
// 将进位加到后面两个字节上
middleSum += carries;
// 将进位再取出来,因为上面相加后可能又产生了进位
carries = (short)((middleSum & 0xFFFF0000) >> 16);
}
// 将上面计算结果的低端2个字节取出来返回,就是1的补码和
return (short)(middleSum & 0x0000FFFF);
}
}
调用场景举例:要计算校验和的纯数据是偶数个字节
package com.thb;
public class Test2 {
public static void main(String[] args) {
// 发送端的原始数据
byte[] sourceData = new byte[] {(byte)0x01, (byte)0x06, 0x00, 0x06, (byte)0x37, (byte)0x02, (byte)0x23, (byte)0x23};
short checksum = ChecksumUtil.calculateChecksum(sourceData);
System.out.println("checksum: 0x" + Integer.toHexString(Short.toUnsignedInt(checksum)));
byte[] dstData = new byte[] {(byte)0x01, (byte)0x06, 0x00, 0x06, (byte)0x37, (byte)0x02, (byte)0x23, (byte)0x23, (byte)0xa4, (byte)0xce};
boolean result = ChecksumUtil.checkChecksum(dstData);
System.out.println("verify checksum result: " + result);
}
}
运行输出:
checksum: 0xa4ce
verify checksum result: true
调用场景举例:要计算校验和的纯数据是奇数个字节
package com.thb;
public class Test2 {
public static void main(String[] args) {
// 发送端的原始数据
byte[] sourceData = new byte[] {(byte)0x01, (byte)0x00, 0x00, 0x00, (byte)0x04, (byte)0x18, (byte)0x28, (byte)0x38, (byte)0x48};
short checksum = ChecksumUtil.calculateChecksum(sourceData);
System.out.println("checksum: 0x" + Integer.toHexString(Short.toUnsignedInt(checksum)));
byte[] dstData = new byte[] {(byte)0x01, (byte)0x00, 0x00, 0x00, (byte)0x04, (byte)0x18, (byte)0x28, (byte)0x38, (byte)0x48, (byte)0x8a, (byte)0xaf};
boolean result = ChecksumUtil.checkChecksum(dstData);
System.out.println("verify checksum result: " + result);
}
}
运行输出:
checksum: 0x8aaf
verify checksum result: true