物联网设备,采用tcp通讯,基于netty框架进行十六进制报文封装

  • 一、背景
  • 二、步骤
  • 1.引入Netty架包
  • 2.消息内容实体类
  • 2.1 ProtocolBody
  • 2.2 MsgTypeHandleBean
  • 2.3 ProtocolField
  • 2.4 Other
  • 3.消息解析类
  • 3.1 BytesToJsonParse
  • 3.2 JsonToBytesParse
  • 3.3 IMessageParse
  • 3.4 BaseByteTransferHandler
  • 3.5 ICheckTypeParse
  • 4.终端设备与平台交互
  • 4.1 SendMsgServer
  • 4.2 SendMessageTask
  • 4.3 PackageManager
  • 4.4 WaitObject
  • 5. Netty相关类
  • 5.1 NettyRunner
  • 5.2 MyNettyServerBootstrap
  • 5.3 MessageCodec
  • 5.4 ServerHandler
  • 5.5 NamedChannelGroup
  • 三、完结


一、背景

由于近几年从事的工作都是物联网相关的,常常需要跟设备打交道。常见的设备通讯协议有Http(基于第三方平台:例电信平台)、Mqtt(基于边缘网关)、TCP(设备直连)、SDK(设备直连)等。除TCP协议外,其它通讯协议下发或上报大多数是采用JSON格式数据,而基于TCP通讯协议的设备往往需要根据设备厂商制定的协议来解析。这类协议往往有个特点,大体结构如下图,位置可能会有变化。
固定字段:无论哪种类型的消息,都会包含这些字段。
灵活字段:针对不同类型的消息,会有不同的字段。

开始字

…固定字段 …

消息类型

数据长度

…灵活字段…

校验字

结束字

那么接下来开始封装协议了,只是提供一个思路,希望对第一次接触的你会有所帮助。如有错误,请批评指教,谢谢。

二、步骤

1.引入Netty架包

<dependency>
   <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.22.Final</version>
</dependency>

2.消息内容实体类

2.1 ProtocolBody

消息包体,用来定义消息整体结构。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProtocolBody {
    /**
     * 模式:小端模式/大端模式
     */
    private ByteOrder byteOrder;
    /**
     *开始字或结束字集合 会判断是否匹配
     */
    private List<ProtocolField> startOrEndList;
    /**
     * 校验字 用来校验数据的合法性
     */
    private ProtocolField checkWord;
    /**
     * 校验类型beanId 常见的有和校验/异或校验,默认为异或校验 可根据需要补充其它类型
     */
    private String checkTypeBeanId;
    /**
     * 不校验下标集合 负数代表从末尾开始计数 一般校验内容排除开始、结束、校验字
     */
    private int[] noCheckIndex;

    /**
     * 数据长度【可变部分+调整长度】可能存在多个字段表示数据长度
     */
    private List<ProtocolField> dataLenList;

    /**
     * 调整长度
     */
    private int adjustmentLen;

    /**
     * 固定长度
     */
    private int fixLen;

    /**
     * 固定部分报文数据内容
     */
    private List<ProtocolField> fixDataList;

    /**
     *可变部分报文数据【针对不同的消息类型做不同的处理】
     */
    private Map<Object, MsgTypeHandleBean> flexibleDataMap;

}

2.2 MsgTypeHandleBean

根据不同的消息类型做不同的处理,例如设备主动上报故障,需往数据库添加一条故障数据,那么我们可以在IMsgParseAfterHandler 实现类完成相关的逻辑处理。同一种消息类型,下发或者上报的字段也不一样。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MsgTypeHandleBean {
    private String beanId;//解析完毕后处理数据类
    private List<ProtocolField> sendProtocolFieldList;//下发字段集合
    private List<ProtocolField> reportProtocolFieldList;//上报字段集合
}
public interface IMsgParseAfterHandler {
    public void handleData(JSONObject jsonObject);
}

2.3 ProtocolField

字段定义,定义每个字段相关属性。

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProtocolField {
    /**
     * 字段类型 参考枚举类FieldTypeEnum 可根据需要补充
     */
    private FieldTypeEnum fieldType;
    /**
     * 字段描述
     */
    private String fieldName;
    /**
     * 字段对应实体类属性
     */
    private String fieldKey;
    /**
     * 开始下标(相对) 负数代表从末尾开始计数
     */
    private int offset;
    /**
     *数据长度
     */
    private int len;

    /**
     * 当前值
     */
    private Object curValue;

    /**
     * 期望值 1)开始字、结束字、校验字会校验是否合法 2)数据上报类型是否匹配
     */
    private List expectValueList;
    /**
     * 常见协议上报多条数据时前面会有1-2字节表示此次上报的数据条数 遍历的数据集合
     */
    private List<ProtocolField> childFieldList;

    private String childFieldKey;


    /**
     *可变部分报文数据【针对不同的值做不同的处理】 例如数据上报类型/故障
     */
    private Map<Object, List<ProtocolField>> protocolFieldMap;


}

2.4 Other

字段类型转换处理类,只列举部分,可根据需要自行添加。

public enum FieldTypeEnum {
   
    BYTE_ZERO("byteZeroLittleTransferHandler","获取byte数组第一个字节"),
    SHORT_LITTLE("byteShortLittleTransferHandler", "short与byte互转(小端)"),
    INT_LITTLE("byteIntLittleTransferHandler", "int与byte互转(小端)"),
    LONG_LITTLE("byteLongLittleTransferHandler", "long与byte互转(小端)"),
    FLOAT_LITTLE("byteFloatLittleTransferHandler", "float与byte互转(小端)"),
    DOUBLE_LITTLE("byteDoubleLittleTransferHandler", "double与byte互转(小端)"),
    HEX_LITTLE("byteHexLittleTransferHandler","十六进制与byte互转(小端)"),
    BIT_LITTLE("byteBitLittleTransferHandler","八位二进制与byte互转(小端)"),

