SpringBoot整合Netty服务端在Tomcat运行实坑经历

SpringBoot整合Netty

公司是做人工智能人脸识别技术落地的,上周确定要做设备的远程控制,android端推荐使用netty,如是坑开始了~~~

第1天
netty服务的代码网上已经烂大街了,在此不赘述,但是!!!
netty服务搭建后无法保持长连接,客户端发完消息就断开连接

……

第1+n天
android大哥看不下去了
因为我用的netty4,遂推荐我使用5,本地测试没问题,整合至springBoot的时候出现版本冲突,无法解决(ps:最后发现项目用了redis,底层应该用了netty通讯),netty5卒

又回到4上,结果大哥的netty代码毫无问题,就给我用了,整合至本地没问题,废话不多说贴代码
server:

@Component
public class EchoServer implements onChannelOperation {
    @Value("${netty.port}")
    private  int port;
    private static final Logger log= LoggerFactory.getLogger(EchoServer.class);
    private EventLoopGroup bossGroup = new NioEventLoopGroup();
    private EventLoopGroup workerGroup = new NioEventLoopGroup();
    //public List socketList = new ArrayList();
    private static final ConcurrentHashMap channelMap= new ConcurrentHashMap<>();
    
    public void run() {
     /*   EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于处理服务器端接收客户端连接
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // 进行网络通信(读写)*/
        try {
            Thread t=Thread.currentThread();
           log.info("run() in EchoServer"+ Calendar.getInstance().getTime()+"___"+t.getName());
            ServerBootstrap bootstrap = new ServerBootstrap(); // 辅助工具类,用于服务器通道的一系列配置
            bootstrap.group(bossGroup, workerGroup) // 绑定两个线程组, 绑定线程池
                    .channel(NioServerSocketChannel.class) // 指定NIO的模式
                    .childHandler(new ChannelInitializer() { // 配置具体的数据处理方式
                        @Override // 这个方法里,连接一个客户端,进入一次,连接一个客户端进入一次
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            log.info("有客户端连接了:" + socketChannel);
                            // 设置超时时间,可选
                            // socketChannel.pipeline().addLast(new IdleStateHandler(READ_IDEL_TIME_OUT,
                            //  WRITE_IDEL_TIME_OUT, ALL_IDEL_TIME_OUT, TimeUnit.SECONDS));
                            NettyServerHandler scobj = new NettyServerHandler(EchoServer.this); //设置监听
                            socketChannel.pipeline().addLast(scobj);
                            //socketList.add(scobj);
                            channelMap.put(scobj,socketChannel);
                            log.info("socket通道数量:" + "--"+channelMap.size());
                        }
                    })
                  
                    .option(ChannelOption.SO_BACKLOG, 32 * 1024) // 设置TCP缓冲区
                    .option(ChannelOption.SO_SNDBUF, 64 * 1024) // 设置发送数据缓冲大小
                    .option(ChannelOption.SO_RCVBUF, 64 * 1024) // 设置接受数据缓冲大小
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // true保持连接, false no
            //.childOption(ChannelOption.ALLOW_HALF_CLOSURE, true); // 允许半关闭socket即可。默认为false,客户端shutdownoutput时,SocketChannel.read(..)
            ChannelFuture future = bootstrap.bind(port).sync();
           log.info("服务器启动成功,监听端口号:" + port);
            future.channel().closeFuture().sync();// 关闭服务器通道
        } catch (Exception e) {
            e.printStackTrace();
           log.info("服务器启动失败,监听端口号:" + port);
        } finally {
            workerGroup.shutdownGracefully();// 释放线程池资源
            bossGroup.shutdownGracefully();// 释放线程池资源
           log.info("服务器启动失败,监听端口号:" + port);
        }
    }

    @PreDestroy
    public void destroy() {
        log.info("正在尝试关闭 Netty");
        bossGroup.shutdownGracefully().syncUninterruptibly();
        workerGroup.shutdownGracefully().syncUninterruptibly();
        channelMap.clear();
        log.info("关闭成功");
    }

    @Override
    public void onRemoveChannel(NettyServerHandler obj) {
        log.info("移除链接!!!!");
        channelMap.remove(obj);//移除该链接
    }

    // 根据设备ID发送数据
    public void sendDataAPI(String equipId,String msgType, String sendData) {
        NettyServerHandler cnobj = getSocketHandler(equipId);
        if (cnobj != null) {
            log.info("已匹配通道,准备发送,剩余通道数:"+channelMap.size());
            cnobj.sendDataAPI(equipId, sendData);
            if(msgType.equals(SocketMsgType.REBOOT)){
                log.info("设备重启了,移除该链接");
                channelMap.remove(cnobj);
            }
        }else   log.info("没有匹配通道,消息无法发送,剩余通道数:"+channelMap.size());
    }

    // 获取发送对象socket
    public NettyServerHandler getSocketHandler(String equipId) {
        /*if (socketList == null || socketList.size() <= 0) return null;
        for (NettyServerHandler cnobj : socketList) {
            if (cnobj.getEquipId().equals(equipId)) {
                return cnobj;
            }
        }*/
        if(channelMap.size()<1)return  null;
        for (NettyServerHandler key : channelMap.keySet()) {
           if(key.getEquipId().equals(equipId)){
               return key;
           }
        }
        return null;
    }
    // 检测选择的机器车是否在线 true 在线,可以发送信息给它
    public boolean socketIsActive(String equipId) {
        /*for (NettyServerHandler cnobj : socketList) {
            if (cnobj.getEquipId().equals(equipId)) {
                return true;
            }
        }*/
       for(Map.Entry entry:channelMap.entrySet()){
           if (entry.getKey().getEquipId().equals(equipId)){
               return  true;
           }
       }
        return false;
    }

}

