前言
通讯协议有很多种,这里主要针对自定义协议,方便数据帧的读取和写入,并使用netty进行通讯测试,服务端处理粘包、拆包、同步等待命令响应。
数据帧格式
帧结构及数据排列格式
| 起始码 | 检测类型 | 控制字 | 数据长度 | 数据域 | 校验码 | 结束符 |
|---|---|---|---|---|---|---|
| 1字节 | 1字节 | 1字节 | 2字节 | 变长 | 2字节 | 1字节 |
字节定义
-
起始符:1字节,该值定义为
65H -
检测类型:1字节,用于区分检测类型
-
控制字:1字节,用于区分数据类型
-
数据长度:2字节,其中高字节在前,低字节在后。代表数据域的长度,若为零表示无数据域
-
数据帧长度不大于1000字节;每个数据占4个字节;遵循 IEEE754标准的32位浮点数 (高字节在前,低字节在后)
-
校验码:采用CRC校验方式,发送方将起始符,检测类型、控制字、数据长度和数据区的所有字节进行CRC校验。(低字节在前,高字节在后)
-
结束符: 1字节,该值定义为
56H
无效数据定义约定
无效数据每个字节都用FFH表示。
检测类型定义
| 检测类型 | 含义 | 说明 |
|---|---|---|
| 01H | A型设备 | A型设备 |
控制字定义
控制字可供使用的有256个(00H--FFH),可根据实际应用需求进行扩充,具体定义见下表:
| 控制字 | 含义 | 说明 |
|---|---|---|
| 01H | 获取设备信息 |
报文示例
获取设备信息
检测类型:01H
APP端下行:65 01 01 00 00 38 34 56
| 起始码 | 检测类型 | 控制字 | 数据长度 | 数据域 | 校验码 | 结束码 |
|---|---|---|---|---|---|---|
| 65H | 01H | 01H | 00H 00H | 无 | 2字节 | 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
| 起始码 | 检测类型 | 控制字 | 数据长度 | 数据域 | 校验码 | 结束码 |
|---|---|---|---|---|---|---|
| 65H | 01H | 01H | 00H 23H | 23H字节 | 2字节 | 56H |
数据域长度为30字节,格式为:
| 数据意义 | 字节长度 |
|---|---|
| 仪器名称(GB2312) | 32 |
| 出厂日期(HEX) | 3 |
说明:
- 仪器名称(字符串)为不满32个字节的,后面以空格
0X20表示 - 出厂日期为十进制的三个数,例如2024年11月4日,那么值分别为24、11、4
工具封装
调用示例
为方便读写数据,封装了DataFrameWriter和DataFrameReader,只需要传入值(写)和占用的字节数(读),就能按顺序读写值,不需要关心字节索引到了哪个位置,没有额外依赖。
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
处理接收到的数据,并处理粘包(多次数据合并成一次上传)、拆包(数据包太大分成多次上传)
- 粘包:
原始数据为65 02 01 00 00 38 70 56 和 65 02 01 00 00 38 70 56,被合并上传
65 02 01 00 00 38 70 56 65 02 01 00 00 38 70 56
- 拆包
原始数据为65 02 01 00 00 38 70 56,被分成了两次上传
65 02 01 00 00和38 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());
}
}
}