            ;
    private String beanId;//处理类id
    private String desc;//描述

    FieldTypeEnum(String beanId, String desc) {
        this.beanId = beanId;
        this.desc = desc;
    }

    public static FieldTypeEnum getInstanceByType(String beanId){
        FieldTypeEnum[] values = values();
        for(FieldTypeEnum val : values){
            if(val.getBeanId().equals(beanId)){
                return val;
            }
        }
        return null;
    }


    public String getBeanId() {
        return beanId;
    }
}

错误码

public enum ErrorEnum {
    OK(0,"解析成功"),
    FIELD_TYPE_NOT_FOUND(101,"未知字段类型"),
    FILE_TYPE_CONFIG_ERROR(102,"字段类型配置有误"),
    FILE_VALUE_NOT_MATCH(103, "字段值与期望值不匹配"),
    CHECK_WORD_NOT_MATCH(104, "校验字不匹配"),
    BYTE_LEN_NOT_ENOUGH(105,"字节数不够"),
    BYTE_LEN_NOT_EQUAL(106,"字节数不相等"),
    REPORT_TYPE_NOT_FOUND(107,"未知上报消息类型"),
    UNKNOWN_ERROR(108,"未知异常"),
    CHECK_TYPE_PARSE_BEAN_NOT_FOUND(109, "未知校验类型解析beanId"),
    REPORT_TYPE_PARSE_BEAN_NOT_FOUND(110, "未知上报类型解析beanId"),
    REPORT_TYPE_HANDLE_BEAN_NOT_FOUND(111, "未知上报类型处理beanId"),


    ;

    private int code;
    private String msg;


    ErrorEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

自定义协议解析异常类

@Data
public class ProtocolParseException extends RuntimeException{


    private int code;
    private String msg;
    private Object data;
    public ProtocolParseException(ErrorEnum errorEnum){
        this.code = errorEnum.getCode();
        this.msg = errorEnum.getMsg();
    }

    public ProtocolParseException(ErrorEnum errorEnum, Object data){
        this.code = errorEnum.getCode();
        this.msg = errorEnum.getMsg();
        this.data = data;
    }

}

返回结果

@Data
public class ParseResult<T> {
    private int code;
    private String msg;
    private T data;

    public static <T> ParseResult<T>  ok(T t){
        ParseResult<T> result = new ParseResult<>();
        result.setCode(ErrorEnum.OK.getCode());
        result.setMsg(ErrorEnum.OK.getMsg());
        result.setData(t);
        return result;
    }

    public static <T> ParseResult<T> error(ProtocolParseException exception) {
        ParseResult<T> result = new ParseResult<>();
        result.setCode(exception.getCode());
        result.setMsg(exception.getMsg());
        return result;
    }

    public static <T> ParseResult<T> error(ErrorEnum errorEnum) {
        ParseResult<T> result = new ParseResult<>();
        result.setCode(errorEnum.getCode());
        result.setMsg(errorEnum.getMsg());
        return result;
    }
}
@Data
public class MessageBean {
    private JSONObject jsonObject;

    public MessageBean(){

    }

    public MessageBean(JSONObject jsonObject){
        this.jsonObject = jsonObject;
    }
}

3.消息解析类

3.1 BytesToJsonParse

上报报文处理类,byte数组转json数据解析。

@Slf4j
public class BytesToJsonParse {

