根据通讯协议封装数据帧

319 阅读18分钟

前言

通讯协议有很多种,这里主要针对自定义协议,方便数据帧的读取和写入,并使用netty进行通讯测试,服务端处理粘包、拆包、同步等待命令响应。

数据帧格式

帧结构及数据排列格式

起始码检测类型控制字数据长度数据域校验码结束符
1字节1字节1字节2字节变长2字节1字节

字节定义

  1. 起始符:1字节,该值定义为65H

  2. 检测类型:1字节,用于区分检测类型

  3. 控制字:1字节,用于区分数据类型

  4. 数据长度:2字节,其中高字节在前,低字节在后。代表数据域的长度,若为零表示无数据域

  5. 数据帧长度不大于1000字节;每个数据占4个字节;遵循 IEEE754标准的32位浮点数 (高字节在前,低字节在后)

  6. 校验码:采用CRC校验方式,发送方将起始符,检测类型、控制字、数据长度和数据区的所有字节进行CRC校验。(低字节在前,高字节在后)

  7. 结束符: 1字节,该值定义为56H

无效数据定义约定

无效数据每个字节都用FFH表示。

检测类型定义

检测类型含义说明
01HA型设备A型设备

控制字定义

控制字可供使用的有256个(00H--FFH),可根据实际应用需求进行扩充,具体定义见下表: 

控制字含义说明
01H获取设备信息

报文示例

获取设备信息

检测类型:01H

APP端下行:65 01 01 00 00 38 34 56

起始码检测类型控制字数据长度数据域校验码结束码
65H01H01H00H 00H2字节56H

设备端上行: 65 01 01 00 23 68 6F 6E 79 65 65 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 18 0B 04 22 21 56

起始码检测类型控制字数据长度数据域校验码结束码
65H01H01H00H 23H23H字节2字节56H

数据域长度为30字节,格式为:

数据意义字节长度
仪器名称(GB2312)32
出厂日期(HEX)  3

说明:

  1. 仪器名称(字符串)为不满32个字节的,后面以空格0X20表示
  2. 出厂日期为十进制的三个数,例如2024年11月4日,那么值分别为24、11、4

工具封装

调用示例

为方便读写数据,封装了DataFrameWriterDataFrameReader,只需要传入值(写)和占用的字节数(读),就能按顺序读写值,不需要关心字节索引到了哪个位置,没有额外依赖

public static void main(String[] args) {
    // 写数据
    DataFrameWriter writer = new DataFrameWriter((byte) 0x01, (byte) 0x01); // 检测字和控制字
    writer.writeString("honyee", 32);
    writer.writeDate("24 11 4");
    byte[] downBytes = writer.build();

    // 读数据
    DataFrameReader reader = new DataFrameReader(downBytes);
    String name = reader.readString(32); // honyee
    String date = reader.readDate(); // 2024年11月4日
}

如果使用了netty,可以用ByteBuf来达到类似的效果

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
    
public static void main(String[] args) {
    
    // 写数据
    ByteBuf writer = Unpooled.buffer();
    writer.writeByte((byte)0x65);
    writer.writeByte((byte)0x01);
    writer.writeByte((byte)0x01);
    writer.writeByte((byte)0x00);
    writer.writeByte((byte)0x23);
    StringBuilder stringBuilder = new StringBuilder("honyee");
    for (int i = "honyee".length(); i < 32; i++) {
        stringBuilder.append(' ');
    }
    writer.writeBytes(stringBuilder.toString().getBytes());
    writer.writeByte((byte)24);
    writer.writeByte((byte)11);
    writer.writeByte((byte)4);
    writer.writeByte((byte)0x22);
    writer.writeByte((byte)0x21);
    writer.writeByte((byte)0x56);

    // 获取bytes
    int readableBytes = writer.readableBytes();
    downBytes = new byte[readableBytes];
    writer.readBytes(downBytes);
    
    // 读数据
    ByteBuf reader = Unpooled.copiedBuffer(downBytes);
    byte start = reader.readByte();
    byte detection = reader.readByte();
    byte control = reader.readByte();
    byte lengthH = reader.readByte();
    byte lengthL = reader.readByte();
    byte[] nameBytes = new byte[32];
    for (int i = 0; i < 32; i++) {
        nameBytes[i] = reader.readByte();
    }
    String name = new String(nameBytes);
    byte year = reader.readByte();
    byte month = reader.readByte();
    byte date = reader.readByte();
}

封装代码

接下来基本是纯代码了……代码中说明了一些注意事项

提供的是完整代码,理论上来说,拷贝后能直接运行(需要根据copy后的package进行导包处理)

CRC16

CRC16-modbus,如果用查表法,计算时可能会溢出

public class CRC16 {
    /**
     * crc16-modbus
     */
    public static byte[] calc(byte[] bytes) {
        int CRC = 0x0000ffff;
        int POLYNOMIAL = 0x0000a001;

        int i, j;
        for (i = 0; i < bytes.length; i++) {
            CRC ^= ((int) bytes[i] & 0x000000ff);
            for (j = 0; j < 8; j++) {
                if ((CRC & 0x00000001) != 0) {
                    CRC >>= 1;
                    CRC ^= POLYNOMIAL;
                } else {
                    CRC >>= 1;
                }
            }
        }
        byte byteL = (byte) (CRC & 0xff);
        byte byteH = (byte) ((CRC >> 8) & 0xff);
        return new byte[]{byteL, byteH};
    }


}

