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 网络调试助手模拟硬件(客户端)发送数据

java netty客户端高并发连接 java netty连接硬件_spring

4.2 项目开启之后打印的日志 

java netty客户端高并发连接 java netty连接硬件_java_02

4.3 发送一次数据,关闭服务,看下日志

java netty客户端高并发连接 java netty连接硬件_java netty客户端高并发连接_03

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;