    /**
     * 将bytes数组转为json数据
     * @param protocolBody
     * @param bytes
     * @return
     */
    public static ParseResult<JSONObject> bytesToJson(ProtocolBody protocolBody, byte[] bytes){
        try {
            JSONObject jsonObject = new JSONObject();
            //校验数据
            validateData(jsonObject, protocolBody, bytes);
            //固定数据域
            bytesToJson(jsonObject, bytes, protocolBody.getFixDataList());
            //获取上报类型+可变部分数据域
            IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
            if(messageParse == null){
                throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
            }
            Object reportType = messageParse.getReportType(jsonObject);
            Map<Object, MsgTypeHandleBean> flexibleDataMap = protocolBody.getFlexibleDataMap();
            if(flexibleDataMap == null || !flexibleDataMap.containsKey(reportType)){
                throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_NOT_FOUND, reportType);
            }
            MsgTypeHandleBean msgTypeHandleBean = flexibleDataMap.get(reportType);
            bytesToJson(jsonObject, bytes, msgTypeHandleBean.getReportProtocolFieldList());
            if(!StringUtils.isEmpty(msgTypeHandleBean.getBeanId())){
                IMsgParseAfterHandler afterHandler = SpringContextUtils.getBean(msgTypeHandleBean.getBeanId(), IMsgParseAfterHandler.class);
                if(afterHandler == null){
                    throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_HANDLE_BEAN_NOT_FOUND, msgTypeHandleBean.getBeanId());
                }
                afterHandler.handleData(jsonObject);
            }
            return ParseResult.ok(jsonObject);
        }catch (ProtocolParseException e){

            log.error("协议解析出错:data:{},e:{}",e.getData(), e);
            return ParseResult.error(e);
        } catch (Exception e){
            log.error("协议解析出错:{}",e);
            return ParseResult.error(ErrorEnum.UNKNOWN_ERROR);
        }

    }

    private static void bytesToJson(JSONObject jsonObject, byte[] bytes, List<ProtocolField> fieldList){
        if(fieldList != null){
            for(int i = 0; i < fieldList.size(); i++){
                ProtocolField protocolField = fieldList.get(i);
                bytesToJson(jsonObject, bytes, protocolField);
            }
        }
    }

    private static void bytesToJson(JSONObject jsonObject, byte[] bytes, ProtocolField protocolField){
        if(protocolField != null){
            Object curVal = bytesToObj(bytes, protocolField);
            boolean flag = judgeIsEqual(curVal, protocolField.getExpectValueList());
            if(!flag){
                throw new ProtocolParseException(ErrorEnum.FILE_VALUE_NOT_MATCH, protocolField.getFieldKey());
            }
            if(jsonObject != null){
                jsonObject.put(protocolField.getFieldKey(), curVal);
                if(protocolField.getChildFieldList() != null && !StringUtils.isEmpty(protocolField.getChildFieldKey())){
                    JSONArray jsonArray = new JSONArray();
                    int num = Integer.parseInt(String.valueOf(curVal));
                    int index = protocolField.getOffset() + protocolField.getLen();
                    for(int i = 0; i < num; i++){
                        JSONObject json = new JSONObject();
                        int len = 0;
                        for(int j= 0; j < protocolField.getChildFieldList().size(); j++){
                            ProtocolField childField = protocolField.getChildFieldList().get(j);
                            int offset = childField.getOffset() ;
                            bytesToJson(json, bytes, CommonUtils.copyData(childField, offset + index));
                            len += childField.getLen();
                        }
                        index += len;
                        jsonArray.add(json);
                    }
                    jsonObject.put(protocolField.getChildFieldKey(),jsonArray);
                }
                if(protocolField.getProtocolFieldMap() != null && protocolField.getProtocolFieldMap().containsKey(curVal)){
                    List<ProtocolField> protocolFieldList = protocolField.getProtocolFieldMap().get(curVal);
                    bytesToJson(jsonObject, bytes, protocolFieldList);
                }
            }

        }
    }

    /**
     * 校验数据的合法性
     * @param jsonObject
     * @param protocolBody
     * @param bytes
     */
    private static void validateData(JSONObject jsonObject, ProtocolBody protocolBody, byte[] bytes){
        if(bytes == null || bytes.length < protocolBody.getFixLen()){
            throw new ProtocolParseException(ErrorEnum.BYTE_LEN_NOT_ENOUGH, bytes == null ? 0 : bytes.length);
        }
        //开始字和结束字匹配
        bytesToJson(jsonObject, bytes, protocolBody.getStartOrEndList());
        if (protocolBody.getCheckWord() != null){
            String beanId = null;
            if(StringUtils.isEmpty(protocolBody.getCheckTypeBeanId())){
                beanId = CommonConst.SUM_CHECK_TYPE_PARSE_BEAN ;
            }else{
                beanId = protocolBody.getCheckTypeBeanId();
            }
            ICheckTypeParse checkTypeParse = SpringContextUtils.getBean(beanId, ICheckTypeParse.class);
            if(checkTypeParse == null){
                throw new ProtocolParseException(ErrorEnum.CHECK_TYPE_PARSE_BEAN_NOT_FOUND, beanId);
            }
            byte calCrc = checkTypeParse.getCheckWorkResult(protocolBody.getNoCheckIndex(), bytes);
            byte curCrc = (byte) bytesToObj(bytes, protocolBody.getCheckWord());
            if(calCrc != curCrc){
                throw new ProtocolParseException(ErrorEnum.CHECK_WORD_NOT_MATCH, new byte[]{calCrc, curCrc});
            }
            jsonObject.put(protocolBody.getCheckWord().getFieldKey(), curCrc);

        }
    }

    /**
     * 判断当前值是否与期望值相等
     * @param curVal
     * @param expectValueList
     * @return
     */
    private static boolean judgeIsEqual(Object curVal, List expectValueList){
        if(expectValueList != null && expectValueList.size() > 0){
            for(int i = 0; i < expectValueList.size(); i++){
                if(curVal == expectValueList.get(i) || String.valueOf(curVal).equals(String.valueOf(expectValueList.get(i)))){
                    return true;
                }
            }
            return  false;
        }
        return true;
    }

    /**
     * 将byte数组转为其他数据类型
     * @param bytes
     * @param protocolField
     * @return
     */
    private static Object bytesToObj(byte[] bytes, ProtocolField protocolField){
        FieldTypeEnum instance =  protocolField.getFieldType();
        if(instance == null){
            throw new ProtocolParseException(ErrorEnum.FIELD_TYPE_NOT_FOUND, protocolField.getFieldKey());
        }
        try {
            String beanId = instance.getBeanId();
            BaseByteTransferHandler handler = SpringContextUtils.getBean(beanId, BaseByteTransferHandler.class);
            int offset = protocolField.getOffset() >= 0 ? protocolField.getOffset() : bytes.length + protocolField.getOffset();
            Object obj = handler.bytesToObj(bytes, offset, protocolField.getLen());
            return obj;
        }catch (Exception e){
            throw new ProtocolParseException(ErrorEnum.UNKNOWN_ERROR, protocolField.getFieldKey());
        }

    }



}

3.2 JsonToBytesParse

下发报文处理类,json转byte数组解析。

