去年的工作中遇到一个需求涉及到SLIP协议、CSI协议和CheckSum算法,特此记录与总结一下。

需求描述

公司有一款硬件设备,该硬件设备可以采集一些数据(20KB左右),该设备支持串口通信,现在需要在Android App端获取设备采集的数据并上传至服务器,其中Android App使用蓝牙通信的方式,增加一个蓝牙转串口的小设备,以此来和公司设备进行串口通信。

通信协议整体描述

公司设备所使用的串口通信协议分为四层,如下图:

SLIP、CSI和CheckSum算法_CSI


最下面一层(第四层)为SLIP协议层,SLIP协议层的数据域为第三层的IP协议层,IP协议层的数据域为第二层的UDP协议层,UDP协议层的数据域为第一层的CSI协议层。前三层协议都由协议头和数据区组成,四层协议层层嵌套最后组成了完成的串口通信协议。

SLIP协议

SLIP(Serial Line Internet Protocol,串行线路网际协议),该协议是Windows远程访问的一种旧工业标准,主要在Unix远程访问服务器中使用,现今仍然用于连接某些ISP。因为SLIP协议是面向低速串行线路的,可以用于专用线路,也可以用于拨号线路,Modem的传输速率在1200bps到19200bps。(摘自百度百科​​SLIP(网际协议)​​)

上述介绍可能有些难以理解,但是如果将SLIP写成Serial Line IP(串行线路 IP )就很好理解了,可以将SLIP看成是对IP协议数据包的再封装,封装完了之后可以在串口通信中使用。那么为什么说它是一种旧的工业标准呢?因为现在它已经被PPP(点对点协议)广泛替代了。

SLIP 是一个包组帧协议(将IP协议数据包封装成帧),它很简单,仅仅定义了在串行线路上将数据包封装成帧的一系列字符,它没有提供寻址、包类型标识、错误检查 / 修正或者压缩机制。

SLIP定义的特殊字符及使用规则

SLIP定义的特殊字符如下:

SLIP、CSI和CheckSum算法_UDP_02


这些字符的使用规则如下:

