concox协议与部标协议存在着很大的不同,特别是包头包尾的定义,部标使用的是打个byte位,然后通过转义的方式来实现,这也是目前主流的处理方式,无论是808,809还是1078与苏标都是单字节的包头包尾,而concox则使用的是双字节来定义包头包尾,中间无转义的方式,其实也有很多私有协议使用的是双字节的包头包尾。
话不多说,我们首先看一下concox的协议格式(以登录包为例):
|
长度 |
描述 |
|
2 |
0x78 0x78 |
||
包长 |
1 |
长度= 协议号 + 信息内容 +信息序列号 + 错误校验 |
|
协议号 |
1 |
0x01 |
|
信息内容 |
终端ID |
8 |
例:如果IMEI是 123456789123456,终端ID 为:0x01 0x23 0x45 0x67 0x89 0x120x34 0x56 |
类型识别码 |
2 |
根据此识别码判断终端类型 |
|
时区语言 |
2 |
时区语言标志,详见下表 |
|
信息序列号 |
2 |
从开机后,每次发送数据序列号都自动加1 |
|
错误校验 |
2 |
“包长度”到“信息序列号”的CRC-ITU值。接收方若收到的信息计算有CRC错误,则忽略,抛弃这个数据包(算法详见附件1) |
|
停止位 |
2 |
固定值: 0x0D 0x0A |
Example: 78 78 11 01 07 52 53 36 78 90 02 42 70 00 32 01 00 05 12 79 0D 0A
首先我们定义一个解码器,写一个初步解析方法叫decodeMessage()
private ConcoxMessage decodeMessage(ChannelHandlerContext ctx, ByteBuf in) {
//可读长度不能小于基本长度
if (in.readableBytes() < ConcoxConstant.MSG_BASE_LENGTH) {
return null;
}
//防止字节流攻击,数据太大为异常数据
if (in.readableBytes() > ConcoxConstant.MSG_MAX_LENGTH) {
in.skipBytes(in.readableBytes());
return null;
}
//初始化包长
int dataLen = 0;
//包长度字节长度
int length=0;
while (true) {
//查找消息头,如果未找到则丢弃所有数据
in.markReaderIndex();
//获取包头
int head=in.readShort();
if(head==0x7878)
{
dataLen=in.readByte()&0xFF;
length=1;
//可读长度不能小于数据长度加包尾
if (in.readableBytes() < dataLen+2) {
in.resetReaderIndex();
return null;
}
break;
}else if(head==0x7979)
{
dataLen=in.readShort();
length=2;
//可读长度不能小于数据长度加包尾
if (in.readableBytes() < dataLen+2) {
in.resetReaderIndex();
return null;
}
break;
}
//可读长度不能小于基本长度
if (in.readableBytes() < ConcoxConstant.MSG_BASE_LENGTH) {
in.resetReaderIndex();
return null;
}
}
ConcoxMessage concoxMessage = new ConcoxMessage();
concoxMessage.setProtocolType(protocolType);
//协议号
int msgId = in.readByte()&0xFF;
concoxMessage.setMsgId(msgId);
//终端号与消息体处理
if(msgId==0x01) {
//读取终端号码
byte[] terminalNumArr = new byte[8];
in.readBytes(terminalNumArr);
String terminalNum = ByteBufUtil.hexDump(terminalNumArr);
concoxMessage.setTerminalNum(terminalNum.replaceAll("^(0+)", ""));
//读取剩余消息体(信息内容(去掉设备号码)+信息序列号)
byte[] remainingMsgBodyArr = new byte[dataLen-11];
in.readBytes(remainingMsgBodyArr);
//组合完整消息体(包长度+协议号+信息内容+信息序列号)
ByteBuf frame= ByteBufAllocator.DEFAULT.heapBuffer(dataLen +1);
//包长度
frame.writeByte(dataLen);
//协议号
frame.writeByte(msgId);
//终端号码
frame.writeBytes(terminalNumArr);
//剩余消息体
frame.writeBytes(remainingMsgBodyArr);
concoxMessage.setMsgBody(frame);
}else{
//读取终端号码
String terminalNum = SessionUtil.getTerminalInfo(ctx).getTerminalNum();
concoxMessage.setTerminalNum(terminalNum);
//读取剩余消息体(消息内容+序列号),去掉错误校验
byte[] remainingMsgBodyArr = new byte[dataLen - 3];
in.readBytes(remainingMsgBodyArr);
//组合完整消息体(包长度+协议号+信息内容+信息序列号)
//包长度字节+协议号+消息内容+消息序列号(等同于消息长度-错误校验+包长度的字节)
int msgLen = dataLen + length - 2;
ByteBuf frame = ByteBufAllocator.DEFAULT.heapBuffer(msgLen);
if (length == 1) {
//包长度
frame.writeByte(dataLen);
} else {
frame.writeShort(dataLen);
}
//协议号
frame.writeByte(msgId);
//剩余消息体(消息内容+序列号)
frame.writeBytes(remainingMsgBodyArr);
concoxMessage.setMsgBodyArr(remainingMsgBodyArr);
concoxMessage.setMsgBody(frame);
}
//读取校验
in.readShort();
//读取包尾
in.readShort();
// 回收已读字节
in.discardReadBytes();
return concoxMessage;
}
因为包头是两个字节,所以我们先找到设备传输过来的原始数据的包头,不过concox包头有两种分别是0x78 0x78,0x79 0x79,所以我们这里找两个连续字节分别是0x78 0x78或者0x79 0x79,循环查找,直到找到包头为止。
找的过程中需要根据剩余的字节数去判断是否小于数据的基本长度,如果小于基本长度则重置读指针然后等待下个包的到来,不然数据是不完整的,即便进入后续的解析也是有问题的。
//可读长度不能小于基本长度
if (in.readableBytes() < ConcoxConstant.MSG_BASE_LENGTH) {
in.resetReaderIndex();
return null;
}
如果找到包头后,则读取协议号,因为concox的协议号是一个字节,我们这时候转成int的时候需要进行与运算,不过也可以将协议号定义成byte,这样就不需要进行与运算了。
这里说一下为什么要进行与运算:
在做byte -> int类型转换时,JVM会做一个补位处理,
(注:补位是补1还是补0,取决于byte的最高位是1还是0)
以协议中出现的0x8A的协议号为例,转成二进制为:10001010,
如果直接赋值int值后是其实在计算机存储的是11111111 11111111 11111111 10001010(32位),
这个时候其实与最初的0x8A已经完全不等了,所以我们需要对其进行与0xFF做与运算,
可以将高24位置为0,低8位保持不变,这样做就可以保证和二进制补码的一致了。
因此我们在做协议解析的时候一定要注意将readByte()得到的数值与0xFF做一次与计算!
在登录过后设备需要进行回复确认,按照协议进行组包:
|
长度 |
描述 |
起始位 |
2 |
0x78 0x78 |
包长 |
1 |
长度 = 协议号 + 信息内容 +信息序列号 + 错误校验 |
协议号 |
1 |
0x01 |
信息序列号 |
2 |
从开机后,每次发送数据序列号都自动加1 |
错误校验 |
2 |
“包长度”到“信息序列号”的CRC-ITU值。接收方若收到的信息计算有CRC错误,则忽略,抛弃这个数据包(算法详见附件1) |
停止位 |
2 |
固定值: 0x0D 0x0A |
Example:78 78 05 01 00 05 9F F8 0D 0A
public static void replyMessage(ChannelHandlerContext ctx,int msgId,int index) {
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
//包长度
byteBuf.writeByte(5);
//协议号
byteBuf.writeByte(msgId);
//信息序列号
byteBuf.writeShort(index);
//读取消息体
byte[] bodyArr = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bodyArr);
//获取crc
int crc=(CommonUtil.GetCrc16(bodyArr, bodyArr.length));
//写入包头
byteBuf.writeByte(0x78);
byteBuf.writeByte(0x78);
//写入包长度
byteBuf.writeByte(5);
//协议号
byteBuf.writeByte(msgId);
//信息序列号
byteBuf.writeShort(index);
//错误校验
byteBuf.writeShort(crc);
byteBuf.writeByte(0x0D);
byteBuf.writeByte(0x0A);
log.info("回复确认包:{}", ByteBufUtil.hexDump(byteBuf));
ctx.writeAndFlush(byteBuf);
}
这个时候设备就可以完成正常登录了,后面的解析可参考以上解析思路进行,下面附上协议中错误校验(CRC-ITU)的方法实现
public static char GetCrc16(byte[] pData, int nLength)
{
int i=0;
// 初始化
char fcs = 0xffff;
while(nLength>0){
fcs = (char) ((fcs >> 8) ^ crctab16[(fcs ^ pData[i]) & 0xff]);
nLength--;
i++;
}
// 取反
return (char) ~fcs;
}
做物联网的朋友可以一起交流学习哦!QQ:571521973
来源:oschina
链接:https://my.oschina.net/u/4363105/blog/4482530