@Slf4j
public class JsonToBytesParse {
    /**
     * 将json数据转为bytes数组
     * @param protocolBody
     * @param jsonObject
     * @return
     */
    public static ParseResult<byte[]> jsonToBytes(ProtocolBody protocolBody, JSONObject jsonObject){
        try {
            //获取数据域长度
            List<ProtocolField> sendProtocolFieldList = new ArrayList<>();
            int dataLen = getFlexibleDataLen(protocolBody, jsonObject, sendProtocolFieldList);
            if(protocolBody.getDataLenList() != null && protocolBody.getDataLenList().size() > 0){
                String fieldKey = protocolBody.getDataLenList().get(0).getFieldKey();
                jsonObject.put(fieldKey, dataLen + protocolBody.getAdjustmentLen());
            }
            //初始化bytes数组
            byte[] bytes = new byte[protocolBody.getFixLen() + dataLen];
            //设置开始字和结束字
            jsonToBytes(jsonObject, bytes, protocolBody.getStartOrEndList());
            //设置固定部分报文数据
            jsonToBytes(jsonObject, bytes, protocolBody.getFixDataList());
            //设置数据长度
            jsonToBytes(jsonObject, bytes, protocolBody.getDataLenList());
            //设置数据内容
            jsonToBytes(jsonObject, bytes, sendProtocolFieldList);
            //校验字
            setCheckWord(bytes, protocolBody.getCheckWord(), protocolBody.getNoCheckIndex(), protocolBody.getCheckTypeBeanId());
            return ParseResult.ok(bytes);
        }catch (ProtocolParseException e){
            log.error("协议解析出错:{}",e);
            return ParseResult.error(e);
        } catch (Exception e){
            log.error("协议解析出错:{}",e);
            return ParseResult.error(ErrorEnum.UNKNOWN_ERROR);
        }
    }

    private static void setCheckWord(byte[] bytes, ProtocolField checkWord, int[] noCheckIndex, String checkTypeBeanId) {
        if (checkWord != null){
            String beanId = checkTypeBeanId;
            if(StringUtils.isEmpty(checkTypeBeanId)){
                beanId = CommonConst.SUM_CHECK_TYPE_PARSE_BEAN ;
            }
            ICheckTypeParse checkTypeParse = SpringContextUtils.getBean(beanId, ICheckTypeParse.class);
            if(checkTypeParse == null){
                throw new ProtocolParseException(ErrorEnum.CHECK_TYPE_PARSE_BEAN_NOT_FOUND, beanId);
            }
            byte calCrc = checkTypeParse.getCheckWorkResult(noCheckIndex, bytes);
            int offset = checkWord.getOffset() >= 0 ? checkWord.getOffset() : bytes.length + checkWord.getOffset();
            bytes[offset] = calCrc;
        }
    }

    /**
     * 获取数据域长度
     * @param protocolBody
     * @param jsonObject
     * @param sendProtocolFieldList
     * @return
     */
    private static int getFlexibleDataLen(ProtocolBody protocolBody, JSONObject jsonObject, List<ProtocolField> sendProtocolFieldList){
        //获取数据类型
        IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
        if(messageParse == null){
            throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
        }
        Object reportType = messageParse.getReportType(jsonObject);
        Map<Object, MsgTypeHandleBean> flexibleDataMap = protocolBody.getFlexibleDataMap();
        if(flexibleDataMap == null || !flexibleDataMap.containsKey(reportType)){
            throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_NOT_FOUND, reportType);
        }
        MsgTypeHandleBean msgTypeHandleBean = flexibleDataMap.get(reportType);
        List<ProtocolField> tempList = msgTypeHandleBean.getSendProtocolFieldList();
        if(tempList != null){
            tempList.forEach(obj -> {
                sendProtocolFieldList.add(obj);
            });
        }
        return getDataLen(sendProtocolFieldList,jsonObject);
    }

    private static int getDataLen(List<ProtocolField> protocolFieldList,JSONObject jsonObject){
        int len = 0;
        for(int i = 0; i < protocolFieldList.size(); i++){
            ProtocolField protocolField = protocolFieldList.get(i);
            len += getDataLen(protocolField, jsonObject);

        }
        return len;
    }


    private static int getDataLen(ProtocolField protocolField,JSONObject jsonObject){
        int len = protocolField.getLen();
        if(protocolField.getChildFieldList() != null && !StringUtils.isEmpty(protocolField.getChildFieldKey())){
            JSONArray jsonArray = jsonObject.getJSONArray(protocolField.getChildFieldKey());
            for(int i = 0; i < jsonArray.size(); i++){
                len += getDataLen(protocolField.getChildFieldList(),jsonArray.getJSONObject(i));
            }
        }
        Object fieldValue = getFieldValue(jsonObject, protocolField);
        Map<Object, List<ProtocolField>> protocolFieldMap = protocolField.getProtocolFieldMap();
        if(protocolFieldMap != null && protocolFieldMap.containsKey(fieldValue)){
            List<ProtocolField> typeProtocolFieldList = protocolFieldMap.get(fieldValue);
            len += getDataLen(typeProtocolFieldList,jsonObject);

        }
        return len;
    }



    private static void jsonToBytes(JSONObject jsonObject, byte[] bytes, List<ProtocolField> fieldList){
        if(fieldList != null){
            for(int i = 0; i < fieldList.size(); i++){
                ProtocolField protocolField = fieldList.get(i);
                jsonToBytes(jsonObject, bytes, protocolField);
            }
        }

    }
    private static void jsonToBytes(JSONObject jsonObject, byte[] bytes, ProtocolField protocolField){
        if(protocolField != null){
            objToBytes(jsonObject, bytes, protocolField);
            if(protocolField.getChildFieldList() != null && !StringUtils.isEmpty(protocolField.getChildFieldKey())){
                JSONArray jsonArray = jsonObject.getJSONArray(protocolField.getChildFieldKey());
                int index = protocolField.getOffset() + protocolField.getLen();
                for(int i = 0; i < jsonArray.size(); i++){
                    JSONObject json = jsonArray.getJSONObject(i);
                    int len = 0;
                    for(int j= 0; j < protocolField.getChildFieldList().size(); j++){
                        ProtocolField childField = protocolField.getChildFieldList().get(j);
                        int offset = childField.getOffset();
                        jsonToBytes(json, bytes, CommonUtils.copyData(childField,offset + index));
                        len += childField.getLen();
                    }
                    index += len;
                }
            }
            if(protocolField.getProtocolFieldMap() != null && protocolField.getProtocolFieldMap().containsKey(jsonObject.get(protocolField.getFieldKey()))){
                List<ProtocolField> protocolFieldList = protocolField.getProtocolFieldMap().get(jsonObject.get(protocolField.getFieldKey()));
                jsonToBytes(jsonObject, bytes, protocolFieldList);
            }

        }
    }

    /**
     * 将其他数据类型值转为byte数组
     * @param jsonObject
     * @param bytes
     * @param protocolField
     */
    private static Object objToBytes(JSONObject jsonObject, byte[] bytes, ProtocolField protocolField){
        FieldTypeEnum instance =  protocolField.getFieldType();
        if(instance == null){
            throw new ProtocolParseException(ErrorEnum.FIELD_TYPE_NOT_FOUND, protocolField.getFieldKey());
        }
        try {
            String beanId = instance.getBeanId();
            BaseByteTransferHandler handler = SpringContextUtils.getBean(beanId, BaseByteTransferHandler.class);
            Object obj = handler.getValueByKey(jsonObject, protocolField.getFieldKey());
            if(bytes != null){
                int offset = protocolField.getOffset() >= 0 ? protocolField.getOffset() : bytes.length + protocolField.getOffset();
                handler.objToBytes(bytes, offset, protocolField.getLen(), obj);
            }
            return obj;
        }catch (Exception e){
            throw new ProtocolParseException(ErrorEnum.UNKNOWN_ERROR, protocolField.getFieldKey());
        }

    }

    private static Object getFieldValue(JSONObject jsonObject, ProtocolField protocolField){
        return objToBytes(jsonObject, null, protocolField);

    }

}

