Springboot整合Netty

该博客介绍了如何配置Netty服务端和客户端,包括创建NioEventLoopGroup线程组,设置编码解码器,初始化ChannelInitializer,以及处理类NettyServerHandler。服务端通过NettyServer启动并监听指定端口,客户端使用Bootstrap连接服务端,并实现心跳检测和断线重连机制。
摘要由CSDN通过智能技术生成

配置Netty服务端

package com.chalk.netty.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

/**
 * Netty配置类
 */
@Component
@Order(100)
public class NettyServer {

    private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);

    public void start(InetSocketAddress address) {
        /* 配置服务端的NIO线程组 */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)//绑定线程池
                    .channel(NioServerSocketChannel.class)
                    .localAddress(address)
                    .childHandler(new NettyServerChannelInitializer())//编码解码
                    .option(ChannelOption.SO_BACKLOG, 128)//服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
                    .childOption(ChannelOption.SO_KEEPALIVE, true);//保持长连接,2小时无数据激活心跳机制

            // 绑定端口,开始接收进来的连接 / 绑定对应ip和端口,同步等待成功
            ChannelFuture future = bootstrap.bind(address).sync();
            logger.info("Server start listen at " + address.getPort());

            //关闭channel和块,直到它被关闭 / 等待服务端监听端口关闭
            future.channel().closeFuture().sync();

            System.out.println("--------------------"+Thread.currentThread().getName()+"--------------------------");

        } catch (Exception e) {
            e.printStackTrace();
//            bossGroup.shutdownGracefully();
//            workerGroup.shutdownGracefully();
//            logger.info("Server Closed:{}", e.getMessage());
        }
        finally {
            //优雅退出,释放 NIO 线程组
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

配置Netty从客户端获取、发送数据的编码格式

package com.chalk.netty.server;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

/**
 *
 * 配置Netty从客户端获取、发送数据的编码格式
 *
 */
public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        //获取客户端数据的编码格式
        channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
        //发送给客户端数据的编码格式
        channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
        //添加Netty处理类
        channel.pipeline().addLast(new NettyServerHandler());
    }
}

添加Netty处理类

package com.chalk.netty.server;

import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Netty处理类
 */
@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 管理一个全局map,保存连接进服务端的通道数量
     */
    private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();

    /**
     * 所有的channel
     */
    private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 存储设备号与channel的Map
     */
    private static Map<String, Channel> channelMap = new ConcurrentHashMap<>();

    /**
     * 存储channel与设备号的Map
     */
    private static Map<Channel, String> channelReverseMap = new ConcurrentHashMap<>();

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        channels.add(incoming);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        channels.remove(incoming);
    }

    /**
     * 服务端监听到客户端活动 / 只要有客户端连接就进行这个方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        log.info("ChatClient:{},在线", incoming.remoteAddress());

        /* ------ */
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = insocket.getAddress().getHostAddress();
        int clientPort = insocket.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("客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
        /* ------ */
    }

    /**
     * 服务端监听到客户端不活动 / 客户端终止连接就进入
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        /* ------ */
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = insocket.getAddress().getHostAddress();
       //获取终止连接的客户端ID
        ChannelId channelId = ctx.channel().id();
        //包含此客户端才去删除
        if (CHANNEL_MAP.containsKey(channelId)) {
            //删除连接
            CHANNEL_MAP.remove(channelId);
            System.out.println();
            log.info("客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
        /* ------ */
//        Channel incoming = ctx.channel();
//        channels.remove(incoming);
//        String key = channelReverseMap.get(incoming);
//        if (StringUtils.isNotBlank(key)) {
//            channelMap.remove(key);
//        }
//        channelReverseMap.remove(incoming);
//        log.info("ChatClient:{},掉线", incoming.remoteAddress());
    }

    /**
     * @param ctx
     * @DESCRIPTION: 有客户端发消息会触发此函数
     * @return: void
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        log.info("加载客户端报文......");
        log.info("【" + ctx.channel().id() + "】" + " :" + msg);
        Channel incoming = ctx.channel();
        log.info("From Client:{}, msg:{}", incoming.remoteAddress(), msg.toString());

        //以下可以做获取客户端数据后返回
        //响应客户端
        this.channelWrite(ctx.channel().id(), msg);

        // 消息处理
//        messageManage(incoming, msg.toString());
    }

    /**
     * @param msg  需要发送的消息内容
     * @param channelId 连接通道唯一id
     * @DESCRIPTION: 服务端给客户端发送消息
     * @return: void
     */
    public void channelWrite(ChannelId channelId, Object msg) throws Exception {

        ChannelHandlerContext ctx = CHANNEL_MAP.get(channelId);

        if (ctx == null) {
            log.info("通道【" + channelId + "】不存在");
            return;
        }
        if (msg == null && msg == "") {
            log.info("服务端响应空的消息");
            return;
        }
        log.info("服务端响应------------------------------------------");
        //将客户端的信息直接返回写入ctx
        ctx.write(msg);
        //刷新缓存区
        ctx.flush();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        String socketString = ctx.channel().remoteAddress().toString();
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("Client: " + socketString + " READER_IDLE 读超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("Client: " + socketString + " WRITER_IDLE 写超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Client: " + socketString + " ALL_IDLE 总超时");
                ctx.disconnect();
            }
        }
    }

    /**
     * 连接错误时调用
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Channel incoming = ctx.channel();
        log.info("ChatClient:{},异常", incoming.remoteAddress());
        // 当出现异常就关闭连接
        cause.printStackTrace();
        ctx.close();
        log.info(ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + CHANNEL_MAP.size());
    }

}

配置Netty客户端

package com.chalk.netty.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;

import java.util.concurrent.TimeUnit;

/**
 * @ClassName: NettyClinet
 * @Author: gluoh
 * @Date: 2021/02/24 15:29
 * @Description:
 */