1、IP数据报以一个称作END(0xC0)的特殊字符结束,同时,为了防止数据报到来之前的线路噪声被当成数据报内容,大多数实现在数据报的开始处也传一个END字符;(SLIP 的帧以END开头和结束

2、如果IP报文中某个字符为END,那么就要连续传输两个字节ESC,ESC_END(0XDB,0xDC)来取代它;

3、如果IP报文中某个字符为SLIP的ESC字符,那么就要连续传输两个字节ESC,ESC_ESC(0xDB,0xDD)来取代它。

IP协议

IP协议是TCP/IP协议族中最为核心的协议,它提供不可靠、无连接的服务,也即依赖其他层的协议进行差错控制,协议数据格式如下:

SLIP、CSI和CheckSum算法_UDP_03


上图中画红框的是本文的另一个重点——CheckSum算法字段。

UDP协议

UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,具体格式如下:

SLIP、CSI和CheckSum算法_SLIP_04

UDP协议和IP协议一样,都有一个CheckSum字段(16位,2Byte)

CSI协议

由于IP协议和UDP协议是不可靠的,因此必须在他们之上再添加一层用于提供更高的可靠性,本文中使用的是CSI协议,CSI协议头信息如下:

SLIP、CSI和CheckSum算法_checksum_05

CSI头信息中包含了数据包长度、每包的序列号、授权码、消息类型、CRC校验码,以此提高传输的可靠性。

CheckSum算法

在IP协议和UDP协议中都有一个CheckSum字段,下面说一下这个字段是怎么来的,即CheckSum算法。

什么叫CheckSum

CheckSum算法,也叫和校验,顾名思义,将数据的各项相加求和,以此判断数据的正确性

与另一种常用的校验方式CRC校验相比,和校验实现简单,但其校验的精确度相比CRC稍差。如果不是对数据的准确性要求特别高的使用场合,校验和都是一种不错的短数据校验方式。

CheckSum算法的实现步骤

CheckSum算法(16 bit,2 byte)的实现步骤如下:

1、先将需要计算checksum数据中的checksum设为0;
2、将计算checksum的数据按2 byte划分开来,每2 byte组成一个16 bit的值,如果最后有单个byte的数据,补一个byte的0组成2 byte;
3、把所有16位的字相加,如果遇到进位,则将高于16字节的进位部分的值加到最低位上,举例,0xBB5E+0xFCED=0x1 B84B,则将1放到最低位,得到结果是0xB84C
4、将所有字相加得到的结果应该为一个16位的数,将该数取反则可以得到检验和checksum。

在具体实现时可以将步骤三作如下修改,以方便实现:
3.1、将所有的16 bit值累加到一个32 bit的值中;
3.2、将32 bit值的高16 bit与低16 bit相加到一个新的32 bit值中,若新的32 bit值大于0xFFFF, 再将新值的高16 bit与低16 bit相加;

在java中实现CheckSum算法的代码如下:

    /***
* @param hexString
* 需要校验数据的16进制字符串
* @param preTotal
* 和校验字段的初始值:"0000"
* @return 未经过取反的16进制字符串
* */
public static String checkSum(String hexString, String preTotal) {
String hexStr = hexString;
List<String> hexStrArrey = new ArrayList<String>(); //步骤二
int i = 0;
int len = hexString.length() / 4;
for (i = 0; i < len; i++) {
hexStrArrey.add(hexStr.substring(i * 4, i * 4 + 4));
}
int total = Integer.parseInt(preTotal, 16); //步骤三
for (int j = 0; j < hexStrArrey.size(); j++) {
total += Integer.parseInt(hexStrArrey.get(j), 16);
}

String totalHex = Integer.toHexString(total);

int zeroNum = 8 - totalHex.length();
if (zeroNum > 0) {
String zero = "";
for (int k = 0; k < zeroNum; k++) {
zero += "0";
}
totalHex = zero + totalHex;
}
String heigh = totalHex.substring(0, 4);
String low = totalHex.substring(4, totalHex.length());

int heighAddlow = Integer.parseInt(heigh, 16)
+ Integer.parseInt(low, 16);

String heighAddlowStr = Integer.toHexString(heighAddlow);
int resZeroLen = 4 - heighAddlowStr.length();

if (resZeroLen > 0) {
String zero = "";
for (int k = 0; k < resZeroLen; k++) {
zero += "0";
}
heighAddlowStr = zero + heighAddlowStr;
}

return heighAddlowStr.toUpperCase();
}

/***
* @param heighAddlowStr 需要取反的16进制字符串
* @return 按位取反之后的16进制字符串
* */
public static String getAginst(String heighAddlowStr) { //步骤四
String binaryStr = hexString2binaryString(heighAddlowStr);
String temp = binaryStr.replaceAll("1", "3");
String temp2 = temp.replaceAll("0", "1");
String temp3 = temp2.replaceAll("3", "0");
return binaryString2hexString(temp3).toUpperCase();
}

public static String binaryString2hexString(String bString) {
if (bString == null || bString.equals("") || bString.length() % 8 != 0)
return null;
StringBuffer tmp = new StringBuffer();
int iTmp = 0;
for (int i = 0; i < bString.length(); i += 4) {
iTmp = 0;
for (int j = 0; j < 4; j++) {
iTmp += Integer.parseInt(bString.substring(i + j, i + j + 1)) << (4 - j - 1);
}
tmp.append(Integer.toHexString(iTmp));
}
return tmp.toString();
}

IP、TCP/UDP中如何实现CheckSum?

在生成IP协议的CheckSum字段时和生成UDP协议的CheckSum字段略有不同,具体区别如下:

1、 生成IP协议的CheckSum字段只需要使用IP头数据;关键代码如下:

        String data = this.ipHandr.ver_ihl + this.ipHandr.tos + this.ipHandr.tlen + this.ipHandr.fragid + this.ipHandr.fragoff + this.ipHandr.ttl + this.ipHandr.proto + this.ipHandr.checksum + this.ipHandr.sAddr + this.ipHandr.dAddr;
String checkSum = checkSum(data, "0000");
this.ipHandr.checksum = getAginst(checkSum);

2、 TCP/UDP的CheckSum计算的数据,除了包含TCP/UDP头及TCP/UDP的数据域外,还需要增加一个伪TCP/UDP头,这样,TCP/UDP的CheckSum计算数据如下:
伪头部+TCP/UDP头部+TCP/UDP数据

关键代码如下:

        String pseudoHeader = this.ipHandr.sAddr + this.ipHandr.dAddr + "0011" + this.updHandr.length;
String pseudoHeaderCheckSum = checkSum(pseudoHeader, "0000");
String udpHeader = this.updHandr.sPort + this.updHandr.dPort + this.updHandr.length + this.updHandr.checksum + this.data;
String udpCheckSum = checkSum(
udpHeader,
pseudoHeaderCheckSum);
this.updHandr.checksum = getAginst(udpCheckSum);

TCP/UDP伪首部的作用

伪首部(pseudo header),通常有TCP伪首部和UDP伪首部。在UDP/TCP伪首部中,包含32位源IP地址,32位目的IP地址,8位填充0,8位协议,16位TCP/UDP长度。通过伪首部的校验,UDP可以确定该数据报是不是发给本机的,通过首部协议字段,UDP可以确认有没有误传。