1.Netty的业务场景
平台主要需求是和硬件(铁路小车安装的硬件)对接,并定时对设备进行监控检查,需要使用Netty作为通信中间件来监听端口,小车上的硬件通过TCP连接向服务端发送指令,后台主要是通过netty的ChannelHandler来实现对硬件数据的接收和处理。我的项目是把接收到的数据存入数据库,当然也可以放到缓存中。由于我也是刚开始接触netty,所以直接上代码,至于原理请自行百度。
2. Netty的主要组件
2.1 Channel
Channel作为Netty网络通信的主体,可以看作是通讯的载体,主要有三个状态:打开、关闭、连接。
Channel主要的IO操作:读(read)、写(write)、连接(connect)、绑定(bind),均为异步,也就是说在调用如上方法后,并不保证IO操作完成,但会在IO操作成功、失败或取消后,生成相应的记录保存在一个凭证中并返回。
3.下面把项目代码贴出来:
spring boot项目netty依赖
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.0.Final</version>
</dependency>
3.1 Netty服务 服务启动监听器
package com.htkj.netty.server;
import com.htkj.netty.handler.DecoderHandler;
import com.htkj.netty.handler.ServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.Charset;
/**
* Netty服务 服务启动监听器
* @author zl
* @date 2019-12-11
*
*/
@Component
public class NettyServer {
private static Logger logger = LoggerFactory.getLogger(ServerHandler.class);
@Value("${netty.port}")
private int port;
public static ServerSocketChannel serverSocketChannel;
@Autowired
private ServerHandler serverHandler;
public void start() throws Exception {
// 连接处理group
EventLoopGroup boss = new NioEventLoopGroup();
// 事件处理group
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();//1.创建ServerBootStrap实例
// 绑定处理group
bootstrap.group(boss, worker)//2.设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
.channel(NioServerSocketChannel.class)//3.设置并绑定服务端的channel
// 保持连接数
.option(ChannelOption.SO_BACKLOG, 1024)
// 有数据立即发送
.option(ChannelOption.TCP_NODELAY, true)
// 保持连接
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 处理新连接
.childHandler(new ChannelInitializer<SocketChannel>() {//设置了客户端连接socket属性。
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 增加任务处理
ChannelPipeline p = sc.pipeline();
p.addLast(new DecoderHandler(), // 自定义解码器
//默认的编码器
new StringEncoder(Charset.forName("utf-8")),
new StringDecoder(Charset.forName("utf-8")),
// 自定义的处理器
// new ServerHandler()
serverHandler);
}
});
// 绑定端口,同步等待成功
ChannelFuture future;
try {
logger.info("netty服务器在[{}]端口启动监听",port);
future = bootstrap.bind(port).sync();//真正让netty跑起来的重点
if (future.isSuccess()) {
serverSocketChannel = (ServerSocketChannel) future.channel();
logger.info("netty服务开启成功");
} else {
logger.info("netty服务开启失败");
}
// 等待服务监听端口关闭,就是由于这里会将线程阻塞,导致无法发送信息,所以我这里开了线程
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 优雅地退出,释放线程池资源
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
3.2 自定义解码器
package com.htkj.netty.handler;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义解码器
*/
public class DecoderHandler extends ByteToMessageDecoder {
private static Logger logger = LoggerFactory.getLogger(ServerHandler.class);
private static Map<ChannelHandlerContext, String> msgBufMap = new ConcurrentHashMap<>();
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
byte[] data = new byte[in.readableBytes()];
in.readBytes(data);
String msg = new String(data, Charset.forName("utf-8"));
// 处理粘包拆包问题
if (msg.startsWith("#")) {
if (msg.endsWith("#")) {
out.add(msg);
} else {
msgBufMap.put(ctx, msg);
}
} else if (msg.endsWith("#") && msgBufMap.containsKey(ctx)) {
msg = msgBufMap.get(ctx) + msg.split("#")[0];
out.add(msg);
msgBufMap.remove(ctx);
}
}
}
3.3 自定义服务端处理器(业务逻辑在此处进行处理,里面包含netty的生命周期)
package com.htkj.netty.handler;
import com.alibaba.druid.util.StringUtils;
import com.htkj.netty.mapper.CarMapper;
import com.htkj.netty.pojo.Car;
import com.htkj.netty.pojo.GisLocation;
import com.htkj.netty.pojo.Monitor;
import com.htkj.netty.pojo.ParameterRecord;
import com.htkj.netty.pojo.SysLog;
import com.htkj.netty.server.GisLocationService;
import com.htkj.netty.server.MonitorService;
import com.htkj.netty.server.ParameterRecordService;
import com.htkj.netty.server.SysLogService;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
/**
* 自定义服务端处理器
*/
@Component
public class ServerHandler extends ChannelInboundHandlerAdapter {
private static Logger logger = LoggerFactory.getLogger(ServerHandler.class);
@Autowired
private GisLocationService gisLocationService;
@Autowired
private MonitorService monitorService;
@Autowired
private SysLogService sysLogService;
@Autowired
private CarMapper carMapper;
@Autowired
private ParameterRecordService parameterRecordService;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 在与客户端的连接已经建立之后将被调用
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("netty客户端与服务端连接开始...");
}
/**
* 当从客户端接收到一个消息时被调用
* msg 就是硬件传送过来的数据信息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
this.getGisLocation(msg.toString()); //这是下面自己写的业务逻辑处理的方法
}
/**
* 客户端与服务端断开连接时调用
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("netty客户端与服务端连接关闭...");
}
/**
* 服务端接收客户端发送过来的数据结束之后调用
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
logger.info("信息接收完毕...");
}
/**
* 在处理过程中引发异常时被调用
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
System.out.println("异常信息:rn " + cause.getMessage());
}
/**
* 获取定位数据逻辑
*
* @param msg
* @return
*/
@Transactional(rollbackFor = Exception.class)
private void getGisLocation(String msg) {
String[] msgArr = msg.replace("#", "").split(",");
if ("CMD-T".equals(msgArr[1]) || "CMD-F".equals(msgArr[1])) {
GisLocation gisLocation = new GisLocation();
gisLocation.setDevCode(msgArr[0]);
if ("A".equals(msgArr[2])) {
if(!StringUtils.isEmpty(msgArr[5]) && !StringUtils.isEmpty(msgArr[6])) {
gisLocation.setLatitude(
msgArr[5].substring(msgArr[5].indexOf(":") + 1, msgArr[5].length() - 1));
gisLocation.setLongitude(
msgArr[6].substring(msgArr[6].indexOf(":") + 1, msgArr[6].length() - 1));
gisLocation.setSpeed(Double.parseDouble(
msgArr[7].substring(msgArr[7].indexOf(":") + 1)));
gisLocation.setDirection(Integer.parseInt(msgArr[9]));
gisLocation.setLatitudeFlag(String.valueOf(msgArr[5].charAt(msgArr[5].length() - 1)));
gisLocation.setLongitudeFlag(String.valueOf(msgArr[6].charAt(msgArr[6].length() - 1)));
gisLocation.setType("GPS");
// TODO 后续逻辑处理
gisLocationService.save(gisLocation);
}
}
} else if ("ALM-I".equals(msgArr[1])) {
Monitor monitor = new Monitor();
monitor.setDevCode(msgArr[0]);
monitor.setStatus(msgArr[2]);
// TODO 后续逻辑处理
monitorService.save(monitor);
List<Car> carList=carMapper.selectByHardwareMac(msgArr[0]);
if(CollectionUtils.isEmpty(carList)) {
String content="该车未与硬件MAC值进行绑定/绑定有误,请及时处理";
SysLog sysLog=new SysLog(1,content);
sysLogService.save(sysLog);
}else {
List<GisLocation> glList=gisLocationService.lambdaQuery().eq(GisLocation::getDevCode, msgArr[0]).orderByDesc(GisLocation::getAddTime).list();
//给小车赋值
if(!CollectionUtils.isEmpty(glList)) {
carList.get(0).setLatitude(glList.get(0).getLatitude());
carList.get(0).setLongitude(glList.get(0).getLongitude());
carMapper.updateById(carList.get(0));
}
//给台账赋值
List<ParameterRecord> prLi=parameterRecordService.lambdaQuery().eq(ParameterRecord::getCarId, carList.get(0).getId())
.ne(ParameterRecord::getStatus, "4").orderByAsc(ParameterRecord::getCreateTime).list();
if("ON".equals(msgArr[2])) {//上道
if(!CollectionUtils.isEmpty(prLi) && prLi.size()>1) {
if("1".equals(prLi.get(0).getStatus())) {
if(!CollectionUtils.isEmpty(glList)) {
prLi.get(0).setStatus("2");
prLi.get(0).setUpTime(sdf.format(new Date()));
prLi.get(0).setLongitude(glList.get(0).getLongitude());
prLi.get(0).setLatitude(glList.get(0).getLatitude());
parameterRecordService.lambdaUpdate().update(prLi.get(0));
}
}
}else {
List<ParameterRecord> prList=parameterRecordService.lambdaQuery().eq(ParameterRecord::getCarId, carList.get(0).getId())
.eq(ParameterRecord::getStatus, "1").orderByAsc(ParameterRecord::getCreateTime).list();
if(!CollectionUtils.isEmpty(prList) && !CollectionUtils.isEmpty(glList)) {
prList.get(0).setStatus("2");
prList.get(0).setUpTime(sdf.format(new Date()));
prList.get(0).setLongitude(glList.get(0).getLongitude());
prList.get(0).setLatitude(glList.get(0).getLatitude());
parameterRecordService.lambdaUpdate().update(prList.get(0));
}
}
}else {//下道
if(!CollectionUtils.isEmpty(prLi) && prLi.size()>1) {
if("2".equals(prLi.get(0).getStatus())) {
if(!CollectionUtils.isEmpty(glList)) {
prLi.get(0).setStatus("3");
prLi.get(0).setDownTime(sdf.format(new Date()));
prLi.get(0).setDownLongitude(glList.get(0).getLongitude());
prLi.get(0).setDownLatitude(glList.get(0).getLatitude());
parameterRecordService.lambdaUpdate().update(prLi.get(0));
}
}
}else {
List<ParameterRecord> prList=parameterRecordService.lambdaQuery().eq(ParameterRecord::getCarId, carList.get(0).getId())
.eq(ParameterRecord::getStatus, "2").orderByAsc(ParameterRecord::getCreateTime).list();
if(!CollectionUtils.isEmpty(prList) && !CollectionUtils.isEmpty(glList)) {
prList.get(0).setStatus("3");
prList.get(0).setDownTime(sdf.format(new Date()));
prList.get(0).setDownLongitude(glList.get(0).getLongitude());
prList.get(0).setDownLatitude(glList.get(0).getLatitude());
parameterRecordService.lambdaUpdate().update(prList.get(0));
}
}
}
}
}
}
}
3.4 springboot项目 启动类(实现CommandLineRunner,里面的run()方法启动netty服务的监听器)
package com.htkj.netty;
import com.htkj.netty.server.NettyServer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
@MapperScan("com.htkj.netty.mapper") // 启动时扫描的mapper
public class NettyApplication implements CommandLineRunner {
@Autowired
NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(NettyApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
nettyServer.start();
}
}
4.展示运行效果
4.1 网络调试助手模拟硬件(客户端)发送数据
4.2 项目开启之后打印的日志
4.3 发送一次数据,关闭服务,看下日志
ok!!!
补充:在项目进行实测阶段,连接不上,报错为:
io.netty.channel.ChannelPipelineException: com.htkj.netty.handler.ServerHandler is not a @Sharable h
问题描述:引用的Netty版本为4.x,NettyServerHandler实例是通过@AutoWired注入的;
问题解决方法:在处理类NettyServerHandler前加入注解@Sharable,问题就解决了。
@Component
@Sharable
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
@Autowired
private AdminService adminService;