接下来是:Handler 包含了部分消息的处理逻辑,业务代码已略去

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log= LoggerFactory.getLogger(NettyServerHandler.class);
    /** 空闲次数 */
    private AtomicInteger idle_count = new AtomicInteger(1);
    /** 发送次数 */
    private AtomicInteger count = new AtomicInteger(1);
    //在普通类里获取spring管理的bean,可以借助componet注解类做中继
    private ApplicationContext applicationContext= SpringUtils.getApplicationContext();
    //注入service层代码
    private ***service ***service=applicationContext.getBean(***service.class);
    /**
     * 建立连接时,发送一条消息
     */
    @Autowired
    private onChannelOperation mListener;
    @Autowired
    public NettyServerHandler(onChannelOperation mListener) {
        this.mListener = mListener;
    }

    private ChannelHandlerContext channelHanlder;
    private String equipId;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // ctx.writeAndFlush(user);
        super.channelActive(ctx);
    }

    /**
     * channel失效处理,客户端下线或者强制退出等任何情况都触发这个方法
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        mListener.onRemoveChannel(this); // 移除通道
        log.info("捕获异常,通道为:" + ctx.channel().remoteAddress());
        super.channelInactive(ctx);
    }
    /**
     * 超时处理 如果5秒没有接受客户端的心跳,就触发; 如果超过两次,则直接关闭;
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
        if (obj instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) obj;
            // 如果读通道处于空闲状态,说明没有接收到心跳命令
            if (IdleState.READER_IDLE.equals(event.state())) {
                // log.info("已经5秒没有接收到客户端的信息了");
                if (idle_count.get() > 1) {
                    //  log.info("关闭这个不活跃的channel");
                    ctx.channel().close();
                }
                idle_count.getAndIncrement();
            }
        } else {
            super.userEventTriggered(ctx, obj);
        }
    }
    /**
     * 业务逻辑处理
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("接收到客户端数据了: " + msg);
        // do something msg
        ByteBuf buf = (ByteBuf) msg;
        byte[] data = new byte[buf.readableBytes()];
        buf.readBytes(data);
        //RestTemplate restTemplate=new RestTemplate();
        String request = new String(data, "utf-8");
        log.info("request:" + request);
        //解析json
        JSONObject jsonObject=null;
        try {
            String end=request.substring(request.length()-1,request.length());
            if(!end.equals("}")){
                log.info("error msg!!!!"+end.toCharArray());
                return;
            }
            jsonObject = new JSONObject(request);

            log.info("解析完成:"+jsonObject.toString());
            String msgType=jsonObject.get("msgType").toString();
            log.info("msgType:"+msgType);
            String dataMsg=jsonObject.get("data").toString();
            String dataMap="";
          //这里可以写业务代码
        }catch (Exception e){
            log.info("请注意,报文异常!");
            e.printStackTrace();
        }
        dealData(ctx, request);

        //count.getAndIncrement();
    }
    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        mListener.onRemoveChannel(this);
        ctx.close();
    }

    private void dealData(ChannelHandlerContext ctx, String msg) {
        try {
            channelHanlder = ctx; // 更新通道信息
            JSONObject js = new JSONObject(msg);
            String msgType = js.getString("msgType");
            equipId = js.getString("equipId");
            log.info("收到设备(" + equipId + ")发回的数据:" + msg);
            // 模拟发送数据
            if (msgType.equals("200002")) // 心跳数据原数据返回去
                sendDataAPI(equipId, js.toString());
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    /**
     *
     * @param equipId 设备ID
     * @param sendData 要发送的内容 JSON格式
     */
    public void sendDataAPI(String equipId, String sendData) {
        if (channelHanlder !=null) {
            channelHanlder.writeAndFlush(Unpooled.copiedBuffer(sendData.getBytes()));
            log.info("往设备(" + equipId + ")发送了数据:" + sendData);
        }
    }

    public String getEquipId() {
        return equipId;
    }

    public void setEquipId(String equipId) {
        this.equipId = equipId;
    }

    public ChannelHandlerContext getChannelHanlder() {
        return channelHanlder;
    }

    public void setChannelHanlder(ChannelHandlerContext channelHanlder) {
        this.channelHanlder = channelHanlder;
    }

    // 检测再NettyServer是否需要新增此对象
    public interface onChannelOperation {
        public void onRemoveChannel(NettyServerHandler obj);
    }

}