3.3 IMessageParse

每种设备协议消息标识、序列号、消息类型、消息体都不一样,那么这里只写了一个接口,引入架包后必须实现这个接口的方法。

public interface IMessageParse {
    /**
     * 获取上传类型
     * @param jsonObject
     * @return
     */
    public Object getReportType(JSONObject jsonObject);

    /**
     * 获取消息标识(例如:设备唯一标识)
     * @param jsonObject
     * @return
     */
    public Object getMessageKey(JSONObject jsonObject);

    /**
     * 获取消息序列号
     * @param jsonObject
     * @return
     */
    public Object getMessageSeq(JSONObject jsonObject);

    /**
     * 获取消息体内容
     * @return
     */
    public ProtocolBody getProcolBody();

    /**
     * 获取应答消息
     * @param messageBean
     * @return
     */
    public MessageBean getAckMessageBean(MessageBean messageBean);

    /**
     * 更新状态
     * @param key  设备唯一标识
     * @param connectStatus 连接状态( 1 在线 ; 0 离线)
     */
    public void updateConnectStatus(Object key, int connectStatus);

}

3.4 BaseByteTransferHandler

针对枚举类FieldTypeEnum的beanId实现类。

public abstract class BaseByteTransferHandler<T> {

    /**
     * byte转为其他数据类型
     * @param bytes
     * @param offset
     * @param len
     * @return
     */
    public abstract T bytesToObj(byte[] bytes, int offset, int len);

    /**
     * 其他数据类型转为byte
     * @param bytes
     * @param offset
     * @param len
     * @param value
     */
    public abstract void objToBytes(byte[] bytes, int offset, int len, T value);


    /**
     * 从jsonobject获取值
     * @param jsonObject
     * @param field
     * @return
     */
    public abstract T getValueByKey(JSONObject jsonObject, String field);


    /**
     * 小端模式 获取ByteBuffer
     * @param bytes byte数组
     * @param offset 偏移量
     * @param curLen 当前长度
     * @param maxLen 最大长度
     * @return
     */
    public static ByteBuffer getByteBufferLittle(byte[] bytes, int offset, int curLen, int maxLen){
        ByteBuffer buffer = ByteBuffer.allocate(maxLen);
        for(int i = curLen; i < maxLen; i++){
            buffer.put((byte) 0x00);
        }
        for(int i = 0; i < curLen; i++){
            buffer.put(bytes[offset + curLen - i - 1]);
        }
        buffer.flip();
        return buffer;
    }

    /**
     * 小端模式 给byte数组赋值
     * @param array 大端模式下值对应的存储数组
     * @param bytes 赋值byte数组
     * @param offset 偏移量
     * @param curLen 当前长度
     * @param maxLen 最大长度
     */
    public static void setBytesLittle(byte[] array, byte[] bytes, int offset, int curLen, int maxLen){
        for(int i = 0; i < curLen; i++){
            bytes[i + offset] = array[maxLen - i- 1];
        }

    }

这里就不一一列举了,只列举int与byte类型小端模式转换。

@Component
public class ByteIntLittleTransferHandler extends BaseByteTransferHandler<Integer> {