DataFrameException

异常类

public class DataFrameException extends RuntimeException{
    public DataFrameException(String message) {
        super(message);
    }
}

DataFrameConstant

常量

import java.math.BigDecimal;
import java.nio.charset.Charset;

/**
 * 通讯常量
 */
public interface DataFrameConstant {
    /**
     * GB2312
     */
    Charset GB2312 = Charset.forName("GB2312");
    /**
     * 用于左移8位
     */
    BigDecimal LEFT_MOVE_8 = BigDecimal.valueOf(Math.pow(2, 8));
    /**
     * 无效数据
     */
    byte INVALID = (byte) 0xFF;
    /**
     * 仪器型号为不满15个字节的,后面以空格0X20表示
     */
    byte SPACE = (byte) 0x20;

    /**
     * 数据各帧长度
     */
    interface Length {
        int START = 1;
        int DETECTION_TYPE = 1;
        int CONTROL = 1;
        int DATA_LENGTH = 2;
        int DATA = 1000;
        int CRC = 2;
        int END = 1;

        int LENGTH_EXCLUDE_DATA = START + DETECTION_TYPE + CONTROL + DATA_LENGTH + CRC + END;
        int LENGTH_BEFORE_DATA = START + DETECTION_TYPE + CONTROL + DATA_LENGTH;
        int LENGTH_AFTER_DATA = CRC + END;
    }

    /**
     * 起始码
     */
    interface Start {
        byte START = (byte) 0x65;
    }

    /**
     * 检测类型
     */
    interface DetectionType {
        /**
         * 匿名
         */
        byte ANONYMOUS = (byte) 0xFF;
        /**
         * 设备1型
         */
        byte DEVICE_1 = (byte) 0x01;
    }

    /**
     * 控制字
     */
    interface Control {

        /**
         * 获取设备信息
         */
        byte QUERY_DEVICE_INFO = (byte) 0x01;
    }

    /**
     * 结束符
     */
    interface End {
        byte END = (byte) 0x56;
    }

}

DataFrame

数据帧结构

import lombok.Data;

@Data
public class DataFrame {

    /**
     * 起始码
     */
    byte start;

    /**
     * 检测类型
     */
    byte detectionType;

    /**
     * 控制字
     */
    byte control;

    /**
     * 数据长度,高位在前,低位在后
     */
    byte[] length;

    /**
     * 数据域
     */
    byte[] data;

    /**
     * 校验码
     */
    byte[] crc;

    /**
     * 结束码
     */
    byte end;

    public int getLengthValue() {
        return (length[0] & 0xff)  << 8 | length[1] & 0xff;
    }
}

DataFrameUtil

调试用工具类,封装了一些常用方法

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Locale;
import java.util.Random;

public class DataFrameUtil {
    private static final Random random = new Random();

    /**
     * 打印字节码(十六进制)
     *
     * @param bytes 字节码数组
     */
    public static void printBytesHex(byte[] bytes) {
        for (byte b : bytes) {
            System.out.printf("%02X ", b);
        }
        System.out.println();
    }

    /**
     * 打印字节码(十六进制)
     *
     * @param bytes 字节码数组
     */
    public static void printBytesHex0x(byte[] bytes) {
        for (byte b : bytes) {
            System.out.printf("0x%02X,", b);
        }
        System.out.println();
    }

    /**
     * 转字节码(十六进制)
     *
     * @param bytes 字节码数组
     */
    public static String toBytesStr(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }

    /**
     * 将一堆十六进制的字符串转数组
     *
     * @param dataStr "65 02 01 00 00 38 70 56"
     * @return [0x65, 0x02, 0x01, 0x00, 0x00, 0x38, 0x70, 0x56]
     */
    public static byte[] dataStrToBytes(String dataStr) {
        String[] arr = dataStr.split(" ");
        byte[] result = new byte[arr.length];
        for (int i = 0; i < arr.length; i++) {
            result[i] = (byte) Integer.parseInt(arr[i], 16);
        }
        return result;
    }

    /**
     * 创建UUID(48字节)
     */
    public static BigInteger generalUUID() {
        // 暂不考虑重复的情况
        return BigDecimal.valueOf(Math.abs(random.nextLong())).toBigInteger();
    }

    /**
     * 创建用户工号(10字节)
     */
    public static BigInteger generalUserNo() {
        // 暂不考虑重复的情况
        return BigDecimal.valueOf(Math.abs(random.nextLong()) & Long.MAX_VALUE).toBigInteger();
    }

    /**
     * 创建测试编号(8字节)
     */
    public static BigInteger generalUniqueCode() {
        // 年月日时分 + 2位随机
        Calendar time = Calendar.getInstance(Locale.CHINESE);
        int year = time.get(Calendar.YEAR);
        int month = time.get(Calendar.MONTH) + 1;
        int day = time.get(Calendar.DAY_OF_MONTH);
        int hour = time.get(Calendar.HOUR_OF_DAY);
        int minute = time.get(Calendar.MINUTE);
        byte[] eachData = new byte[]{
                (byte) (year / 100),
                (byte) (year % 100),
                (byte) month,
                (byte) day,
                (byte) hour,
                (byte) minute,
                (byte) random.nextInt(),
                (byte) random.nextInt()
        };

        return new BigInteger(eachData);
    }