其中组件:SpringUtils 来源自网络,其实注入有很多方式,只做参考

@Component
public class SpringUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtils.applicationContext == null){
            SpringUtils.applicationContext  = applicationContext;
        }
    }
    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    //通过name获取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }
    //通过class获取Bean.
    public static  T getBean(Class clazz){
        return getApplicationContext().getBean(clazz);
    }
    //通过name,以及Clazz返回指定的Bean
    public static  T getBean(String name,Class clazz){
        return getApplicationContext().getBean(name, clazz);
    }

}

最后是springBoot本地启动

public class App extends SpringBootServletInitializer     implements CommandLineRunner{
  //测试的时候 App需要实现CommandLineRunner 并重写run方法  其他重复代码和注解已省略
@Override
    public void run(String... args) throws Exception {
        new Thread(()->nettyServer.run()).run();
    }
}

但是,重点来了,这么写放到tomcat上不行滴!!!!

又加了监听器,如下

@WebListener
public class NettyServerListener implements //ApplicationListener
   ServletContextListener
{
    private static final Logger log= LoggerFactory.getLogger(NettyServerListener.class);
    private  EchoServer echoServer= null;
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("tomcat is going start");
        WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
        log.info("echoServer is ready");
        echoServer = (EchoServer) context.getBean("echoServer");
         //echoServer= SpringUtils.getApplicationContext().getBean(EchoServer.class);
        Thread t=Thread.currentThread();
        log.info("run() of netty"+ Calendar.getInstance().getTime()+"___"+t.getName()+"  flag:"+(null==echoServer));

    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("tomcat is deading");
        log.info("destroy() of netty"+ Calendar.getInstance().getTime()+"  flag:"+(null==echoServer));
        new Thread(()->echoServer.destroy()).run();
    }
}

事实证明new Thread的方式不好使,加上其他错误,在这里我耽误了三天(浪费了我的周末时间),甚至想到给服务器加cpu核心的方式,当然要审批就作废了,最后在网上有位无名大哥点到要用线程池启动,就试了下,幸福来的无比突然,我都已经准备好自裁了……下面是完整的Listener

@WebListener
public class NettyServerListener implements //ApplicationListener
   ServletContextListener
{
    private static final Logger log= LoggerFactory.getLogger(NettyServerListener.class);
    private ExecutorService webSocketSinglePool;
    private  EchoServer echoServer= null;
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("tomcat is going start");
        WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
        log.info("echoServer is ready");
        echoServer = (EchoServer) context.getBean("echoServer");
         //echoServer= SpringUtils.getApplicationContext().getBean(EchoServer.class);
        Thread t=Thread.currentThread();
        log.info("run() of netty"+ Calendar.getInstance().getTime()+"___"+t.getName()+"  flag:"+(null==echoServer));
        webSocketSinglePool.execute(() -> {
            try {
                log.info("running......");
                echoServer.run();
            } catch (Exception e) {
                log.error("webSocket listen and serve error.", e);
            }
        //在这儿启动netty会阻塞tomcat
       // new Thread(()->echoServer.run()).run();
           });
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("tomcat is deading");
        log.info("destroy() of netty"+ Calendar.getInstance().getTime()+"  flag:"+(null==echoServer));
        new Thread(()->echoServer.destroy()).run();
    }

    @PostConstruct
    public  void setup() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("webSocketSinglePool-%d").build();
        webSocketSinglePool = new ThreadPoolExecutor(1, 1, 0L,
                TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024),
                namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
        log.info("webSocketSinglePool init.");
    }

  /*  @Override
    public void onApplicationEvent(ContextStartedEvent event) {
        log.info("监听到事件了");
        runWebSocketServer(event.getApplicationContext());
    }*/

    private void runWebSocketServer(ApplicationContext applicationContext) {
        final EchoServer echoServer = applicationContext.getBean(EchoServer.class);
       new Thread(() -> {
            try { //开始启动netty服务
                log.info("ready to start Netty");
                echoServer.run();
            } catch (Exception e) {
                log.error("webSocket listen and serve error.", e);
            }
        }).run();
    }
    @PreDestroy
    public void  cleanup(){
        webSocketSinglePool.shutdown();
        log.info("webSocketSinglePool destroyed.");
    }

}

至此netty服务成功发布,耗时一周左右,吃一堑长一智

你可能感兴趣的:(SpringBoot整合Netty服务端在Tomcat运行实坑经历)