    public static final int WORD = 4;


    @Override
    public Integer bytesToObj(byte[] bytes, int offset, int len) {
        ByteBuffer buffer = getByteBufferLittle(bytes, offset, len, WORD);
        return buffer.getInt();
    }

    @Override
    public void objToBytes(byte[] bytes, int offset, int len, Integer value) {
        ByteBuffer buffer = ByteBuffer.allocate(WORD);
        buffer.putInt(value);
        byte[] array = buffer.array();
        setBytesLittle(array, bytes, offset, len, WORD);
    }

    @Override
    public Integer getValueByKey(JSONObject jsonObject, String field) {
        return jsonObject.getInteger(field);
    }

}

3.5 ICheckTypeParse

校验字获取。

public interface ICheckTypeParse {
    /**
     * 获取校验字结果
     * @param noCheckIndex 不校验的下标
     * @param bytes byte数组
     * @return
     */
    public byte getCheckWorkResult(int[] noCheckIndex, byte[] bytes);

}

和校验

@Component
public class SumCheckTypeParse implements ICheckTypeParse {

    @Override
    public byte getCheckWorkResult(int[] noCheckIndex, byte[] bytes) {
        int len = bytes.length;
        boolean flag = false;
        byte b = 0;
        for(int i = 0; i < len; i++){
            if(!CommonUtils.isExist(noCheckIndex, i , len)){
                if(!flag){
                    b = bytes[i];
                }else{
                    b += bytes[i];
                }
                flag = true;
            }
        }
        return b;
    }
}

异或校验

@Component
public class XorCheckTypeParse implements ICheckTypeParse {

    @Override
    public byte getCheckWorkResult(int[] noCheckIndex, byte[] bytes) {
        int len = bytes.length;
        boolean flag = false;
        byte b = 0;
        for(int i = 0; i < len; i++){
            if(!CommonUtils.isExist(noCheckIndex, i , len)){
                if(!flag){
                    b = bytes[i];
                }else{
                    b ^= bytes[i];
                }
                flag = true;
            }
        }
        return b;
    }
}

4.终端设备与平台交互

4.1 SendMsgServer

下发命令

@Slf4j
@Component
public class SendMsgServer {

    @Value("${netty.server.delaySeconds:30}")
    private Integer delaySeconds;//超时时间

    @Value("${netty.server.targetTimes:3}")
    private Integer targetTimes;//重试次数

    public JSONObject sendTcp(JSONObject jsonObject) {
        SendMessageTask task = new SendMessageTask(jsonObject, delaySeconds, targetTimes);
        PackageManager.getInstance().addMessage(task);
        task.start();
        // 实现阻塞通信
        synchronized (task.getObject()) {
            try {
                task.getObject().wait();
            } catch (InterruptedException e) {
                log.error("sendTcp error!", e);
            }
        }
        MessageBean receiveBean = task.getObject().getReceiveBean();
        if(receiveBean == null){
            return null;
        }
        return receiveBean.getJsonObject();
    }

}

4.2 SendMessageTask

定时任务 超时重试机制

@Data
public class SendMessageTask extends TimerTask {


    private final Timer timer;
    /**
     * 同步对象
     */
    private final WaitObject object;
    /**
     * 超时时间
     */
    private int delaySeconds;
    /**
     * 消息唯一标识序列号
     */
    private Object seq;
    /**
     * 当前超时次数.
     */
    private int curTimes;
    /**
     * 超时总次数
     */
    private int targetTimes;

    public SendMessageTask(JSONObject jsonObject, int delaySeconds, int targetTimes) {
        super();
        this.delaySeconds = delaySeconds;
        this.targetTimes = targetTimes;
        this.timer = new Timer();
        this.seq = getSeq(jsonObject);
        this.curTimes = 0;
        this.object = new WaitObject();
        this.object.setSendBean(new MessageBean(jsonObject));
    }


    public void start() {
        synchronized (timer) {
            timer.schedule(this, 0, delaySeconds * 1000);
        }
    }


    @Override
    public void run() {
        if (curTimes < targetTimes && NamedChannelGroup.getInstance().sendMessageBySeq(object.getSendBean())) {
            curTimes = curTimes + 1;
        } else {
            PackageManager.getInstance().removeMessage(seq);
            synchronized (timer) {
                this.cancel();
                timer.cancel();
            }
            synchronized (object) {
                object.notifyAll();
            }
        }
    }


    private Object getSeq(JSONObject jsonObject) {
        IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
        if(messageParse == null){
            throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
        }
        return messageParse.getMessageSeq(jsonObject);
    }
}

4.3 PackageManager

记录每次发送的消息,等待终端应答返回结果。

public class PackageManager {

  /**
   * 已发送未收到回应的消息.
   */
  private ConcurrentMap<Object, SendMessageTask> messages;

  private PackageManager() {
    messages = new ConcurrentHashMap<>();

  }


  public static PackageManager getInstance() {
    return SingletonHolder.INSTANCE;
  }

  public Object addMessage(SendMessageTask message) {
    Object seq = message.getSeq();
    if (messages.containsKey(seq)) {
      return null;
    }
    messages.put(seq, message);
    return seq;
  }

  public SendMessageTask removeMessage(Object seq) {
    return messages.remove(seq);
  }


  private static class SingletonHolder {

    private static final PackageManager INSTANCE = new PackageManager();
  }

}

4.4 WaitObject

@Data
public class WaitObject {