    /**
     * 测试编号转日期
     *
     * @param uniqueCode generalUniqueCode()生成的测试编号
     * @return 2024年11月4日10时18分
     */
    public static String uniqueCodeToDatetime(BigInteger uniqueCode) {
        if (uniqueCode == null) {
            return null;
        }
        byte[] bytes = uniqueCode.toByteArray();
        if (bytes.length < 6) {
            return null;
        }
        return String.format("%s%s年%s月%s日%s时%s分", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]);
    }

}

DataFrameWriter

数据帧-输出


import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;

/**
 * 数据帧-输出
 */
public class DataFrameWriter {

    private static final int DATA_MAX_LENGTH = DataFrameConstant.Length.DATA;

    /**
     * 检查类型
     */
    final byte detectionType;
    /**
     * 控制
     */
    final byte control;

    private final byte[] frameData;
    private int dataCount;

    public DataFrameWriter(byte detectionType, byte control) {
        this.detectionType = detectionType;
        this.control = control;
        frameData = new byte[DATA_MAX_LENGTH];
        dataCount = 0;
    }

    /**
     * 构建完整的数据
     *
     * @return DataFrameWrite
     */
    public byte[] build() {
        if (dataCount > DATA_MAX_LENGTH) {
            throw new RuntimeException("数据超出限制,最大" + DATA_MAX_LENGTH + "个字符");
        }
        // 起始符,检测类型、控制字、数据长度、数据区、CRC、结束符
        byte[] temp = new byte[DataFrameConstant.Length.LENGTH_BEFORE_DATA + dataCount + DataFrameConstant.Length.LENGTH_AFTER_DATA];
        int index = 0;
        temp[index++] = DataFrameConstant.Start.START;
        temp[index++] = detectionType;
        temp[index++] = control;
        byte[] dataLengthBytes = dataLengthBytes(dataCount);
        temp[index++] = dataLengthBytes[0];
        temp[index++] = dataLengthBytes[1];
        System.arraycopy(getFrameData(), 0, temp, index, dataCount);
        index += dataCount;
        byte[] crcData = new byte[DataFrameConstant.Length.LENGTH_BEFORE_DATA + dataCount];
        System.arraycopy(temp, 0, crcData, 0, crcData.length);
        byte[] crcBytes = CRC16.calc(crcData);
        temp[index++] = crcBytes[0];
        temp[index++] = crcBytes[1];
        temp[index++] = DataFrameConstant.End.END;
        return temp;
    }

    /**
     * 占据1字节
     */
    public DataFrameWriter writeByte(byte data) {
        frameData[dataCount++] = (byte) ((data & 0xFF));
        return this;
    }

    /**
     * 占据bytes.length字节
     */
    public DataFrameWriter writeByteArray(byte[] bytes) {
        for (byte data : bytes) {
            writeByte(data);
        }
        return this;
    }

    /**
     * 占位填充数值
     *
     * @param bytes  数据
     * @param length 占用字节数
     */
    private DataFrameWriter addPlaceholderNumber(byte[] bytes, int length) {
        byte[] temp = new byte[length];
        // 字节长度超过需要占用的长度,需要截取
        if (bytes.length > length) {
            for (int i = 0; i < length; i++) {
                temp[temp.length - i - 1] = bytes[bytes.length - i - 1];
            }
        }
        // 字节长度小于需要占用的长度,需要高字节补零
        else {
            int copyStart = length - bytes.length;
            System.arraycopy(bytes, 0, temp, copyStart, bytes.length);
        }
        writeByteArray(temp);
        return this;
    }

    /**
     * 占位填充字符串
     *
     * @param bytes  数据
     * @param length 占用字节数
     */
    private DataFrameWriter addPlaceholderString(byte[] bytes, int length) {
        byte[] temp = new byte[length];
        for (int i = 0; i < length; i++) {
            if (i < bytes.length) {
                temp[i] = bytes[i];
            } else {
                temp[i] = DataFrameConstant.SPACE;
            }
        }
        writeByteArray(temp);
        return this;
    }

    /**
     * 添加整型
     *
     * @param value  数值
     * @param length 占用字节数
     */
    public DataFrameWriter writeInt(Integer value, int length) {
        byte[] byteArray = new BigDecimal(value).toBigInteger().toByteArray();
        return addPlaceholderNumber(byteArray, length);
    }

    /**
     * 添加大整型
     *
     * @param value  数值
     * @param length 占用字节数
     */
    public DataFrameWriter writeBigInt(BigInteger value, int length) {
        byte[] byteArray = value.toByteArray();
        return addPlaceholderNumber(byteArray, length);
    }

    /**
     * 添加年月日,或者时分秒,固定3字节
     *
     * @param value 例如 20 10 16,用空格分隔
     */
    public DataFrameWriter writeDate(String value) {
        for (String str : value.split(" ")) {
            writeInt(Integer.parseInt(str), 1);
        }
        return this;
    }

    /**
     * 添加浮点数
     *
     * @param value  数值
     * @param scale  保留小数位数
     */
    public DataFrameWriter writeFloat(float value, int scale) {
        return writeFloat(value, 4, scale);
    }

