记录使用 netty 的过程,本人是一个菜鸟。
废话不多说直接开始
业务是程序需要接收两个互联网设备的数据,数据格式不一样,设备A是String,设备B是需要将每个字节进行解析。
实现处理多个设备过程
思路:我们定义一个转接处理器 TypeHandle。
public class WSServerInitializer extends ChannelInitializer<NioSocketChannel> {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) {
nioSocketChannel.pipeline().addLast(new TypeHandle());
}
}
在这里继承 ChannelInitializer,它是一个父类方法。重写 channelRead 进行处理数据。
通过 ctx.pipeline().addLast(new StringHandle()); 进行添加处理器,通过ctx.fireChannelRead(str);进行数据转发。这样我们就可以在符合目标的处理器中处理数据了。字符串数据直接通过转字符串就可以了,不过多介绍这一点了。
public class TypeHandle extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String str = ((ByteBuf) msg).toString(StandardCharsets.UTF_8);
if (str.startsWith("*CS") && str.endsWith("#")) {
ctx.pipeline().addLast(new StringHandle());
ctx.fireChannelRead(str);
} else {
ctx.pipeline().addLast(new ByteDecodeHandle());
ctx.fireChannelRead(msg);
}
}
}
看一下 StringHandle 的类,继承 SimpleChannelInboundHandler 类,并泛型指定 String。
SimpleChannelInboundHandler<String> 是 Netty 框架中用于处理特定类型消息(字符串类型)的处理器。它是 Netty 中的一个泛型类,用于处理入站数据
public class StringHandle extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) {
}
}
处理字节数据
首先我们先认识一下 ByteToMessageDecoder
解码器,用于将字节解码为消息对象,在网路通信中,数据是以字节流形式进行传输的,而 ByteToMessageDecoder 作用是实现了这一个过程。
触发时机
在自定义的 ByteDecodeHandle 类中我们继承了 ByteToMessageDecoder
,所以当设备数据发送之后,会自动进入 decode 进行解码数据。decode 方法就是我们的解码方法,在这里我们主要对 ByteBuf 进行处理,它就是我们的数据。
public class ByteDecodeHandle extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
}
}
crc 校验
需要将数据传入指定算法中,首先 COPY 一个 bytebuf,因为读数据时,bytebuf中的指针会位移。不包括最后的2个字节数据,这是根据实际业务规定的。最终通过 calculateCRC16 进行校验。因为是 COPY 的 bytebuf,最后要释放一下资源,通过 release
ByteBuf byteBuf = in.copy();
// 校验 CRC
// 创建一个字节数组,长度为 copiedByteBuf 的可读字节数减去2(不包括倒数第二个字节)
byte[] byteArray = new byte[byteBuf.readableBytes() - 2];
// 从 copiedByteBuf 中读取数据到字节数组中,从0读到倒数第二个字节
byteBuf.readBytes(byteArray);
// 释放 copiedByteBuf
byteBuf.release();
int i = CRC16.calculateCRC16(byteArray);
判断数据是否完整
getShortLE 是 bytebuf 的方法,用于获取小端序2个字节数据,参数6表示字节偏移6,可以看上面文档。通过 readableBytes 方法获取可读长度,如果小于数据长度,表示当前数据不完整。
// 解析数据长度
short dataLength = in.getShortLE(6);
// 判断数据是否完整
if (in.readableBytes() < dataLength) {
System.out.println("数据不完整!");
}
读取 IMEI 16位字节的呢
由于 IMEI 是16位的字节数,我目前业务的实现方式是,将字节数组每一位转成 char,最后拼接起来转换成数字。
// IMEI,读入 16位
byte[] dst = new byte[16];
in.getBytes(26, dst);
StringBuilder sb = new StringBuilder();
// 将字节数组转换为字符串
for (byte b : dst) {
if (b == 0) break;
sb.append((char) b);
}
// 将字符串解析为数字
Long IMEI = Long.parseLong(sb.toString());
最后说一下读字节的方式,去除 LE 表示大端序
# 获取四个字节
int productSN = in.getIntLE(8);
# 获取一个字节
byte deviceType = in.getByte(12);
# 获取一个字节,结果是 char
char hardwareVersion = (char) in.getByte(13);
# 获取两个字节
short backgroundAngle = in.getShortLE(18);
处理十六进制数据
有时候我们接收到的数据是十六进制数据,但是使用 ByteToMessageDecoder 接收到的数据并不是十六进制的,我们需要做一些处理。
通过 ByteBufUtil.hexDump(msg1).toUpperCase();
将数据转换成字符串。我们发现结果是十六进制的正常数据。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 将数据转换成十六进制
ByteBuf msg1 = (ByteBuf) msg;
String hexDump = ByteBufUtil.hexDump(msg1).toUpperCase();
看下面数据,假设我们拿到 AA,市面上方式是这样加密的:AA = 65
我们需要将 netty 读取到的 6565,转换成 AA,通过 Long.parseLong 转换成十进制。
Long.parseLong(((char) in.readByte()) + String.valueOf((char) in.readByte()), 16);
再来看一个低位数据和高位数据的案例
// 首先由于十六进制数据是两个数据进行拼接的,我们将两个数据进行拼接,作为一个十六进制数据。
String one = ((char) one1 + String.valueOf((char) one2));
String two = ((char) one3 + String.valueOf((char) one3));
// 我们拿到两个十六进制数据后,进行低位在前高位在后进行计算。two 是高位,one是低位。所以 高位+低位,转十六进制。
Long result = Long.parseLong(two + one, 16);
再来看处理二进制数据。还是按照低位在前,高位在后,进入拼接转换成十六进制,并转换成二级制数据。
String one = ((char) one1 + String.valueOf((char) one2));
String two = ((char) one3 + String.valueOf((char) one4));
String result = Integer.toBinaryString(Integer.parseInt(two + one, 16));
十六进制粘包如何处理呢?
首先写一个成员变量,用于积累缓冲区
// 用于累积数据的缓冲区
private StringBuffer cumulationBuffer = new StringBuffer();
方案就是,接收到数据之后,判断是否结尾,假设是 55 结尾表示完整数据。
判断字符串,是否以 55 结尾,如果是表示完整数据,需要与累计的缓冲区合并转换成 bytebuf,并清除缓冲区。
否则将当前字符串赋值给缓冲区。
通过Unpooled.wrappedBuffer(hexDump.getBytes())
将十六进制字符串转换成 bytebuf
try {
// 将数据转换成十六进制
String hexDump = ByteBufUtil.hexDump(msg).toUpperCase();
log.info("接收到数据:{}", hexDump);
ByteBuf buf = Unpooled.wrappedBuffer(hexDump.getBytes());
if (hexDump.endsWith("55")) {
log.info("表示------入库数据");
if (cumulationBuffer.length() > 0) {
hexDump = cumulationBuffer + hexDump;
}
ByteBuf buffer = Unpooled.wrappedBuffer(hexDump.getBytes());
// 进入处理数据
decode(buffer);
cumulationBuffer = new StringBuffer();
} else {
log.info("表示------半包数据");
cumulationBuffer.append(hexDump);
}
buf.release();
} finally {
msg.release();
}
Netty 多客户端实现方案
目的:通过以下方式,Netty 客户端可以不断尝试连接到服务器,直到成功为止。同时,通过 ExecutorService 提交任务,可以使连接操作在单独的线程中执行,避免阻塞主线程。
@Slf4j
public class WSServer {
// Netty客户端的引导类,用于配置和启动客户端
private final Bootstrap bootstrap;
/**
* 构造函数,初始化Bootstrap并提交任务到ExecutorService
*
* @param executorService 用于提交连接任务的ExecutorService
* @param port 服务器端口
* @param acu 包含服务器IP和其他配置信息的ACU对象
*/
public WSServer(ExecutorService executorService, Integer port, ACU acu) {
bootstrap = new Bootstrap();
// 配置Bootstrap
bootstrap.group(new NioEventLoopGroup()) // 设置EventLoopGroup处理所有的I/O操作
.channel(NioSocketChannel.class) // 指定通道类型为NioSocketChannel
.handler(new WSServerInitializer(acu)); // 设置通道的处理器
// 提交连接任务到ExecutorService
executorService.submit(() -> getWsServer(acu.getIp(), port));
}
/**
* 尝试连接到WebSocket服务器,并在失败时重试
*
* @param ip 服务器IP
* @param port 服务器端口
*/
public void getWsServer(String ip, int port) {
while (true) {
try {
// 尝试启动连接
start(ip, port);
break; // 如果连接成功,跳出循环
} catch (Exception exception) {
// 连接失败时记录错误信息并重试
log.error("netty...连接--{}--{}---失败", ip, port);
log.error("{}---原因:{}", ip, exception.toString());
try {
// 等待30秒后重试
Thread.sleep(30000);
} catch (InterruptedException ignored) {
}
}
}
}
/**
* 启动连接到指定的IP和端口
*
* @param ip 服务器IP
* @param port 服务器端口
* @throws InterruptedException 如果连接过程中线程被中断
*/
public void start(String ip, int port) throws InterruptedException {
// 连接到服务器并等待连接完成
bootstrap.connect(ip, port).sync().channel();
// 连接成功,记录日志
log.info("netty...连接--{}--{}---成功", ip, port);
}
}
将连接交给Spring 管理,可以初始化多个Netty 客户端,方便在应用程序的其他部分中进行依赖注入。同时,通过使用配置属性类 DataProperties,可以灵活地从外部配置文件中读取必要的属性值,使得配置更加灵活和可维护。
@Configuration
public class WSServerConfig {
private final ExecutorService executorService = Executors.newFixedThreadPool(5);
@Autowired
private DataProperties dataProperties;
@Bean(value = "wsServer-1-1")
public WSServer wsServer() {
ACU acu = new ACU(1, 1, 5, 1, dataProperties.getIp1(),
dataProperties.getData1(), 1);
return new WSServer(executorService, dataProperties.getPort(), acu);
}
}
ModbusTCP 和 acu
这是两种协议,acu 整合多个数据源,形成点表,我们使用tcp去发送指定的遥信与遥控可以获取数据和控制。
CRC 校验,首先我们需要发送带校验的指令
010410000074
通过 CRC工具获取校验值 EDF4,那发送的命令就变成了,010410000074F4ED,寄存器数量为什么 * 2 呢,因为一个寄存器数量 = 2 个字节。一个数据是 4 个字节,58 * 4 ,也等于 58 * 2 个寄存器。
具体命令是什么意思呢?
- 01:地址
- 04:功能码
- 10 00:寄存器地址
- 00 74:寄存器数量(58 * 2 = 116)(116 十六进制 = 74)
- F4:校验值
- ED:校验值
接收到数据:
01 04 E8 29 5C 8F 3D CC CC 4C 3D 29 5C 8F 3D 33 33 A7 41 00 00 00 00 00 00 00 00 00 00 00 00 67 66 D6 41 00 00 92 42 00 00 80 3F 00 00 80 3F 00 00 80 3F 00 00 00 00 00 BC B7 46 00 00 E8 41 00 00 16 46 00 00 00 00 00 00 00 40 00 00 80 3F 00 00 80 3F 00 00 00 00 00 F6 B6 46 00 00 F0 41 00 00 16 46 00 00 00 00 00 00 40 40 00 00 80 3F 00 00 80 3F 00 00 00 00 00 06 B9 46 00 00 F0 41 00 00 16 46 00 00 00 00 00 00 80 40 00 00 80 3F 00 00 80 3F 00 00 00 00 00 1A B6 46 00 00 F0 41 00 00 16 46 00 00 00 00 00 00 A0 40 00 00 80 3F 00 00 80 3F 00 00 00 00 00 DA B8 46 00 00 E8 41 00 00 16 46 00 00 00 00 00 00 C0 40 00 00 80 3F 00 00 80 3F 00 00 00 00 00 E8 B7 46 00 00 0C 42 00 00 16 46 00 00 00 00 CD CC 26 42 9A 8D
具体命令是什么意思呢?
- 01:地址
- 04:功能码
- 5C:可读字节数
- 后面所有表示数据区
- F4:校验值
- ED:校验值
CRC 校验
需要把上面的字符串,从 0 ~ 长度 - 4,因为我们不需要最后四位校验,校验后得到的结果,与最后四位进行比较,一致校验成功。
// 开始处理 CRC
String calculatedCRC = NettyUtil.calculateCRC16(hexDump.substring(0, hexDump.length() - 4));
Assert.isTrue(calculatedCRC.equals(hexDump.substring(hexDump.length() - 4)), "crc 校验错误!");
数据是怎么解的呢?
数据说明:数据为32位浮点数,解码顺序4321;
00 00 C0 41 为 24
String c1 = String.valueOf((char) '0') + (char) '0';
String c3 = String.valueOf((char) '0') + (char) '0';
String c5 = String.valueOf((char) 'C') + (char) '0';
String c7 = String.valueOf((char) '4') + (char) '1';
// 翻转后:41C00000
String s = c7 + c5 + c3 + c1;
// 将十六进制字符串转换成无符号整数:1103101952
int intBits = Integer.parseUnsignedInt(s, 16);
// 用于将整数类型的二进制位表示转换为对应的浮点数值
float v = Float.intBitsToFloat(intBits);
具体数据是如何对应呢,假设我们获取了点表。点表对应的是那个服务器的地址。我的101点表对应的是 101 的服务器数据。
这样一来,我拿到的第一个数据就对应第一个,第二个就对应第二个。
自制的工具类
public class NettyUtil {
/**
* 拼接字符串
*/
public static String get16StringTo2(byte one1, byte one2) {
return ((char) one1 + String.valueOf((char) one2)).trim();
}
/**
* 转换一个字节
* 根据两个字节,将十六进制转成十进制
*/
public static Long readInDataInt(ByteBuf in) {
return Long.parseLong(((char) in.readByte()) + String.valueOf((char) in.readByte()), 16);
}
/**
* 低位在前,高位在后,拼接,转十进制
*/
public static Long readInDataIntTo4(ByteBuf in) {
String intTo1 = readInDataStringTo2(in);
String intTo2 = readInDataStringTo2(in);
return Long.parseLong(intTo2 + intTo1, 16);
}
/**
* 低位在前,高位在后,拼接,转十进制
*/
public static Double readInDataStringTo4(ByteBuf in) {
String intTo1 = readInDataStringTo2(in);
String intTo2 = readInDataStringTo2(in);
return Integer.parseInt(intTo2 + intTo1, 16) / 2.0;
}
/**
* 低位在前,高位在后,拼接
*/
public static String readInDataStringTo2(ByteBuf in) {
return NettyUtil.get16StringTo2(in.readByte(), in.readByte());
}
/**
* 校验数据
*/
public static boolean isValidHex(ByteBuf in, byte[] bytes) {
// 确保有足够的数据来读取前8个字节
if (in.readableBytes() < 16) {
return false;
}
byte[] dst = new byte[16];
in.readBytes(dst);
// 校验前8个字节是否匹配
for (int i = 0; i < 16; i++) {
if (dst[i] != bytes[i]) {
// 不匹配
return false;
}
}
return true;
}
/**
* 校验数据
*/
public static boolean isValidHex(ByteBuf in, byte[] bytes, boolean flag) {
if (!flag) in = in.copy();
boolean validHex = isValidHex(in, bytes);
if (!flag) in.release();
return validHex;
}
// 读取四位字节,返回一个字符串
private static String readFourDigits(ByteBuf byteBuf) {
String c1 = String.valueOf((char) byteBuf.readByte()) + (char) byteBuf.readByte();
String c3 = String.valueOf((char) byteBuf.readByte()) + (char) byteBuf.readByte();
String c5 = String.valueOf((char) byteBuf.readByte()) + (char) byteBuf.readByte();
String c7 = String.valueOf((char) byteBuf.readByte()) + (char) byteBuf.readByte();
return c7 + c5 + c3 + c1;
}
/**
* 将十六进制字符串转换为32位浮点型
*/
public static float hexToFloat(ByteBuf buf) {
String digits = readFourDigits(buf);
int intBits = Integer.parseUnsignedInt(digits, 16);
return Float.intBitsToFloat(intBits);
}
/**
* crc 校验使用
*/
public static String calculateCRC16(String hexDump) {
// 字符串转十六进制字节数组
byte[] bytes = ByteBufUtil.decodeHexDump(hexDump);
// crc 校验
int crc = 0xFFFF;
for (byte b : bytes) {
crc ^= b & 0xFF;
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
String format = String.format("%04X", crc);
return format.substring(2, 4) + format.substring(0, 2);
}
}