  private MessageBean sendBean;
  private MessageBean receiveBean;
}
@Data
public class MessageBean {
    private JSONObject jsonObject;

    public MessageBean(){

    }

    public MessageBean(JSONObject jsonObject){
        this.jsonObject = jsonObject;
    }
}

5. Netty相关类

5.1 NettyRunner

@Slf4j
@Component
public class NettyRunner {

  @Autowired
  MyNettyServerBootstrap nettyServerBootstrap;

  /**
   * .
   */
  @PostConstruct
  public void runNettyServer() {
    log.info("run netty server");
    try {
      nettyServerBootstrap.startNettyServer();
    } catch (InterruptedException e) {
      log.error(e.getMessage());
      Thread.currentThread().interrupt();
    }
  }

  /**
   * .
   */
  @PreDestroy
  public void shutdownNettyServer() {
    log.info("shutdown netty server");
    try {
      nettyServerBootstrap.shutdownNettyServer();
    } catch (InterruptedException e) {
      log.error(e.getMessage());
      Thread.currentThread().interrupt();
    }
  }

}

5.2 MyNettyServerBootstrap

@Component
public class MyNettyServerBootstrap {

    @Value("${netty.server.port:9016}")
    private Integer port;//Netty服务端端口号

    @Value("${netty.server.readerIdleTime:300}")
    private Integer readerIdleTime;//读取超时时间(秒)

    @Value("${netty.server.byteOrderLittle:true}")
    private Boolean byteOrderLittle;//true小端模式 false大端模式

    @Value("${netty.server.maxFrameLength:65536}")
    private Integer maxFrameLength;//报文最大长度

    @Value("${netty.server.lengthFieldOffset:0}")
    private Integer lengthFieldOffset;//长度字段偏移量

    @Value("${netty.server.lengthFieldLength:1}")
    private Integer lengthFieldLength;//长度字段长度

    @Value("${netty.server.lengthAdjustment:0}")
    private Integer lengthAdjustment;//可调整长度

    @Value("${netty.server.initialBytesToStrip:0}")
    private Integer initialBytesToStrip;//初始化长度

    private ServerBootstrap serverBootstrap;

    private Channel serverChannel;

    private EventLoopGroup bossEventLoopGroup;

    private EventLoopGroup workerEventLoopGroup;

    public void startNettyServer() throws InterruptedException {
        serverBootstrap = new ServerBootstrap();
        bossEventLoopGroup = new NioEventLoopGroup();
        workerEventLoopGroup = new NioEventLoopGroup();
        serverBootstrap.group(bossEventLoopGroup,workerEventLoopGroup);
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
        serverBootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
        serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
        serverBootstrap.childOption(ChannelOption.SO_LINGER, 0);
        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast("codec", new MessageCodec());
                if(readerIdleTime > 0){
                    pipeline.addLast("heartbeat", new IdleStateHandler(readerIdleTime, 0, 0, TimeUnit.SECONDS));
                }
                ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
                if(!byteOrderLittle){
                    byteOrder = ByteOrder.BIG_ENDIAN;
                }
                pipeline.addLast("baseFrameDecoder",new LengthFieldBasedFrameDecoder(byteOrder, maxFrameLength, lengthFieldOffset,
                        lengthFieldLength,lengthAdjustment, initialBytesToStrip, true));
                pipeline.addLast("handler", new ServerHandler());
            }
        });
        ChannelFuture future = this.serverBootstrap.bind(this.port).sync();
        this.serverChannel = future.channel();
    }

    public void shutdownNettyServer() throws InterruptedException {
        serverChannel.close().sync();
        bossEventLoopGroup.shutdownGracefully().await();
        workerEventLoopGroup.shutdownGracefully().await();
    }

5.3 MessageCodec

@Slf4j
public class MessageCodec extends ByteToMessageCodec<MessageBean> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MessageBean messageBean, ByteBuf byteBuf) throws Exception {
        try {
            IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
            if(messageParse == null){
                throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
            }
            ParseResult<byte[]> parseResult = JsonToBytesParse.jsonToBytes(messageParse.getProcolBody(), messageBean.getJsonObject());
            if(parseResult.getCode() == ErrorEnum.OK.getCode()){
                byteBuf.writeBytes(parseResult.getData());
            }
        }catch (Exception e){
            log.error("协议编码出错:{}",e);
        }
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
        try {
            IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
            if(messageParse == null){
                throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
            }
            int num = byteBuf.readableBytes();
            byte[] bytes = new byte[num];
            byteBuf.readBytes(bytes);
            ParseResult<JSONObject> parseResult = BytesToJsonParse.bytesToJson(messageParse.getProcolBody(), bytes);
            if(parseResult.getCode() == ErrorEnum.OK.getCode()){
                MessageBean messageBean = new MessageBean();
                messageBean.setJsonObject(parseResult.getData());
                list.add(messageBean);
            }
        }catch (Exception e){
            log.error("协议解码出错:{}",e);

        }

    }


}

5.4 ServerHandler

