场景
使用netty和设备进行tcp通讯,设备通过配置ip地址和端口,写到设备里面,设备就会通过这个地址发送注册、心跳以及一些应答数据,首先得去看设备那边的文档,看它使用的消息格式是什么
首先引入netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
编写服务类
需要在springboot启动成功后启动,那么需要实现ApplicationRunner或者CommandLineRunner接口,进行启动,在销毁时需要关闭服务
/**
* @Description
* @Date 2024/4/22 15:58
* @Author tom
**/
@Component
@Slf4j
public class LoraServer implements ApplicationRunner {
private final EventLoopGroup boss = new NioEventLoopGroup();
private final EventLoopGroup worker = new NioEventLoopGroup();
private Channel channel;
@Value("${sf.netty.host}")
private String host;
@Value("${sf.netty.port}")
private int port;
@Override
public void run(ApplicationArguments args) throws Exception {
try {
// 启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置参数,组配置
serverBootstrap.group(boss, worker)
// 指定channel
.channel(NioServerSocketChannel.class)
// 初始化服务端可连接队列
.option(ChannelOption.SO_BACKLOG, 1024)
// 允许重复使用本地地址和端口,连接关闭后,可以立即重用端口
.option(ChannelOption.SO_REUSEADDR, true)
// 设置TCP长连接,TCP会主动探测空闲连接的有效性
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 禁用Nagle算法,小数据时可以即时传输
.childOption(ChannelOption.TCP_NODELAY, true)
// 发送缓冲区大小
.childOption(ChannelOption.SO_SNDBUF, 256 * 1024)
// 接收缓冲区大小
.childOption(ChannelOption.SO_RCVBUF, 256 * 1024)
// Netty服务端channel初始化
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
// 添加心跳检测,如果60s不在认定为下线
channel.pipeline().addLast(new IdleStateHandler(60,-1,-1, TimeUnit.SECONDS));
// 根据长度进行分包
channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.wrappedBuffer(new byte[]{0x0D})));
channel.pipeline().addLast("logging",new LoggingHandler("INFO"));
// channel.pipeline().addLast("loraServerDecoder",new LoraServerDecoder());
//自定义编码解码器
channel.pipeline().addLast("loraServerEncoder",new LoraServerEncoder());
channel.pipeline().addLast("loraServerHandler",new LoraServerHandler());
}
});
// 绑定端口,开始接收进来的连接
ChannelFuture future = serverBootstrap.bind(port).sync();
if (future.isSuccess()) {
log.info("Netty服务端启动!! 端口:[{}]", port);
}
channel = future.channel();
} catch (Exception e) {
log.error("Netty服务端启动异常!! error:{}", e.getMessage());
}
}
@PreDestroy
public void destroy() {
if (channel != null) {
channel.close();
}
worker.shutdownGracefully();
boss.shutdownGracefully();
log.info("Netty服務端关闭成功");
}
}
发送时需要对数据进行编码,这里是根据满足对方的格式需要去维护
@Component
@Slf4j
public class LoraServerEncoder extends MessageToByteEncoder<LoraSendPacket> {
@Override
protected void encode(ChannelHandlerContext ctx, LoraSendPacket packet, ByteBuf byteBuf) throws Exception {
// 发送
if (packet.getCommandCode() == COMMAND_CODE_SEND) {
// 固定协议头
byteBuf.writeByte(packet.getHead());
// 包序号
byteBuf.writeByte((byte)packet.getSortNo());
byte[] ipaddr = getAddressHex(packet.getAddress());
// ip
byteBuf.writeBytes(ipaddr);
// 命令码
byteBuf.writeByte(packet.getCommandCode());
// 发送数据
if (StrUtil.isNotBlank(packet.getData())) {
byte[] bytes = packet.getData().getBytes("GBK");
byteBuf.writeByte(bytes.length);
byteBuf.writeBytes(bytes);
// crc
byteBuf.writeByte(calcCrc(packet.getHead(),packet.getSortNo(),ipaddr,packet.getCommandCode(),bytes.length,bytes));
}
byte[] response = new byte[byteBuf.readableBytes()];
int i = 0;
while (byteBuf.readableBytes() > 0) {
response[i]= byteBuf.readByte();
i++;
}
// log.info("发送数据:{}", HexUtil.encodeHexStr(response));
log.info("channelId:【{}】 发送数据 数据 [{}]", ctx.channel().id(), HexUtil.encodeHexStr(response));
byteBuf.resetReaderIndex();
}
// 发送清除命令
if (packet.getCommandCode() == COMMAND_CODE_CLEAR) {
//5A 00 FF FF FF FF E6 01 00 3D;
byteBuf.writeByte(0x5A);
byteBuf.writeByte(0x00);
byteBuf.writeByte(0xFF);
byteBuf.writeByte(0xFF);
byteBuf.writeByte(0xFF);
byteBuf.writeByte(0xFF);
byteBuf.writeByte(0xE6);
byteBuf.writeByte(0x01);
byteBuf.writeByte(0x00);
byteBuf.writeByte(0x3D);
log.info("channelId:【{}】 发送清除命令", ctx.channel().id());
}
}
/**
* 前面所有数字和 % 256 的 余数
* @return
*/
private int calcCrc(int head, int sortNo, byte[] ipaddr, int commandCode, int length, byte[] bytes) {
int i = 0;
for (byte b : ipaddr) {
i += b;
}
i += commandCode;
i += length;
for (byte aByte : bytes) {
i += aByte;
}
return (head + sortNo + i) % 256;
}
private byte[] getAddressHex(String address) {
String[] split = address.split("\\.");
byte[] bytes = new byte[4];
for (int i = 0; i < split.length; i++) {
bytes[i] = (byte) Integer.parseInt(split[i]);
}
return bytes;
}
}
下面就是处理具体的逻辑,当它进行TCP连接后把连接存起来,方便业务上去发送数据
@Slf4j
@Component
@ChannelHandler.Sharable
public class LoraServerHandler extends ChannelInboundHandlerAdapter {
// 管理一个全局map,保存连接进服务端的通道
public static final Map<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 当客户端主动连接服务端,通道活跃后触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = inetSocketAddress.getAddress().getHostAddress();
int clientPort = inetSocketAddress.getPort();
// 获取连接通道唯一标识
ChannelId channelId = ctx.channel().id();
// 如果map中不包含此连接,就保存连接
if (CHANNEL_MAP.containsKey(channelId)) {
log.info("客户端【{}】是连接状态,连接通道数量:{}", channelId, CHANNEL_MAP.size());
} else {
// 保存连接
CHANNEL_MAP.put(channelId, ctx);
log.info("客户端【{}】连接Netty服务端!![clientIp:{} clientPort:{}]", channelId, clientIp, clientPort);
log.info("连接通道数量:{}", CHANNEL_MAP.size());
// CLIENT_MAP.put(clientIp + clientPort,new LoraClient(clientIp,clientPort));
}
}
/**
* 当客户端主动断开连接,通道不活跃触发
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = inetSocketAddress.getAddress().getHostAddress();
int clientPort = inetSocketAddress.getPort();
// 获取终止连接的客户端ID
ChannelId channelId = ctx.channel().id();
// 包含此客户端才去删除
if (CHANNEL_MAP.containsKey(channelId)) {
// 删除连接
CHANNEL_MAP.remove(channelId);
log.warn("客户端【{}】断开Netty连接!![clientIp:{} clientPort:{}]", channelId, clientIp, clientPort);
log.info("连接通道数量:{}", CHANNEL_MAP.size());
}
}
/**
* 通道有消息触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// 报文解析处理
ByteBuf byteBuf = (ByteBuf) msg;
byte head = byteBuf.getByte(0);
if (head == BEAT) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
log.info("channelId:【{}】 收到心跳[clientIp:{} clientPort:{}]", ctx.channel().id(), inetSocketAddress.getAddress(),inetSocketAddress.getPort());
}
if (head == HEAD) {
byte[] response = new byte[byteBuf.readableBytes()];
int i = 0;
while (byteBuf.readableBytes() > 0) {
response[i]= byteBuf.readByte();
i++;
}
log.info("channelId:【{}】 收到应答 数据 [{}]", ctx.channel().id(), HexUtil.encodeHexStr(response));
byteBuf.resetReaderIndex();
handleReceive(ctx,byteBuf);
}
// ctx.writeAndFlush(Unpooled.copiedBuffer("服务端响应: " + msg + "\n", CharsetUtil.UTF_8));
// 发布自定义Netty数据包处理事件
// applicationEventPublisher.publishEvent();
} catch (Exception e) {
log.error("channelId:【{}】 报文解析失败!! msg:{} error:{}", ctx.channel().id(), msg.toString(), e.getMessage());
}
}
/**
* 包头:5A 固定
* 包序号:是应答包的序号,也就是当前显示数据的包序号。
* 地址 ID:手表地址 ID
* 命令码:0XF1 固定
* 数据长度:0X01
* 键值:下翻键:0X31,上翻键:0X32,确认键:0X33
* 检验:校验和,CRC 前面所有数据之和除 256 的余数。
* 结束符:0X0D
* @param ctx
* @param byteBuf
*/
private void handleReceive(ChannelHandlerContext ctx, ByteBuf byteBuf) {
byte head = byteBuf.getByte(0); // 应答头
byte pkgNo = byteBuf.getByte(1); // 应答包号
byte ip1 = byteBuf.getByte(2);
byte ip2 = byteBuf.getByte(3);
byte ip3 = byteBuf.getByte(4);
byte ip4 = byteBuf.getByte(5);
byte cmd = byteBuf.getByte(6); // 命令
byte len = byteBuf.getByte(7); // 长度
byte data = byteBuf.getByte(8); // 数据
byte end = byteBuf.getByte(9); // 结束符
if (data == PRESS_CONFIRM) {
// 处理应答消息的事件
applicationEventPublisher.publishEvent(new LoraReceiveEvent(ctx, pkgNo, ip1, ip2, ip3, ip4, cmd, len, data, end));
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
String socketString = ctx.channel().remoteAddress().toString();
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
log.warn("Client: 【{}】 READER_IDLE 读超时", socketString);
CHANNEL_MAP.remove(ctx.channel().id());
ctx.close();
} else if (event.state() == IdleState.WRITER_IDLE) {
log.warn("Client: 【{}】 WRITER_IDLE 写超时", socketString);
CHANNEL_MAP.remove(ctx.channel().id());
ctx.close();
} else if (event.state() == IdleState.ALL_IDLE) {
log.warn("Client: 【{}】 ALL_IDLE 读/写超时", socketString);
CHANNEL_MAP.remove(ctx.channel().id());
ctx.close();
}
}
}
/**
* 当连接发生异常时触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("channelId:【{}】 发生异常!! error:{}", ctx.channel().id(), cause.getMessage());
CHANNEL_MAP.remove(ctx.channel().id());
// 当出现异常就关闭连接
ctx.close();
}
@PreDestroy
public void destroy() {
if (!CHANNEL_MAP.isEmpty()) {
CHANNEL_MAP.forEach((k,v)->{
v.close();
log.info("Netty服务端销毁 {}",k);
});
}
}
public void sendMessage(LoraSendPacket loraSendPacket) {
if (loraSendPacket.getCommandCode() == COMMAND_CODE_SEND) {
Long increment = stringRedisTemplate.opsForValue().increment("iot:lora:sortNo");
assert increment != null;
if (increment.intValue() > 0xFF) {
increment = 0L;
stringRedisTemplate.opsForValue().set("iot:lora:sortNo", "0");
}
loraSendPacket.setSortNo(increment.intValue());
CHANNEL_MAP.forEach((k,v)->{
v.channel().writeAndFlush(loraSendPacket);
});
}
if (loraSendPacket.getCommandCode() == COMMAND_CODE_CLEAR) {
stringRedisTemplate.opsForValue().set("iot:lora:sortNo", "0");
CHANNEL_MAP.forEach((k,v)->{
v.channel().writeAndFlush(loraSendPacket);
});
}
}
}