helvar国外的一家灯设备公司,协议是tcp,格式是他们自己定义的
#发送的消息,格式是>开头,#结尾。命令大概意思是C14修改灯光亮度L亮度100F过度时间1秒@后面是设备编号
>V:2,C:14,L:100,F:100,@0.50.1.63.1#
#返回消息 ?号正常、!号异常,
?V:2,C:14,L:100,F:100,@0.50.1.63.1#
!V:2,C:14,L:100,F:100,@0.50.1.63.1#
服务端客服端沾包拆包有些类似都是以*V开头#结尾进行的
看代码,这是需要的pom
<!--工具-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.5</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<!-- <version>1.18.28</version>-->
</dependency>
模拟器tcpserver
package com.example.haverserver;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author lanyanhua
* @date 2023/5/25 10:14 AM
* @description
*/
@Slf4j
@Component
public class TcpServer implements ApplicationRunner {
public static void main(String[] args) throws InterruptedException {
new TcpServer().start();
}
@Override
public void run(ApplicationArguments args) throws Exception {
new Thread(() -> {
try {
start();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
/**
* 启动tcp服务
*/
public void start() throws InterruptedException {
// 创建两个事件循环组,一个用于接收客户端的连接,另一个用于处理I/O操作
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 添加处理器
ch.pipeline()
.addLast(new StringEncoder()
, new MyStringDecoder()
, new NettyServerHandler());
// .addLast(new LineBasedFrameDecoder(1024, true, true))
// .addLast(new NettyServerHandler());
}
});
// 监听并启动服务器
int port = 8888;
ChannelFuture future = bootstrap.bind(port).sync();
// 阻塞等待服务器关闭
future.channel().closeFuture().sync();
} finally {
// 关闭事件循环组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
@Slf4j
class NettyServerHandler extends SimpleChannelInboundHandler<String> {
public static Map<String, Data> dataMap = new ConcurrentHashMap<>(100);
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("Connected to server: " + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("Failed to handle request: " + cause.getMessage());
ctx.close();
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("拆包后Message received: " + msg);
//推送mq执行宏
try {
if (msg.contains("C:100@")) {
ctx.writeAndFlush(msg.replace(">V", "?V").replace("#", "=1,2,3,4,5#"));
return;
}
String json = msg.replace(">", "{").replace("#", "}").replace("@", "DC:");
JSONObject obj = JSONUtil.parseObj(json);
String v = "";
//亮度、色温、颜色
String l = obj.getStr("L");
String m = obj.getStr("K");
String cx = obj.getStr("CX");
String cy = obj.getStr("CY");
//设备编号
String deviceCode = obj.getStr("DC");
Data data;
switch (obj.getInt("C")) {
//调试场景
case 11:
case 12:
//块、场景
Integer b = obj.getInt("B");
Integer s = obj.getInt("S");
data = dataMap.get(b + "_" + s);
if (data == null) {
dataMap.put(deviceCode, new Data(l,
(!StringUtils.hasLength(m) && !StringUtils.hasText(cx) ? "2700" : m)
, cx, cy));
return;
}
data.set(l, m, cx, cy);
return;
//控制亮度、色温、颜色
case 14:
data = dataMap.get(deviceCode);
if (data == null) {
dataMap.put(deviceCode, new Data(l,
(!StringUtils.hasLength(m) && !StringUtils.hasText(cx) ? "2700" : m)
, cx, cy));
return;
}
data.set(l, m, cx, cy);
return;
//状态
case 110:
v = "0";
break;
//获取亮度
case 152:
data = dataMap.get(deviceCode);
if (data == null) {
dataMap.put(deviceCode, new Data("0", "2700", null, null));
v = "0";
} else {
v = data.l == null ? "0" : data.l;
}
break;
//获取色温、颜色
case 157:
//?V:2,C:157,@0.213.1.1=M:270#
//?V:2,C:157,@0.213.1.2=CX:0.209991,CY:0.679993#
data = dataMap.get(deviceCode);
System.out.println(deviceCode + ":" + data);
if (data == null) {
data = new Data("0", "2700", null, null);
dataMap.put(deviceCode, data);
}
System.out.println(deviceCode + ":" + data);
v = data.m == null ? ("CX:" + data.cx + ",CY:" + data.cy) : "M:" + data.m;
break;
//修改场景
case 202:
default:
return;
}
String send = msg.replace(">V", "?V").replace("#", "=" + v + "#");
System.out.println("回复:" + send);
ctx.writeAndFlush(send);
} catch (Exception e) {
log.error("解析失败:" + e);
}
}
}
@ToString
class Data {
public String l;
public String m;
public String cx;
public String cy;
public Data(String l, String m, String cx, String cy) {
this.l = l;
setM(m);
this.cx = cx;
this.cy = cy;
}
public void set(String l, String m, String cx, String cy) {
if (l != null) {
this.l = l;
}
if (m != null) {
setM(m);
this.cx = null;
this.cy = null;
}
if (cx != null) {
this.cx = cx;
this.m = null;
}
if (cy != null) {
this.cy = cy;
this.m = null;
}
}
public void setM(String m) {
if (m != null) {
this.m = String.valueOf(1000000 / Integer.parseInt(m));
}
}
}
class MyStringDecoder extends StringDecoder {
private final Charset charset = StandardCharsets.UTF_8;
// 用来临时保留没有处理过的请求报文
ByteBuf tempMsg = Unpooled.buffer();
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("Connected to server: " + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("Failed to handle request: " + cause.getMessage());
ctx.close();
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf message, List<Object> out) {
System.out.println("decode Message received: " + message.toString(charset));
//推送mq执行宏
// ctx.writeAndFlush("test");
// 合并报文
ByteBuf in;
int tmpMsgSize = tempMsg.readableBytes();
// 如果暂存有上一次余下的请求报文,则合并
if (tmpMsgSize > 0) {
in = Unpooled.buffer();
in.writeBytes(tempMsg);
in.writeBytes(message);
System.out.println("合并:上一数据包余下的长度为:" + tmpMsgSize + ",合并后长度为:" + in.readableBytes());
} else {
in = message;
}
try {
int i = in.readableBytes();
String string = in.toString(this.charset);
System.out.println("开始处理沾包拆包:::" + string);
if (i < 4) {
return;
}
while (true) {
System.out.println(1);
int length = ">V".length();
//判断是否有开始的命令
int startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
Unpooled.wrappedBuffer(">V'".getBytes(charset)).readByte());
if (startIdx < 0 && (startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
Unpooled.wrappedBuffer("!V'".getBytes(charset)).readByte())) < 0) {
return;
}
//没有结尾消息值 结束
if (in.indexOf(startIdx + length, in.writerIndex(),
Unpooled.wrappedBuffer("#".getBytes(charset)).readByte()) < 0) {
//设置为下一条消息的开始位置
// 多余的报文存起来
// 第一个报文: i+ 暂存
// 第二个报文: 1 与第一次
int size = in.readableBytes();
if (size != 0) {
System.out.println("多余的数据长度:" + size);
// 剩下来的数据放到tempMsg暂存
tempMsg.clear();
tempMsg.writeBytes(in.readBytes(size));
}
return;
}
//将下一个开始的命令作为截取的索引。
int endIdx = in.indexOf(startIdx + length, in.writerIndex(),
Unpooled.wrappedBuffer(">V".getBytes(charset)).readByte());
if (endIdx == -1 && (endIdx = in.indexOf(startIdx + length, in.writerIndex(),
Unpooled.wrappedBuffer("!V".getBytes(charset)).readByte())) == -1) {
//没有时可以直接解码当前所有
super.decode(ctx, in, out);
return;
}
in.readerIndex(startIdx);
byte[] bytes = new byte[endIdx - startIdx];
in.readBytes(bytes);
//设置为下一条消息的开始位置
in.skipBytes(endIdx - in.readerIndex());
//解码字符串添加到了 out 列表中
String s = new String(bytes, charset);
out.add(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
下面是客户端调用
package com.example.haverserver;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* @author lanyanhua
* @date 2023/5/18 10:12 AM
* @description
*/
public class HelvarNetProtocol {
private static final Map<String, Consumer<String>> messageMap = new ConcurrentHashMap<>();
private final Channel channel;
private final EventLoopGroup group;
public boolean singleton = false;
public HelvarNetProtocol(String addr, int port, int timeout) throws Exception {
try {
group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_TIMEOUT, timeout)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// ByteBuf[] delimiters = {Unpooled.wrappedBuffer("#".getBytes())
// , Unpooled.wrappedBuffer("!".getBytes())
// , Unpooled.wrappedBuffer("?".getBytes())
//// , Unpooled.wrappedBuffer("#!".getBytes())
//// , Unpooled.wrappedBuffer("#?".getBytes())
// };
ch.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8),
// new StringDecoder(),
new MyStringDecoder1(StandardCharsets.UTF_8),
new ClientHandler())
;
// .addLast("frameDecoder", new DelimiterBasedFrameDecoder(8192
// , delimiters));
}
});
ChannelFuture channelFuture = bootstrap.connect(addr, port).sync();
channel = channelFuture.channel();
} catch (Exception e) {
singleton = false;
close1();
throw new Exception("连接创建失败:" + e.getMessage());
}
}
public static void main(String[] args) throws Exception {
HelvarNetProtocol localhost = new HelvarNetProtocol("192.168.0.181", 8888, 500);
// HelvarNetProtocol localhost = new HelvarNetProtocol("localhost", 8888, 50000);
localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
localhost.sendCommand(">V:1,C:110#");
localhost.sendCommand(">V:1,C:104#");
Scanner scanner = new Scanner(System.in);
new Thread(() -> {
localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
for (int i = 0; i < 100; i++) {
localhost.sendCommand(">V:1,ID:1704410508190887938,C:152,@0.181.1.1#");
}
localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
}).start();
for (int i = 0; i < 100; i++) {
localhost.sendCommand(">V:1,ID:1704410508190887938,C:152,@0.181.1.1#");
}
localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
while (scanner.hasNext()) {
String ipt = scanner.next();
localhost.sendCommand(ipt);
}
}
public void sendCommand(String command) {
System.out.println("Message sent: " + command);
// 使用发送请求
channel.writeAndFlush(command);
}
public void close1() {
if (singleton) {
return;
}
//不用关闭
if (channel != null) {
channel.close().syncUninterruptibly(); // 关闭Channel对象
}
if (group != null) {
group.shutdownGracefully(); // 关闭EventLoopGroup对象
}
}
public String name() {
return "tcp";
}
public static class ClientHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("Connected to server: " + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("Failed to handle request: " + cause.getMessage());
ctx.close();
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("Message received: " + msg);
}
}
}
class MyStringDecoder1 extends StringDecoder {
private final Charset charset;
// 用来临时保留没有处理过的请求报文
ByteBuf tempMsg = Unpooled.buffer();
public MyStringDecoder1(Charset charset) {
this.charset = charset;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf message, List<Object> out) {
// 合并报文
ByteBuf in;
int tmpMsgSize = tempMsg.readableBytes();
// 如果暂存有上一次余下的请求报文,则合并
if (tmpMsgSize > 0) {
in = Unpooled.buffer();
in.writeBytes(tempMsg);
in.writeBytes(message);
System.out.println("合并:上一数据包余下的长度为:" + tmpMsgSize + ",合并后长度为:" + in.readableBytes());
} else {
in = message;
}
try {
int i = in.readableBytes();
String string = in.toString(this.charset);
System.out.println("开始处理沾包拆包:::" + string);
if (i < 4) {
return;
}
while (true) {
int length = "?V".length();
//判断是否有开始的命令
int startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
Unpooled.wrappedBuffer("?V'".getBytes(charset)).readByte());
if (startIdx < 0 && (startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
Unpooled.wrappedBuffer("!V'".getBytes(charset)).readByte())) < 0) {
return;
}
//没有结尾消息值 结束
if (in.indexOf(startIdx + length, in.writerIndex(),
Unpooled.wrappedBuffer("#".getBytes(charset)).readByte()) < 0) {
//设置为下一条消息的开始位置
// 多余的报文存起来
// 第一个报文: i+ 暂存
// 第二个报文: 1 与第一次
int size = in.readableBytes();
if (size != 0) {
System.out.println("多余的数据长度:" + size);
// 剩下来的数据放到tempMsg暂存
tempMsg.clear();
tempMsg.writeBytes(in.readBytes(size));
}
return;
}
//将下一个开始的命令作为截取的索引。
int endIdx = in.indexOf(startIdx + length, in.writerIndex(),
Unpooled.wrappedBuffer("?V".getBytes(charset)).readByte());
if (endIdx == -1 && (endIdx = in.indexOf(startIdx + length, in.writerIndex(),
Unpooled.wrappedBuffer("!V".getBytes(charset)).readByte())) == -1) {
//没有时可以直接解码当前所有
super.decode(ctx, in, out);
return;
}
in.readerIndex(startIdx);
byte[] bytes = new byte[endIdx - startIdx];
in.readBytes(bytes);
//设置为下一条消息的开始位置
in.skipBytes(endIdx - in.readerIndex());
//解码字符串添加到了 out 列表中
String s = new String(bytes, charset);
out.add(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
踩过的坑:
1.线程未关闭导致内存异常
客户端HelvarNetProtocol在创建时netty是在构造函数中进行创建的由于连接失败异常进入catch模块,但是EventLoopGroup group对象是已经初始化过了的没有关闭group线程,实现AutoCloseable接口的close方法是在外面try(HelvarNetProtocol protocol = new HelvarNetProtocol())的方式实现,实实在在漏掉了异常的关闭线程情况。
2.客户端解析返回的消息代码抛出异常导致当前客户端对象卡死
ClientHandler.messageReceived是服务端返回的消息中,通常我们都需要在这里进行下一步操作,解析参数下发指令等等,当这里出现异常时这个客户端就会卡死,这里估计是我代码有问题还没研究透。估计是exceptionCaught方法中ctx.close();关闭了通道。最好时再messageReceived中做好异常处理