物联网设备,采用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校验两字节,所以以上代码需要小改一下。这里只是提供大概的思路,自己可根据自己的业务修改。