@Slf4j
public class ServerHandler extends SimpleChannelInboundHandler<MessageBean> {


  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    log.info("新的连接:{}", ctx.channel().remoteAddress());
    super.channelActive(ctx);
    NamedChannelGroup.getInstance().channelActive(ctx.channel());
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    log.error("异常连接:{},错误信息:{}", ctx.channel().remoteAddress(), cause);
    ctx.close();
  }

  @Override
  public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    log.info("断开的连接:{}", ctx.channel().remoteAddress());
    super.channelInactive(ctx);
    NamedChannelGroup.getInstance().channelInActive(ctx.channel());

  }

  @Override
  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    // 获取idle事件
    if (evt instanceof IdleStateEvent) {
      IdleStateEvent event = (IdleStateEvent) evt;
      // 读等待事件
      if (event.state() == IdleState.READER_IDLE) {
          if(ctx.channel().isActive() || ctx.channel().isOpen()) {
            ctx.channel().close();
        }
      }
    } else {
      super.userEventTriggered(ctx, evt);
    }
  }

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, MessageBean messageBean) throws Exception {
    try {
      NamedChannelGroup.getInstance().addConnectedChannel(ctx.channel(), messageBean);
      IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
      if(messageParse == null){
        throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
      }
      //设备主动上报消息 一般需要应答
      MessageBean ackMessageBean = messageParse.getAckMessageBean(messageBean);
      if(ackMessageBean != null){
        ctx.writeAndFlush(ackMessageBean);
        return ;
      }
      //下发命令 回复结果
      Object seq = messageParse.getMessageSeq(messageBean.getJsonObject());
      SendMessageTask messageTask = PackageManager.getInstance().removeMessage(seq);
      if(messageTask != null) {
        synchronized (messageTask.getTimer()) {
          messageTask.getTimer().cancel();
          messageTask.cancel();
        }
        synchronized (messageTask.getObject()) {
          messageTask.getObject().setReceiveBean(messageBean);
          messageTask.getObject().notifyAll();
        }
      }
    }catch (Exception e){
      log.error("读取消息失败:{}", e);

    }

  }



}

5.5 NamedChannelGroup

@Slf4j
public class NamedChannelGroup {

    /*
     * 已经连接的channel与ip端口的映射map key:address value:channelId
     */
    private ConcurrentMap<Object, String> keyChannelIdConcurrentMap;


    private  ConcurrentMap<String, Channel> channelConcurrentMap;

    public static final String CHANNEL_ID_KEY = "channelId";


    private NamedChannelGroup() {
        keyChannelIdConcurrentMap = new ConcurrentHashMap<>();
        channelConcurrentMap = new ConcurrentHashMap<>();
    }

   
    public void channelActive(Channel channel) {
        String channelId = UUID.randomUUID().toString();
        channel.attr(AttributeKey.valueOf(CHANNEL_ID_KEY)).set(channelId);
        channelConcurrentMap.put(channelId, channel);
    }

    public void channelInActive(Channel channel) {
        String channelId = String.valueOf(channel.attr(AttributeKey.valueOf(CHANNEL_ID_KEY)).get());
        if (channel.isActive() || channel.isOpen()) {
            channel.close();
        }
        if(channelConcurrentMap.containsKey(channelId)){
            channelConcurrentMap.remove(channelId);
            removeConnectedChannel(channelId);
        }

    }


    public void addConnectedChannel(Channel channel, MessageBean messageBean) {
        IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
        if(messageParse == null){
            throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
        }
        Object key = messageParse.getMessageKey(messageBean.getJsonObject());
        if(!keyChannelIdConcurrentMap.containsKey(key)){
            Object channelId = channel.attr(AttributeKey.valueOf(CHANNEL_ID_KEY)).get();
            log.info("add key:{},channelId:{},address:{}",key,channelId,channel.remoteAddress());
            keyChannelIdConcurrentMap.put(key, String.valueOf(channelId));
            messageParse.updateConnectStatus(key, CommonConst.CONNECT_ONLINE);
        }
    }

    public void removeConnectedChannel(String channelId) {
        Set keySet = keyChannelIdConcurrentMap.keySet();
        for(Object key : keySet){
            if(keyChannelIdConcurrentMap.get(key).equals(channelId)){
                log.info("remove key:{},channelId:{}",key,channelId);
                keyChannelIdConcurrentMap.remove(key);
                IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
                if(messageParse == null){
                    throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
                }
                messageParse.updateConnectStatus(key, CommonConst.CONNECT_OFFLINE);
                return ;
            }
        }
    }


    public boolean sendMessageByKey(MessageBean messageBean, Object key) {
        String uuid = keyChannelIdConcurrentMap.get(key);
        if(!StringUtils.isEmpty(uuid) && channelConcurrentMap.containsKey(uuid)){
            Channel channel = channelConcurrentMap.get(uuid);
            if (channel != null && channel.isActive()) {
                channel.writeAndFlush(messageBean);
                return true;
            }
        }
        return false;
    }

    public static NamedChannelGroup getInstance(){
        return SingletonHolder.INSTANCE;
    }

    public boolean sendMessageBySeq(MessageBean messageBean) {
        IMessageParse messageParse = SpringContextUtils.getBean(IMessageParse.class);
        if(messageParse == null){
            throw new ProtocolParseException(ErrorEnum.REPORT_TYPE_PARSE_BEAN_NOT_FOUND);
        }
        Object key = messageParse.getMessageKey(messageBean.getJsonObject());
        return sendMessageByKey(messageBean, key);
    }


    private static class SingletonHolder {

        private static final NamedChannelGroup INSTANCE = new NamedChannelGroup();
    }
}

三、完结

1.不方便透露设备相关协议,这里就不提供测试例子了。
2.如有疑问或者更好的思路,都可以一起讨论。
3.这里只是以我工作中用到的几种设备为例,不可能适用所有协议,请多多包容。
4.如需转载,请标明出处,码字不易。
5.后面有遇到crc16校验两字节,所以以上代码需要小改一下。这里只是提供大概的思路,自己可根据自己的业务修改。