    /**
     * 添加浮点数
     *
     * @param value  数值
     * @param length 占用字节数,浮点数固定4个字节
     * @param scale  保留小数位数
     */
    public DataFrameWriter writeFloat(float value, int length, int scale) {
        BigDecimal bValue = BigDecimal.valueOf(value);
        return writeFloat(bValue, length, scale);
    }

    /**
     * 添加浮点数
     *
     * @param value  数值
     * @param length 占用字节数,浮点数固定4个字节
     * @param scale  保留小数位数
     */
    public DataFrameWriter writeFloat(BigDecimal value, int length, int scale) {
        BigDecimal scaleValue = value.setScale(scale, RoundingMode.HALF_DOWN);
        ByteBuffer byteBuffer = ByteBuffer.allocate(length);
        byteBuffer.putFloat(scaleValue.floatValue());
        writeByteArray(byteBuffer.array());
        return this;
    }

    /**
     * 添加字符串
     */
    public DataFrameWriter writeString(String str, int length) {
        byte[] bytes = str.getBytes(DataFrameConstant.GB2312);
        return addPlaceholderString(bytes, length);
    }

    /**
     * 获取数据帧
     *
     * @return 数据帧
     */
    public byte[] getFrameData() {
        byte[] result = new byte[dataCount];
        System.arraycopy(frameData, 0, result, 0, dataCount);
        return result;
    }

    /**
     * 数据长度:数值转字节,高位在前,低位在后
     */
    private byte[] dataLengthBytes(int number) {
        byte[] array = new byte[2];
        array[0] = (byte) ((number >> 8) & 0xFF);
        array[1] = (byte) (number & 0xFF);
        return array;
    }
}

DataFrameReader

数据帧-读取


import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;

/**
 * 数据帧-读取
 */
public class DataFrameReader {

    /**
     * 原始数据
     */
    byte[] oriData;

    DataFrame dataFrame;

    /**
     * 当前索引
     */
    int index;

    public DataFrameReader(byte[] bytes) {
        initAndValidOriData(bytes);

        dataFrame = new DataFrame();
        dataFrame.start = bytes[0];
        dataFrame.detectionType = bytes[1];
        dataFrame.control = bytes[2];

        byte byteH = bytes[3];
        byte byteL = bytes[4];

        dataFrame.length = new byte[]{byteH, byteL};
        int dataLength = dataFrame.getLengthValue();
        byte[] data = new byte[dataLength];
        System.arraycopy(bytes, 5, data, 0, dataLength);
        dataFrame.data = data;

        int cur = dataLength + 5;
        dataFrame.crc = new byte[]{bytes[cur], bytes[cur + 1]};
        dataFrame.end = bytes[cur + 2];
    }

    /**
     * 校验原始数据
     */
    public void initAndValidOriData(byte[] bytes) {
        if (bytes[0] != DataFrameConstant.Start.START) {
            throw new DataFrameException("数据格式不符合规范:Begin");
        }

        if (bytes[bytes.length - 1] != DataFrameConstant.End.END) {
            throw new DataFrameException("数据格式不符合规范:End");
        }

        byte[] temp = new byte[bytes.length - 3];
        System.arraycopy(bytes, 0, temp, 0, temp.length);
        byte[] crcBytes = CRC16.calc(temp);
        if (crcBytes[0] != bytes[bytes.length - 3] || crcBytes[1] != bytes[bytes.length - 2]) {
            throw new DataFrameException("数据格式不符合规范:CRC");
        }

        this.oriData = bytes;
    }

    public DataFrame getDataFrame() {
        return dataFrame;
    }

    public byte[] getOriData() {
        return oriData;
    }

    public void reset() {
        index = 0;
    }

    /**
     * 读取字节,并将index后移
     *
     * @param length 占用字节数
     * @return bytes
     */
    private byte[] readFieldBytes(int length) {
        byte[] data = getDataFrame().getData();
        if (index + length > data.length) {
            throw new DataFrameException("读取数据失败,超出索引");
        }

        byte[] temp = new byte[length];
        System.arraycopy(data, index, temp, 0, length);
        index += length;
        return temp;
    }

    /**
     * 读取字节数据
     *
     * @param length 占用字节数
     */
    public byte[] readByte(int length) {
        return readFieldBytes(length);
    }

    /**
     * 读取字符串
     *
     * @param length 占用字节数
     */
    public String readString(int length) {
        byte[] bytes = readFieldBytes(length);
        return readString(bytes);
    }

    /**
     * 读取字符串(文档中标为ASCII),实际应该是GB2312
     *
     * @param bytes 消息中的bytes
     * @return 转明文
     */
    private String readString(byte[] bytes) {
        return new String(bytes, DataFrameConstant.GB2312);
    }

    /**
     * 读取浮点数
     */
    public BigDecimal readFloat() {
        return readFloat(4, 2);
    }


    /**
     * 读取浮点数
     */
    public BigDecimal readFloat(int scale) {
        return readFloat(4, scale);
    }