@Slf4j
public class NettyClient {


    @Value("${printer.server.host}")
    private String host;
    @Value("${printer.server.port}")
    private int port;

    private static Channel channel;

    public NettyClient(){

    }

    public NettyClient(String host, int port) {
        this.host = host;
        this.port = port;
    }



    public  void start()  {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .option(ChannelOption.SO_KEEPALIVE,true)
                    .channel(NioSocketChannel.class)
                    .handler(new NettyClientChannelInitializer(host,port));

            ChannelFuture f = b.connect(host,port).sync();

            //断线重连
            f.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (!channelFuture.isSuccess()) {
                        final EventLoop loop = channelFuture.channel().eventLoop();
                        loop.schedule(new Runnable() {
                            @Override
                            public void run() {
                                log.error("服务端链接不上,开始重连操作...");
                                System.err.println("服务端链接不上,开始重连操作...");
                                start();
                            }
                        }, 1L, TimeUnit.SECONDS);
                    } else {
                        channel = channelFuture.channel();
                        log.info("服务端链接成功...");
                        System.err.println("服务端链接成功...");
                    }
                }
            });

        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new NettyClient("127.0.0.1",7005).start();
    }
}
package com.chalk.netty.client;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

/**
 * @ClassName: ClientChannelInitializer
 * @Author: gluoh
 * @Date: 2021/02/24 15:29
 * @Description:
 */
public class NettyClientChannelInitializer extends ChannelInitializer<SocketChannel> {

    private String host;
    private int port;

    public NettyClientChannelInitializer(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers
                .weakCachingConcurrentResolver(this.getClass().getClassLoader())));
        pipeline.addLast("encoder", new ObjectEncoder());
        //心跳检测
        pipeline.addLast(new IdleStateHandler(0,5,0,TimeUnit.SECONDS));
        //客户端的逻辑
        pipeline.addLast("handler", new NettyClientHandler(host,port));

    }
}
package com.chalk.netty.client;


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoop;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @ClassName: NettyClientHandler
 * @Author: gluoh
 * @Date: 2021/02/24 15:29
 * @Description:
 */
@Slf4j
public class NettyClientHandler extends SimpleChannelInboundHandler {

    private String host;
    private int port;
    private NettyClient nettyClient;


    public NettyClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        nettyClient = new NettyClient(host,port);
    }


    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o)
            throws Exception {
        String response = "{\"data_id\":\"123\",\"message\":\"已接收请求\",\"gatename\":\"入口\",\"gateid\":\"1\",\"service\":\"setopenstrobe\",\"result_code\":0}";
        System.out.println(response);
        if (o.toString().startsWith("<")){
            log.info("response:{}", response);
            channelHandlerContext.writeAndFlush(response);
        }
        System.out.println("Server say : " + o.toString());;
    }


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("通道已连接!!");
        String login = "{\"service\":\"checkKey\",\"parkid\":\"301019120373\",\"parkkey\":\"C80FB9B8-73E8-4C03-B300-2037F14F42C6\"}";
        log.info("login:{}", login);
        ctx.channel().writeAndFlush(login);
        super.channelActive(ctx);
    }

    /**
     * @param ctx
     * @DESCRIPTION: 有服务端端终止连接服务器会触发此函数
     * @return: void
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("断线了。。。。。。");
        //使用过程中断线重连
        final EventLoop eventLoop = ctx.channel().eventLoop();
        eventLoop.schedule(new Runnable() {
            @Override
            public void run() {
                nettyClient.start();
            }
        }, 1, TimeUnit.SECONDS);

        ctx.fireChannelInactive();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
            throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state().equals(IdleState.READER_IDLE)) {
                System.out.println("READER_IDLE");

            } else if (event.state().equals(IdleState.WRITER_IDLE)) {
                /**发送心跳,保持长连接*/
                String heart =  new String("{\"service\":\"heartbeat\",\"parkid\":\"301019120373\",\"time\":\"2020-03-30 16:26:18\"}");
                log.info("heart:{}", heart);
                ctx.channel().writeAndFlush(heart);
                log.debug("心跳发送成功!");
                System.out.println("心跳发送成功!");
            } else if (event.state().equals(IdleState.ALL_IDLE)) {
                System.out.println("ALL_IDLE");
            }
        }
        super.userEventTriggered(ctx, evt);
    }

}

yml配置

netty:
  port: 7005
  # 公司测试私有IP
  # url: 
  url: 127.0.0.1
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值