数据链路层的循环冗余校验CRC(Cyclic Redundancy Check)
简介
背景
为了应对现实中的通信链路在传输过程中可能会产生比特差错(误码率,BER——Bit Error Rate,比如误码率为10-10表示平均没传送1010比特就会出现一个比特的差错,1可能变成0,0可能变成1;当然误码率与信噪比有很大关系,提高信噪比可降低误码率。然而实际的通信链路并非理想的,它不可能使误码率下降到0),为了保证数据传输的可靠性,在计算机网络传输数据时,必须采用各种差错检测措施。目前在数据链路层广泛使用了CRC的检错技术。
原理(以CRC-16/XMODEM为例)
CRC校验的原理是根据接收端与发送端共同约定的除数P,在要发送的数据末尾补上一定位数的CRC校验码,以使“要发送数据+校验码”能够被共同约定的除数P以异或^
运算的方式整除,接收端以此作为数据在传输过程中是否发生比特差错的判断依据。这个【使得要发送数据可以被共同约定的除数P整除的】CRC校验码被称为帧检验序列FCS(Frame Check Sequence)。P通常用多项式表示,比如CRC-16的G(x)=x^16^ +x^15^+x^2^+1
表示除数P为11000000000000101
(共17位)。
异或运算就是教科书中提到的“二进制的模2运算”。
此处可以看到CRC与FCS不是同一个概念,CRC是一种检错方法,而FCS是添加在要发送数据后面、用来校验的冗余码。
补入此博客中的CRC校验原理。
应用场景
- 以太网中使用的是crc-32。
CRC各版本及反转、初始值含义详解
补入本文。
初始值:是指【被除数(即需要添加CRC校验码的原数据在补0后)与初始值】先进行异或运算,再进行下面的crc计算。比如初始值为0xFFFF
时,相当于将【需要添加CRC校验码的原数据】进行取反码操作。
计算过程
计算过程概述
在发送端,先把要发送的数据划分为组,假定每组k个比特。假定待发送的数据 M = 101001 (k = 6)
,CRC就是根据【发送方与接收方共同约定的】除数P(n+1位)生成【用来差错检测用的n位冗余码(FCS)】,并在待发送数据M后添加FCS,由此构成一个新的(k+n)位的帧,使其能够被除数P整除。发送端将新的(k+n)位帧发送出去,接收端在收到后,对每一个收到的(k+n)位帧进行CRC校验:把收到的每一个帧与共同约定的除数P进行异或
运算1,然后检查得到的余数R是否为0。若为0,则说明无差错;不为0,则说明有差错。
n位冗余码的具体计算过程
- 根据G(x)得到约定的除数P,除数P是n+1位。比如G(x)=x8 +x6+x5+1 表示除数P是 101100001,请注意,根据G(x)得到除数P,是从0位开始算——即多项式最后的1,表示除数的0位是1而非0。
- 根据待发送的数据M和第一步中【根据G(x)得到的除数P】,得出校验码。具体计算过程如下
①假设待发送的数据M的二进制位数为k位,除数P二进制位数为n+1位,则在待发送的数据M后面增添n位“0”——即二进制的模2运算——M*2n,形成新的k+n位数据M’。M’取前【与初始值相同位数的】位与初始值进行异或操作,比如初始值有16位,则从M’中取前16位(可包含数据M在后面增添的0部分)。
②然后将k+n位的M’异或除以【发送和接收方共同约定的】除数P(n+1位),请注意,此处用代码实现时,不能直接简单地M'^P
,详见此批注1和下方给出的代码实现。将【所得到的余数R(二进制的形式,n位)添加到M’增添的后n位0上——比如余数为1,M’在第①步中预留的校验码(全0)位数是三位,则在M’预留的后三位校验码上应改为001
,而非1
——001
称之为FCS(帧校验序列)】,即关注余数FCS添加到M’时的补0问题——将FCS填加到k+n位的M’的n位“0”上。但要注意的是,所得校验码全为0时(即整除)也都不能省略。 - 此时,将 M’后n位已更新为FCS的新数据【2nM+FCS】作为要发送数据发出,接收端收到后检查是否其能够被共同约定的除数P(二进制的模2运算)异或1整除。若能够整除,则判定校验通过,数据无误;不能整除,则判定校验不通过,数据在传输过程中出现了差错。
代码实现
Java
实现CRC-16/XMODEM
import java.math.BigInteger;
public class CrcCheckCodeGenerator {
BigInteger Dividend;//CRC被除数(要加校验码的字串),为了防止位数过长使用的BigInteger
BigInteger CRC_Divisor;//CRC除数
BigInteger initialValue;//初始值
public CrcCheckCodeGenerator(String Dividend, String binaryDivisor, String initialValue) {
// TODO Auto-generated constructor stub
int fillingZeroNumber = binaryDivisor.length()-1;
for(int i = 0; i<fillingZeroNumber; i++) {
Dividend = Dividend+"0";
}
根据初始值截取Dividend
this.Dividend = new BigInteger(Dividend,2);
this.CRC_Divisor = new BigInteger(binaryDivisor,2);
}
public String generateCrcCheckCode() {
String CRC_DivisorString = CRC_Divisor.toString(2);
int binaryResultBit = CRC_DivisorString.length()-1;
String binaryResult = "";//CRC最终校验的结果
if (Dividend!=null && CRC_Divisor!=null) {
String dividendString = Dividend.toString(2);
int cRC_Divisor = Integer.parseInt(CRC_DivisorString,2);
int i = 0;//代表从Dividend运算到了第几位
while (i<dividendString.length()||(binaryResult.length()==CRC_DivisorString.length())) {
if (binaryResult.length()<= binaryResultBit) {
binaryResult += dividendString.charAt(i)+"";
++i;
}else {
int result = Integer.parseInt(binaryResult, 2);
result = result^cRC_Divisor;
if (result == 0) {//防止异或整除后,依然保留一位0
binaryResult = "";
}else {
binaryResult = Integer.toBinaryString(result);
}
}
}
//补齐最终结果的位数
while(binaryResult.length() < CRC_DivisorString.length()-1) {
binaryResult = "0"+binaryResult;
}
return binaryResult;
// resultCrcChekCode = Dividend.xor(CRC_Divisor);
}else if (Dividend == null) {
System.out.println("要添加CRC校验码的待发送数据为空");
binaryResult = "0".repeat(binaryResultBit);
return binaryResult;
}else {
System.out.println("CRC计算中要用到的除数为空");
binaryResult = "0".repeat(binaryResultBit);
return binaryResult;
}
}
}
实现CRC-16/CCITT-FALSE
public class CrcCheckCodeGenerator {
String Dividend;
String CRC_Divisor;
String initial_Value;
//为CRC-16/CCITT-FALSE版本的构造函数,G(x)=x^16+x^12+x^5+1,除数、初始化均直接在构造函数中完成
public CrcCheckCodeGenerator(String Dividend) {
// TODO Auto-generated constructor stub
//CRC-16/CCITT-FALSE版本的CRC初始值为0xFFFF,Dividend前同位数与其^,相当于取反操作。
int initial_Value_Int = 0xFFFF;
Dividend = Dividend + "0000000000000000";
int firstHalfofDividend = Integer.parseInt(Dividend.substring(0, 16), 2);
firstHalfofDividend = firstHalfofDividend ^ initial_Value_Int;
Dividend = Integer.toBinaryString(firstHalfofDividend)+Dividend.substring(16);
this.Dividend = Dividend;
this.CRC_Divisor = "10001000000100001";
}
//配合CRC-16/CCITT-FALSE 使用,除数CRC_Divisor字符串写死无法更改。
public String generateCrcCheckCodeForDRO() {
int binaryResultBit = 16;//因为专用于CRC-16/CCITT-FALSE,所以CRC校验码的最终位数可以确定
String binaryResult = "";//用于存储从Dividend逐个取出用来逐步计算CRC校验码、最终结果的变量
if (Dividend!=null && CRC_Divisor!=null) {
//需要计算CRC的字符串存为String变量,从中截取补位继续^运算。
// String dividendString = Dividend.toString(2);
int cRC_Divisor = Integer.parseInt(CRC_Divisor,2);//String转Int
int i = 0;//代表从Dividend运算到了第几位
while (i<Dividend.length()||(binaryResult.length()==17)) {//在取完被除数Dividend的所有位后,如果binaryResult位数还足够17位、则再与除数CRC_Divisor进入一次循环来计算异或结果;不足17位时,则此时的binaryResult直接就是CRC的校验结果。
if (binaryResult.length() <= binaryResultBit) {//因为binaryResult的长度是从0开始的,而除数P是17位,binaryResultBit已经在除数的基础上减1了,所以最后一次计算余数时,是 = 。
binaryResult += Character.toString(Dividend.charAt(i));
++i;
}else {
//
int result = Integer.parseInt(binaryResult, 2);
result = result^cRC_Divisor;
// resultCrcChekCode = Dividend.xor(CRC_Divisor);//此方法的异或取值仅支持4位^4位
if (result == 0) {//防止异或整除后依然保留一位0的情况。
binaryResult = "";
}else {
binaryResult = Integer.toBinaryString(result);
}
}
}
//自动补齐位数,直到16位停止
while(binaryResult.length() < 16) {
binaryResult = "0"+binaryResult;
}
}else if (Dividend == null) {
System.out.println("要添加CRC校验码的待发送数据为空");
String nullDataString = "0";
binaryResult = nullDataString.repeat(binaryResultBit);//即最终输出binaryResultBit个nullDataString
}else {
System.out.println("CRC计算中要用到的除数为空");
String nullDataString = "0";
binaryResult = nullDataString.repeat(binaryResultBit);
}
return binaryResult;
}
}
CRC在线计算器
分享一个CRC多版本在线计算器。