    /**
     * 读取浮点数
     *
     * @param length 占用字节数,浮点数一般是4个字节
     * @param scale  保留小数位数
     */
    public BigDecimal readFloat(int length, int scale) {
        byte[] bytes = readFieldBytes(length);
        int count = 0;
        for (byte b : bytes) {
            if (b == DataFrameConstant.INVALID) {
                count++;
            }
        }
        // 所有字节都为0xFF则为无效数据
        if (count == bytes.length) {
            return null;
        }
        return BigDecimal.valueOf(ByteBuffer.wrap(bytes).getFloat()).setScale(scale, RoundingMode.HALF_DOWN);
    }

    public String readDate() {
        byte[] bytes = readFieldBytes(3);
        return String.format("20%s年%s月%s日", bytes[0], bytes[1], bytes[2]);
    }

    public String readTime() {
        byte[] bytes = readFieldBytes(3);
        return String.format("%s时%s分%s秒", bytes[0], bytes[1], bytes[2]);
    }

    public String readDateTime() {
        byte[] bytes = readFieldBytes(6);
        return String.format("%s年%s月%s日%s时%s分%s秒", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]);
    }

    /**
     * 读取数值
     *
     * @param length 占用字节数
     */
    public Integer readInt(int length) {
        return readBigInt(length).intValue();
    }

    /**
     * 读取大数值
     * <p>
     * 高字节前,低字节后
     *
     * @param length 占用字节数
     */
    public BigInteger readBigInt(int length) {
        byte[] bytes = readFieldBytes(length);
        // 不能直接使用 new BigInteger(bytes) 转换
        BigInteger a = BigInteger.ZERO;
        for (int i = 0; i < bytes.length; i++) {
            a = a.shiftLeft(8).add(BigInteger.valueOf(bytes[i] & 0xFF));
        }
        return a;
    }

}

通讯测试

Maven依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.72.Final</version>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.15</version>
</dependency>

代码

如果需要同步等待命令反馈,在发送消息时需要使用如下方法

byte[] up = SyncUtil.send(Channel channel, byte[] down);

启动服务和客户端

import io.netty.channel.Channel;

public class TestMain {
    public static void main(String[] args) {
        Delegate delegate = new CustomDelegate();
        ChannelHelper channelHelper = new ChannelHelper();
        String host = "127.0.0.1";
        int port = 1900;
        // 创建服务
        NettyServer nettyServer = new NettyServer(port, delegate, channelHelper);

        // 创建客户端
        NettyClient nettyClient = new NettyClient(delegate, channelHelper);
        Channel channel1 = nettyClient.connect(host, port);
        Channel channel2 = nettyClient.connect(host, port);
        channel1.writeAndFlush(new byte[]{1, 2, 3});
        channel2.writeAndFlush(new byte[]{1, 2, 3});
    }
}

NettyDecoder

处理接收到的数据,并处理粘包(多次数据合并成一次上传)、拆包(数据包太大分成多次上传)

  1. 粘包:

原始数据为65 02 01 00 00 38 70 5665 02 01 00 00 38 70 56,被合并上传

65 02 01 00 00 38 70 56 65 02 01 00 00 38 70 56

  1. 拆包

原始数据为65 02 01 00 00 38 70 56,被分成了两次上传

65 02 01 00 0038 70 56

import com.google.common.cache.Cache;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class NettyDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (!in.isReadable()) {
            return;
        }
        int total = in.readableBytes();
        if (total == 0) {
            return;
        }
        byte[] bytes = new byte[total];
        in.readBytes(bytes);
        log.info("Netty 0Decoder:当前收到的字节:{}", DataFrameUtil.toBytesStr(bytes));

        Channel channel = ctx.channel();
        String channelId = channel.id().asLongText();

        // 处理拆包
        byte[] tempBytes = handleUnpacking(bytes, channelId);
        // 处理粘包
        List<byte[]> list = handlePacketSticking(tempBytes, channelId);
        // 发送数据
        out.addAll(list);

    }

    /**
     * 处理拆包
     * <p>
     * 不考虑边界问题:分帧的中间是START和END的问题
     */
    private byte[] handleUnpacking(byte[] bytes, String channelId) {
        Cache<String, byte[]> decodeCache = SyncUtil.getDecodeCache();
        byte start = bytes[0];
        byte end = bytes[bytes.length - 1];

        byte[] finalBytes = new byte[0];
        if (end == DataFrameConstant.End.END) {
            // 到达结束帧
            byte[] temp = decodeCache.getIfPresent(channelId);
            if (temp == null) {
                finalBytes = bytes;
            } else {
                finalBytes = append(temp, bytes);
            }
            // 清理数据
            decodeCache.invalidate(channelId);
            // 继续往下执行
            //log.info("Netty Decoder:到达结束帧,当前传入字节数={}", total);

        } else {
            if (start == DataFrameConstant.Start.START) {
                // 到达开始帧
                decodeCache.put(channelId, bytes);
            } else {
                byte[] temp = decodeCache.getIfPresent(channelId);
                if (temp == null) {
                    // 错误数据,不处理
                    //log.info("Netty Decoder:抛弃数据,没头没尾的数据:{}", DataFrameUtil.toBytesStr(bytes));
                } else {
                    // 中间帧
                    finalBytes = append(temp, bytes);
                    decodeCache.put(channelId, finalBytes);
                }
            }
            // 等待下一帧
            //log.info("Netty Decoder:等待下一帧,当前传入字节数={}", total);
        }
        // 完整帧
        return finalBytes;
    }

    /**
     * 处理粘包
     */
    private List<byte[]> handlePacketSticking(byte[] bytes, String channelId) {
        List<byte[]> list = new ArrayList<>();
        if (bytes.length == 0) {
            return list;
        }

        // 处理粘包,两帧合成一帧的情况
        int total = bytes.length;
        byte before = 0;
        byte now = 0;
        byte[] temp = new byte[total];
        int index = 0;
        for (byte aByte : bytes) {
            before = now;
            now = aByte;
            if (before == DataFrameConstant.End.END && now == DataFrameConstant.Start.START) {
                byte[] outTemp = new byte[index];
                System.arraycopy(temp, 0, outTemp, 0, index);
                list.add(outTemp);
                // 重新初始化
                temp = new byte[total];
                index = 0;
            }
            temp[index++] = now;
        }
        // 最后一段数据
        byte[] outTemp = new byte[index];
        System.arraycopy(temp, 0, outTemp, 0, index);
        Cache<String, byte[]> decodeCache = SyncUtil.getDecodeCache();
        if (outTemp.length > 2) {
            byte start = outTemp[0];
            byte end = outTemp[outTemp.length - 1];
            if (start == DataFrameConstant.Start.START) {
                if (end == DataFrameConstant.End.END) {
                    // 依旧是完整的一帧
                    if (outTemp.length >= DataFrameConstant.Length.LENGTH_EXCLUDE_DATA) {
                        list.add(outTemp);
                    } else {
                        // 无用数据:不满足最小一帧的长度
                    }
                } else {
                    // 不是完整的一帧,放回缓冲池
                    decodeCache.put(channelId, outTemp);
                }
            } else {
                // 无用数据:开头不是Start
            }
        } else {
            // 不是完整的一帧,放回缓冲池
            decodeCache.put(channelId, outTemp);
        }
        return list;
    }

    /**
     * 追加分帧数
     *
     * @param temp   已有数据
     * @param append 追加数据
     * @return 结果
     */
    private byte[] append(byte[] temp, byte[] append) {
        ByteBuf byteBuf = Unpooled.buffer().writeBytes(temp);
        byteBuf.writeBytes(append);
        byte[] result = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(result);
        return result;
    }

}

