【实现netty服务同时监听多个端口,处理多套协议】

Netty

netty一个提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
在上一篇文章中介绍了Netty怎么去实现和硬件双工通信:https://blog.csdn.net/GBK_8/article/details/123256716?spm=1001.2014.3001.5502

1. 实现Netty服务端

@Slf4j
@Component
@RefreshScope
public class NettyServerOfLims {
    /**
     * 端口参数,配置与配置文件中具体参数类型如:
     * netty:
     *   port: {6001: A, 6002: B, 6003: C}
     * Netty服务启动后获取到配置参数,监听配置相对应的端口
     */
    @Autowired
    private PortDefinition portDefinition;

    ChannelFuture future = null;

    NioEventLoopGroup boss = null;

    NioEventLoopGroup worker = null;

    ServerBootstrap bootstrap = new ServerBootstrap();

    public void start() {
        boss = new NioEventLoopGroup();
        worker = new NioEventLoopGroup();
        //1.绑定group
        bootstrap.group(boss, worker)
                //2.设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
                .channel(NioServerSocketChannel.class)
                //3.保存连接数
                .option(ChannelOption.SO_BACKLOG, 1024)
                //4.有数据立即发送
                //.option(ChannelOption.SO_SNDBUF, 4 * 1024)
                //.option(ChannelOption.SO_RCVBUF, 4 * 1024)
                .handler(new LoggingHandler(LogLevel.INFO))
                //5.保持连接
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                //6.handler作用于boss -- childHandler作用于worker
                .childHandler(new SocketChannelInitHandler(portDefinition.getPort()));

        Map<Integer, String> ports = portDefinition.getPort();
        log.info("netty服务器在{}端口启动监听", JSONObject.toJSONString(ports));
        try {
             /*
               绑定多个端口核心代码
             */
            for (Map.Entry<Integer, String> p : ports.entrySet()) {
                final int port = p.getKey();
                // 绑定端口
                ChannelFuture future1 = bootstrap.bind(new InetSocketAddress(port)).sync();
                future1.addListener(future -> {
                    if (future.isSuccess()) {
                        log.info("netty 启动成功,端口:{}", port);
                    } else {
                        log.info("netty 启动失败,端口:{}", port);
                    }
                });
                future1.channel().closeFuture().addListener((ChannelFutureListener) channelFuture -> future1.channel().close());
            }
        } catch (Exception e) {
            log.error("netty 启动时发生异常-------{}", e);
        }
    }

    @PreDestroy
    public void stop() {
        if (future != null) {
            future.channel().close().addListener(ChannelFutureListener.CLOSE);
            future.awaitUninterruptibly();
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            future = null;
            log.info(" 服务关闭 ");
        }
    }
}

端口配置类

@Data
@Configuration
@ConfigurationProperties(prefix = "netty")
public class PortDefinition {

    Map<Integer, String> port;
}

2. 通道初始化

@Slf4j
public class SocketChannelInitHandler extends ChannelInitializer<SocketChannel> {


    /**
     * 用来存储每个连接上来的设备
     */
    public static final Map<ChannelId, ChannelPipeline> CHANNEL_MAP = new ConcurrentHashMap<>();

/**
     * 端口信息,用来区分这个端口属于哪种类型的连接 如:6001 属于 A
     */
    Map<Integer, String> ports;

    public SocketChannelInitHandler(Map<Integer, String> ports) {
        this.ports = ports;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //每次连接上来 对通道进行保存
        CHANNEL_MAP.put(socketChannel.id(), socketChannel.pipeline());
        ChannelPipeline pipeline = socketChannel.pipeline();
        int port = socketChannel.localAddress().getPort();
        String type = ports.get(port);
        //不同类型连接,处理链中加入不同处理协议
        switch (type) {
            case GatewayType.LIMS:
                pipeline.addLast(
                        new AMsgDecoder(),
                        new AMsgEncoder(),
                        new AServerHandler());
                break;
            case GatewayType.EXPLOSION_PROOF:
                //防爆
                pipeline.addLast(
                        new BMsgDecoder(),
                        new BMsgEncoder(),
                        new BServerHandler());
                break;
            case GatewayType.NON_EXPLOSION_PROOF:
                pipeline.addLast(
                        new CExpMsgDecoder(),
                        new CExpMsgEncoder(),
                        new CExpServerHandler());
                break;
            default:
                log.error("当前网关类型并不存在于配置文件中,无法初始化通道");
                break;
        }
        pipeline.addLast(
                new StringEncoder(StandardCharsets.UTF_8),
                new StringDecoder(StandardCharsets.UTF_8));
    }
}

3. 创建对应的解析器和编码器

3.1 信息解析器

