介绍

计算校验和、验证校验和

发送端计算校验和的步骤

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