NettyEncoder

数据输出处理

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class NettyEncoder extends MessageToByteEncoder<byte[]> {

    @Override
    protected void encode(ChannelHandlerContext ctx, byte[] msg, ByteBuf out) throws Exception {
        out.writeBytes(msg);
    }
}

SyncPromise

同步等待下行命令的反馈


import cn.hutool.log.Log;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 同步等待下行反馈
 */
public class SyncPromise {

    // 用于接收结果
    private byte[] data;

    //CountDownLatch可以看作是一个计数器,当计数器的值减到0时,所有等待的线程将被释放并继续执行。
    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    // 用于判断是否超时
    private boolean isTimeout = false;

    /**
     * 同步等待返回结果
     * timeout 超时时间    unit 时间单位
     */
    public byte[] get(long timeout, TimeUnit unit) throws InterruptedException {
        // 等待阻塞,超时时间内countDownLatch减到0,将提前唤醒,以此作为是否超时判断
        // 如果在指定时间内计数器仍未归零,则返回false,否则返回true。
        boolean earlyWakeUp = countDownLatch.await(timeout, unit);
        if (earlyWakeUp) {
            // 超时时间内countDownLatch减到0,提前唤醒,说明已有结果
            return data;
        } else {
            Log.get().warn("超时未响应");
            // 超时时间内countDownLatch没有减到0,自动唤醒,说明超时时间内没有等到结果
            isTimeout = true;
            return null;
        }
    }

    // 计数器清零,唤醒
    public void wake() {
        countDownLatch.countDown();
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

    public boolean isTimeout() {
        return isTimeout;
    }
}

SyncUtil

异步工具类


import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.netty.channel.Channel;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.util.concurrent.Future;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * 异步工具类
 */
@Slf4j
public class SyncUtil {
    /**
     * 等待时间
     */
    private static final int WAIT_TIME = 5;
    /**
     * 过期时间
     */
    private static final int EXPIRED_TIME = WAIT_TIME + 5;
    private static final EventLoopGroup worker = new DefaultEventLoopGroup(Runtime.getRuntime().availableProcessors() + 1);

    // 缓存:下行等待
    private static final Cache<String, SyncPromise> syncPromiseCache = CacheBuilder.newBuilder()
            .expireAfterAccess(EXPIRED_TIME, TimeUnit.SECONDS)
            .maximumSize(100)
            .build();

    // 缓存:上行数据,用于解决上行数据分帧
    private static final Cache<String, byte[]> decodeCache = CacheBuilder.newBuilder()
            .expireAfterAccess(EXPIRED_TIME, TimeUnit.SECONDS)
            .maximumSize(100)
            .build();


    @SneakyThrows
    public static byte[] send(Channel channel, byte[] down) {
        return send(channel.id().asLongText(), channel, down, WAIT_TIME, TimeUnit.SECONDS);
    }

