
还记得上次写CRC是在哪里写的吗?在书本上,在草稿纸上?
还记得上次写CRC是在什么时候吗?五年前,还是十年前?
还记得CRC是怎么写的吗?好像有个什么多项式吧?
还以为这辈子再也用不到CRC校验了。它只会出目前课本上试卷上,没想到它会再次出目前我写的程序里。
说实话我早就将CRC忘得一干二净了,当然我也有机智的时候,目前是用算法写,而不是手写,不必着急。网上搜罗了一下,便和对接同事交流,CRC校验中几个关键要素必须保持一致吧!列如说:多项式、初始值等等。同事超级豪爽,直接把算法扔了过来,C语言实现的。一对比和Java语言实现得一模一样。说什么好呢,语言只是工具。
循环冗余校验(Cyclic Redundancy Check,CRC),是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种信道编码技术,主要用来检测或校验数据传输或者保存后可能出现的错误。CRC是一种用于校验通信链路上数字传输准确性的计算方法(通过某种数学运算来建立数据位和校验位的约定关系的)。CRC是利用除法及余数****的原理来做错误侦测的。CRC16从性能上和开销上,均优于奇偶校验、算术和校验等方式,在数据存储和数据通讯领域,应用得比较广泛。磁盘驱动器的读写也采用了CRC16。通用的图像存储格式GIF、TIFF等也都用CRC作为检错手段。百度百科
CRC16,16代表二进制数的位数,一个字节8位,两个字节16位,CRC16就是包含了16位的二进制值。以此类推,CRC32,则是包含32位的二进制值。在java.util.zip包中有CRC32算法的实现,主要用于压缩包工具软件的校验。
看到了没,如果还没有用到CRC校验,这说明写的代码还不够底层呀。
目前来看看CRC16的实现代码:
/**
* CRC16循环冗余校验码
* @author 程就人生
* @Date
*/
public class CRCUtils {
/**
* 一个字节占 8位
*/
private static final int BITS_OF_BYTE = 8;
/**
* 多项式
*/
private static final int POLYNOMIAL = 0XA001;
/**
* CRC寄存器默认初始值
*/
private static final int INITIAL_VALUE = 0XFFFF;
/**
* CRC16 编码
* @param bytes 编码内容
* @return 编码结果
*/
public static int crc16(int[] bytes) {
int res = INITIAL_VALUE;
for (int data : bytes) {
res = res ^ data;
for (int i = 0; i < BITS_OF_BYTE; i++) {
res = (res & 0X0001) == 1 ? (res >> 1) ^ POLYNOMIAL : res >> 1;
}
}
return res;
}
}
生成一个CRC16的流程为:
1)预置一个16位寄存器为0XFFFF(全为1),称之为CRC寄存器;
2)把数据帧中的第一个字节的8位与CRC寄存器中的低字节进行异或运算,结果存回CRC寄存器;
-
将CRC寄存器向右移一位,最高位填以0,最低位移出并检测;
-
如果LSB(最低位)为0,重复第三步(下一次移位);如果LSB(最低位)为1,将CRC寄存器与一个预设的固定值0XA001(多项式)进行异或运算;
-
重复第三步和第四步直到8次移位,这样处理完一个完整的8位;
-
重复第2步到第5步来处理下一个八位,直到所有的字节处理结束;
-
最终CRC寄存器的值就是CRC16的值。
下面来测试一下:
public static void main(String[] argo){
int[] aa = {43,44,45};
System.out.println("int型数组:" + Arrays.toString(aa) + " CRC校验码为:" + crc16(aa));
}
打印结果:
int型数组:[43, 44, 45] CRC校验码为:5597
和对接同事核对了一下,验证结果也一样,本以为这样就万事大吉了,但是协议对接起来,校验到了他那边怎么都不通过,这是为什么呢?
突然想起,我俩使用的都是字节,但我的算法由int型数组改为byte型数组,改的并不彻底呀,赶紧又把代码核对了一遍:
/**
* CRC16 编码
* @param bytes 编码内容
* @return 编码结果
*/
public static int crc16Before(byte[] bytes) {
int res = INITIAL_VALUE;
for (byte data : bytes) {
res = res ^ data;
for (int i = 0; i < BITS_OF_BYTE; i++) {
res = (res & 0X0001) == 1 ? (res >> 1) ^ POLYNOMIAL : res >> 1;
}
}
return res;
}
/**
* CRC16 编码
* @param bytes 编码内容
* @return 编码结果
*/
public static int crc16(byte[] bytes) {
int res = INITIAL_VALUE;
for (byte data : bytes) {
// 把byte转换成int后再计算
res = res ^ (data & 0XFF);
for (int i = 0; i < BITS_OF_BYTE; i++) {
res = (res & 0X0001) == 1 ? (res >> 1) ^ POLYNOMIAL : res >> 1;
}
}
return res;
}
再调整测试代码:
public static void main(String[] argo){
int[] aa = {43,44,45,213};
System.out.println("int型数组:" + Arrays.toString(aa) + " CRC校验码为:" + crc16(aa));
ByteBuf buf = Unpooled.buffer(4);
buf.writeByte(43);
buf.writeByte(44);
buf.writeByte(45);
buf.writeByte(213);
byte[] data = buf.array();
// CRC循环冗余码
int crc = crc16Before(data);
buf = Unpooled.buffer(6);
buf.writeBytes(data);
// 高低位互换,小字端在前,大字端在后
buf.writeShortLE(crc);
System.out.println("错误的算法:CRC校验码为:" + crc + ",发生出去的协议为:" + ByteBufUtil.hexDump(buf.array()).toUpperCase());
// CRC循环冗余码
crc = crc16(data);
buf = Unpooled.buffer(6);
buf.writeBytes(data);
// 高低位互换,小字端在前,大字端在后
buf.writeShortLE(crc);
System.out.println("正确的算法:CRC校验码为:" + crc + ",发生出去的协议为:" + ByteBufUtil.hexDump(buf.array()).toUpperCase());
}
测试结果:
int型数组:[43, 44, 45, 213] CRC校验码为:50708
byte型数组,错误的算法:CRC校验码为:-50709,发生出去的协议为:2B2C2DD5EB39
byte型数组,正确的算法:CRC校验码为:50708,发生出去的协议为:2B2C2DD514C6
这是为什么呢?
在Java中,byte类型占1个字节8位,存储范围在2的-27次方到27次方,最大存储255;短整型short有2个字节16位,存储范围在2的-215次方到215次方,最大存储65525;整型int有4个字节32位,存储范围再次扩大。
如果我在计算时不把byte转成int型,单个计算可能没有问题,数字小的计算可能也没有问题,但是数字稍微大一些,字节数稍微多一些,作异或运算时问题就出现了。
OK,问题解决了,关于CRC校验到此可以告一段了。


