1.建立springboot项目(只建接收端)
在handler文件夹下新建EPServerHandler.java
在service下新建EPServer.java
在utils下新建HJ212MsgUtils工具类处理数据
2.项目代码
EPServerHandler.java
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class EPServerHandler extends ChannelInboundHandlerAdapter{
/**
* 定义一个HashMap,用于保存所有的channel和设备ID的对应关系。
*/
private static Map deviceInfo = new HashMap(64);
/**
* 消息读取
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
log.info("=============" + (new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date()) + "=============");
log.info("收到" + ctx.channel().id() + "设备消息 ===> " + msg);
// 解析物联网设备发来的数据
//JSONObject data = HJ212MsgUtils.dealMsg1((String) msg);
//System.out.println(data);
//String msg1=data.toJSONString();
//System.out.println(msg);
// log.info("数据库插入结果:"+res);
//log.info("设备消息解析JSON结果:" + data.toJSONString());
}
/***
* 超时关闭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) {
log.error("========= 链接出现异常!exceptionCaught.");
ctx.fireExceptionCaught(cause);
// 将通道从deviceInfo中删除
deviceInfo.remove(ctx.channel().id());
// 关闭通道链接,节省资源
ctx.channel().close();
}
/**
* 每加入一个新的链接,保存该通道并写入上线日志。该方法在channelRead方法之前执行
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
// 获取设备ID
String deviceId = "abc";
// 将该链接保存到deviceInfo
deviceInfo.put(ctx.channel().id(), deviceId);
log.warn("========= " + ctx.channel().id() + "设备加入链接。");
}
/**
* 每去除一个新的链接,去除该通道并写入下线日志
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
// 将通道从deviceInfo中删除
deviceInfo.remove(ctx.channel().id());
log.warn("========= " + ctx.channel().id() + "设备断开链接。");
}
}
EPServer.java
import com.system.office.office_system.handler.EPServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @Describe 环保设备对接程序启动类,Netty服务器端启动类,接收设备上传的数据。
* 重写ApplicationRunner为系统启动类
*/
@Slf4j
@Component
@Order(1)
@EnableAsync
public class EPServer implements ApplicationRunner {
/**
* Netty服务端监听的端口号
*/
public static int PORT=1008;
/**
* 分发线程组
*/
private static final EventLoopGroup bossGroup = new NioEventLoopGroup(2);
/**
* 工作线程组
*/
private static final EventLoopGroup workerGroup = new NioEventLoopGroup(4);
/**
* 启动服务
*/
public static void runEPServer(){
log.info("===== EP Netty Server start =====");
try{
ServerBootstrap b = new ServerBootstrap();
// WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, true,6553600);
//创建事件循环组
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("serverDecoder", new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast("serverEncoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("frame-aggregator", new WebSocketFrameAggregator(10 * 1024 * 1024));
// netty提供了空闲状态监测处理器 0表示禁用事件
pipeline.addLast(new IdleStateHandler(65,0,0, TimeUnit.MINUTES));
pipeline.addLast(new EPServerHandler());
}
});
log.info("环保 Netty Server PORT = " + PORT);
b.bind(PORT).sync();
}catch (Exception e){
e.printStackTrace();
shutdown();
}
}
/**
* 关闭服务
*/
public static void shutdown(){
// 优雅关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
log.info("已关闭 Netty Server");
}
@Override
public void run(ApplicationArguments args) {
// 启动环保监测Netty服务端
runEPServer();
}
}
HJ212MsgUtils.java
package com.system.office.office_system.utils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HJ212MsgUtils {
public static JSONObject dealMsg1(String msg) {
JSONObject data = new JSONObject();
try {
// 拆分消息
String[] subMsg = msg.split("&&");
// 清洗消息头基本数据
String headStr = subMsg[0].substring(2).replace(";CP=", "").replace("=", "\":\"")
.replace(",", "\",\"").replace(";", "\",\"");
data.put("SIZE", headStr.substring(0, 4));
data.putAll(JSONObject.parseObject("{\"" + headStr.substring(4) + "\"}"));
// 清洗数据体基本数据
String paramStr = subMsg[1].replace("=", "\":\"").replace(",", "\",\"")
.replace(";", "\",\"");
data.put("CP", JSONObject.parseObject("{\"" + paramStr + "\"}"));
// 保存消息尾数据,主要是CRC校验和包结束符
data.put("End", subMsg[2]);
} catch (Exception e) {
log.error("HJ212数据转JSON错误。报错信息:{},消息内容:{}", e.getMessage(), msg);
e.printStackTrace();
}
return data;
}
}
3.出现的问题及解决方案
hj212-2017协议中的数据太长会导致TCP出现粘包或拆包现象
原因:TCP作为底层协议不了解上层业务具体逻辑,所以是否为一个完整的包,是按照TCP缓冲区处理逻辑来划分的
解决方案
通过hj212-2017官方文档
发现包尾固定回车换行符,于是可以添加一个回车换行符解码器,Netty也为我们写了LineBasedFrameDecodere 类专门处理此类情况
在EPServer中添加回车换行符解码器即可
//添加一个回车换行符解码器(当数据解读的长度达到最大846时,仍没有看到回车换行,抛出异常)
pipeline.addLast(new LineBasedFrameDecoder(12+834));
pipeline.addLast("serverDecoder", new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast("serverEncoder", new StringEncoder(CharsetUtil.UTF_8));