    /**
     * @param key 唯一主键类,方便在处理逻辑中找到唤醒
     */
    @SneakyThrows
    private static byte[] send(String key, Channel channel, byte[] downBytes, long timeout, TimeUnit unit) {

        try {
            // 创造一个容器,用于存放当前线程与rpcClient中的线程交互
            SyncPromise syncPromise = new SyncPromise();
            syncPromiseCache.put(key, syncPromise);

            // 发送消息,此处如果发送完消息并且在get之前返回了结果,下一行的get将不会进入阻塞,也可以顺利拿到结果
            submit(() -> {
                log.info("发送数据:{}", DataFrameUtil.toBytesStr(downBytes));
                channel.writeAndFlush(downBytes);
            });

            // 等待获取结果
            byte[] up = syncPromise.get(timeout, unit);

            if (up == null) {
                if (syncPromise.isTimeout()) {
                    log.error("等待响应结果超时");
                    // throw new TimeoutException("等待响应结果超时");
                } else {
                    log.error("其他异常");
                    // throw new Exception("其他异常");
                }
            }
            // 移除容器
            return up;
        } finally {
            syncPromiseCache.invalidate(key);
        }
    }

    public static Cache<String, SyncPromise> getSyncPromiseCache() {
        return syncPromiseCache;
    }

    public static Cache<String, byte[]> getDecodeCache() {
        return decodeCache;
    }

    public static Future<?> submit(Runnable task) {
        return worker.submit(task);
    }

}

NettyHandler

Channel各种情况处理:建立连接、收到消息、异常、断开连接


import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;

@ChannelHandler.Sharable
@Slf4j
public class NettyHandler extends SimpleChannelInboundHandler<byte[]> {

    private final Delegate delegate;

    private final ChannelHelper channelHelper;

    public NettyHandler(Delegate delegate, ChannelHelper channelHelper) {
        this.delegate = delegate;
        this.channelHelper = channelHelper;
    }

    /**
     * 消息读取
     */
    @Override
    public void channelRead0(ChannelHandlerContext ctx, byte[] bytes) {
        log.info("收到客户端消息:" + DataFrameUtil.toBytesStr(bytes));
        ChannelData<Object> channelData = channelHelper.getChannelData(ctx);

        //查询相应key对应的是否有返回,如果有返回就唤醒,直接返回响应数据
        SyncPromise syncPromise = SyncUtil.getSyncPromiseCache().getIfPresent(channelData.getChannelId());
        if (syncPromise != null) {
            //在获取对象不为null时执行唤醒操作
            syncPromise.setData(bytes);
            syncPromise.wake();
        } else {
            delegate.channelRead(channelData, bytes);
        }
    }

    /***
     * 超时关闭socket 连接
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;

            String eventType = null;
            switch (event.state()) {
                case READER_IDLE:
                    eventType = "读超时";
                    break;
                case WRITER_IDLE:
                    eventType = "写超时";
                    break;
                case ALL_IDLE:
                    eventType = "读写超时";
                    break;
                default:
                    eventType = "设备超时";
            }
            log.warn("{} : {}---> 关闭该设备", ctx.channel().id(), eventType);
            ctx.channel().close();
        }
    }

    /**
     * 异常处理, 出现异常关闭channel
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ChannelData<Object> channelData = channelHelper.getChannelData(ctx);
        delegate.exceptionCaught(channelData, cause);
        log.error("========= 链接出现异常:{}", cause.getMessage());
    }

    /**
     * 每加入一个新的链接,保存该通道并写入上线日志。该方法在channelRead方法之前执行
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        ChannelData channelData = channelHelper.putChannelData(ctx);
        delegate.handlerAdded(channelData);
        log.info("========= 设备加入链接:{}", channelData.getChannelId());
    }

    /**
     * 每去除一个新的链接,去除该通道并写入下线日志
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        try {
            delegate.handlerRemoved(channelHelper.getChannelData(ctx));
        } finally {
            ChannelData channelData = channelHelper.removeChannelData(ctx);
            log.info("========= 设备断开链接:{}", channelData.getChannelId());
        }
    }
}

Delegate

/**
 * 扩展接口
 */
public interface Delegate {

    void channelRead(ChannelData channelData, byte[] bytes);

    void handlerAdded(ChannelData channelData);

    void handlerRemoved(ChannelData channelData);

    void exceptionCaught(ChannelData channelData, Throwable cause);

}
public class CustomDelegate implements Delegate {
    @Override
    public void channelRead(ChannelData channelData, byte[] bytes) {

    }

    @Override
    public void handlerAdded(ChannelData channelData) {

    }

    @Override
    public void handlerRemoved(ChannelData channelData) {

    }

    @Override
    public void exceptionCaught(ChannelData channelData, Throwable cause) {

    }
}

ChannelData


import io.netty.channel.ChannelHandlerContext;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 连接的通道数据
 * @param <T>
 */
@Data
public class ChannelData<T> {

    /**
     * Channel
     */
    private ChannelHandlerContext ctx;

    /**
     * 连接时间
     */
    private LocalDateTime createTime;

    private T data;

    public String getChannelId() {
        return ctx.channel().id().asLongText();
    }

    public ChannelData(ChannelHandlerContext ctx) {
        this.ctx = ctx;
        this.createTime = LocalDateTime.now();
    }
}

ChannelHelper

import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 管理连接的通道
 */
@Slf4j
public class ChannelHelper {

    private final Map<String, ChannelData> CHANNEL_DATA_MAP = new ConcurrentHashMap<>();