@Slf4j
public class AMsgDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
        //读取数据信息
        byte[] data = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(data);
        StringBuilder msg = new StringBuilder();
        for (byte aByte : data) {
            msg.append(String.format("%02x", aByte).toUpperCase());
        }
        if (StringUtils.isEmpty(msg.toString())) {
            log.error("接收的报文为空!");
        }
        log.info("<----初始数据: {} ", msg);
        assemblyResult(ctx, msg.toString(), list);
    }

    private void assemblyResult(ChannelHandlerContext ctx, String data, List<Object> list) {
        List<Result> limsMessageList = analysisMessageOfLims(data);
        list.addAll(pclMessage);
    }

    /**
     * 解析报文信息
     *
     * @param data 未解析数据
     */
    private List<Result> analysisMessageOfLims(String data) {
       // 解析报文,具体解析方式跟硬件约定协议有关。
       /* 对于数据包会不会出现粘包或拆包,需要根据实际情况进行处理。netty中常用的方法是根据包头上
       的长度字节来判断当前包是否出现粘包或拆包
       */
    }
}

3.2 信息编码器

@Slf4j
public class AMsgEncoder extends MessageToByteEncoder {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
        SendData message = (SendData) o;
        //拼接需要发送的数据信息
        log.info("----> 初始数据【{}】 : {}", message.getCode(), msg);
        //将字符类型转换成字节类型
        byte[] bytes = DataTypeConvert.hexStringToBytes(msg.toString());
        //写入字节缓冲区
        byteBuf.writeBytes(bytes);
    }
}

4. 编写服务端处理器

//继承SimpleChannelInboundHandler的好处在于可以直接拿到解析后的实体类对象。
@Slf4j
@ChannelHandler.Sharable
public class AServerHandler extends SimpleChannelInboundHandler<Result> {

    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 在解析完数据后可以得到当前设备的一些信息,如ip 唯一标示等。可以用来绑定当前通道的唯一id。
     * 业务不同这个步骤操作也不同,在初始化通道的时候,用了ChannelId 来绑定对应的通道,其实是
     * 可以使用远端连接ip来进行绑定的,那么这里就不用再绑定通道id一次。这就需要根据各自的具体业务了。
     */
    private static final Map<String, ChannelId> CHANNEL_MAP = new ConcurrentHashMap<>();

    public static Map<String, ChannelId> getChannelMap() {
        return CHANNEL_MAP;
    }

    /**
     * 当从客户端接收到一个消息时被调用
     * msg 解析后的数据信息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Result message) throws Exception {
        String gwCode = message.getCode();
        ChannelId channelId = ctx.channel().id();
        CHANNEL_MAP.put(gwMac, channelId);
        /*下面就是业务模块了。需要注意的是Spring框架默认@Socpe是singleton单例的,如果你不想当前处理
          器被共用那么你就该指定该类为多例或者手动去new
        */
    }

    /**
     * 通道关闭,清除该网关记录
     *
     * @param gwCode 网关
     */
    public static void closeChannel(String gwCode) {
        CHANNEL_MAP.remove(gwCode);
    }

    /**
     * 在与客户端的连接已经建立之后将被调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("新增网关设备连接成功,远端地址为:{}", ctx.channel().remoteAddress());
    }

    /**
     * 主动发送信息
     *
     * @param code code
     * @param msg  信息
     */
    public void send(String code, String msg) {
        if (CHANNEL_MAP.containsKey(code)) {
            ChannelPipeline pipeline = SocketChannelInitHandler.CHANNEL_MAP.get(CHANNEL_MAP.get(code));
            if (pipeline == null) {
                //
                SocketChannelInitHandler.CHANNEL_MAP.remove(CHANNEL_MAP.get(code));
            }
            assert pipeline != null;
            pipeline.writeAndFlush(msg);
        } else {
            System.out.println("-------设备已经断开连接-------");
        }
    }


    /**
     * 客户端与服务端断开连接时调用
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
        ChannelId id = ctx.channel().id();
        //关闭
        SocketChannelInitHandler.CHANNEL_MAP.remove(id);
        log.info("网关: {} 服务端连接关闭...", ctx.channel().remoteAddress());
    }

    /**
     * 服务端接收客户端发送过来的数据结束之后调用
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    /**
     * 在处理过程中引发异常时被调用
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("remoteAddress: {},连接异常:{}", ctx.channel().remoteAddress(), cause);
    }

5. Netty服务启动类

@Configuration
public class InitNettyServerConfig implements CommandLineRunner {


    @Autowired
    private NettyServer nettyServer;

    @Autowired
    public void setNettyServer(NettyServer nettyServer) {
        this.nettyServer = nettyServer;
    }

    @Override
    public void run(String... args) throws Exception {
        nettyServer.start();
    }
}

结尾

到此netty服务同时监听多个端口,处理多套协议实现完毕,在具体的项目中碰到过许许多多的问题,后面有时间也会编写出来,记录分享。

  • 7
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值