使用Java Netty做Concox协议解析

丶灬走出姿态 提交于 2020-08-10 16:40:55

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!