    public Map<String, ChannelData> getChannelDataMap() {
        return CHANNEL_DATA_MAP;
    }

    public <T> ChannelData<T> getChannelData(ChannelHandlerContext ctx) {
        return CHANNEL_DATA_MAP.get(new ChannelData<>(ctx).getChannelId());
    }

    public <T> ChannelData<T> getChannelData(String channelId) {
        return CHANNEL_DATA_MAP.get(channelId);
    }

    public <T> ChannelData<T> getChannelDataWithCheck(String channelId) {
        ChannelData channelData = CHANNEL_DATA_MAP.get(channelId);
        if (channelData == null) {
            throw new RuntimeException("无法操作,设备可能已经断开");
        }
        return channelData;
    }

    public ChannelData putChannelData(ChannelHandlerContext ctx) {
        ChannelData<Object> channelData = new ChannelData<>(ctx);
        CHANNEL_DATA_MAP.put(channelData.getChannelId(), channelData);
        return channelData;
    }

    public ChannelData removeChannelData(ChannelHandlerContext ctx) {
        return CHANNEL_DATA_MAP.remove(new ChannelData<>(ctx).getChannelId());
    }

}

### NettyServiceConfig

服务端配置类

```java
import lombok.Data;

@Data
public class NettyServerConfig {

    private int port;

    private boolean reuseAddr = true;

        private int backLogSize = 1024;

    private int recBufSize = 10485760;

    private boolean keepAlive = true;

    private boolean nodelay = true;

    private boolean heartBeatEnable = true;

    private boolean scanEnable = true;

    private int messageScanFreq = 10;    // seconds

    public NettyServerConfig(int port) {
        this.port = port;
    }
}

NettyServer

服务端

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyServer {

    private NioEventLoopGroup bossGroup;

    private NioEventLoopGroup workGroup;

    ServerBootstrap bootstrap = null;

    private final NettyServerConfig nettyServerConfig;

    private final NettyHandler nettyHandler;

    public NettyServer(int port, Delegate delegate, ChannelHelper channelHelper) {
        this.nettyServerConfig = new NettyServerConfig(port);
        this.nettyHandler = new NettyHandler(delegate, channelHelper);
        start();
    }

    public void start() {
        bootstrap = new ServerBootstrap();
        bossGroup = new NioEventLoopGroup();
        workGroup = new NioEventLoopGroup();
        bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_REUSEADDR, nettyServerConfig.isReuseAddr())
                .option(ChannelOption.SO_BACKLOG, nettyServerConfig.getBackLogSize())
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .option(ChannelOption.SO_RCVBUF, nettyServerConfig.getRecBufSize())
                .childOption(ChannelOption.SO_KEEPALIVE, nettyServerConfig.isKeepAlive())
                .childOption(ChannelOption.TCP_NODELAY, nettyServerConfig.isNodelay())
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast("encoder", new NettyEncoder())
                                .addLast("decoder", new NettyDecoder())
                                .addLast("handler", nettyHandler);
                    }
                });
        bootstrap.bind(nettyServerConfig.getPort()).addListener((ChannelFutureListener) channelFuture -> {
            if (channelFuture.isSuccess())
                log.info("Netty Server 启动成功");
            else
                log.info("Netty Server 启动失败");
        });

    }

}

NettyClient

客户端

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class NettyClient {

    ExecutorService pool = Executors.newCachedThreadPool();

    Bootstrap bootstrap = null;

    private final NettyHandler nettyHandler;

    public NettyClient(Delegate delegate, ChannelHelper channelHelper) {
        this.nettyHandler = new NettyHandler(delegate, channelHelper);
        start();
    }

    public void start() {
        // 创建事件循环组,用于处理客户端的I/O操作
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建Bootstrap对象,用于配置和启动客户端
            bootstrap = new Bootstrap();
            bootstrap.group(group)
                    // 设置通道类型为NioSocketChannel,用于基于NIO的套接字连接
                    .channel(NioSocketChannel.class)
                    // 设置TCP参数,例如TCP_NODELAY表示禁用Nagle算法,使数据立即发送
                    .option(ChannelOption.TCP_NODELAY, true)
                    // 设置处理器,用于处理连接后的通道事件和数据读写
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 在这里可以添加自定义的解码器、编码器和业务处理器
                            socketChannel.pipeline().addLast(new NettyDecoder());
                            socketChannel.pipeline().addLast(new NettyEncoder());
                            socketChannel.pipeline().addLast(nettyHandler);
                        }
                    });
        } catch (Exception e) {
            log.error("Netty Client 初始化失败", e);
        } finally {
            // 关闭事件循环组,释放资源
            // group.shutdownGracefully();
        }
    }

    public Channel connect(String host, int port) {
        try {
            // 连接服务端,指定服务端的IP地址和端口号
            ChannelFuture future = bootstrap.connect(host, port);
            // 等待连接完成,这会阻塞当前线程直到连接成功或者失败
            future.sync();
            pool.submit(() -> {
                // 等待通道关闭,这会阻塞当前线程直到通道关闭
                try {
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    log.error("Netty Client sync error", e);
                }
            });
            return future.channel();
        } catch (Exception e) {
            log.error("Netty Client connect error", e);
            throw new RuntimeException(e.getMessage());
        }
    }

}