Netty编写客户端

Netty编写客户端

​ 上一篇博客讲了一下解码器,但是其实没有涉及到客户端的编写,今天补上这篇博客。同时深入了解一下Netty(对于我来说)。加深自己的印象。

image-20201208205126779

​ 上面是一个简单的服务端的例子,之前的博客也讲过这个demo,下面按照自己最新的理解再次记录一下。首先是最开始创建两个线程组,boss线程组用来监听客户端的连接,只做到这一步,work线程组相当于干活的,当通道中有IO事件时就开始工作。这样做的好处就是可以提升读写性能。这一块的内容涉及到Netty的线程模型,这一块我不是很熟悉,找到一篇文章,大家可以了解一下,感兴趣的可以继续搜索一下:Netty线程模型,以后也会慢慢补上这些内容的,学习无涯。回到代码上,然后就是创建了一个ServerBootstrap,这个启动类的作用就是辅助与编写服务端,降低开发的复杂度。再然后就是设置线程组、设置Channel、配置TCP的参数,编写Netty之前最好还是有点NIO的编写经验,不要像我上来就开始,一点准备工作都没有做,读代码都很吃力。最后的两行代码含义就比较容易懂了,调用辅助类的bind()方法绑定监听某个端口,同时调用同步阻塞方法等待绑定操作完成。最后一行则是等待服务端链路关闭后,退出主方法。

​ 说了这么多,其实和客户端的编写一点关系都没有,主要还是记录一下Netty启动的流程,对于Netty原理和底层的一些操作,这个需要等我看完NIO再记录一下。下面就讲一下Netty编写客户端的思路。

​ 客户端不需要两个线程组,一个就够了,通道也和服务端不太一样,需要设置为NioSocketChannel,剩下就是设置TCP的参数和handler的设置。发起连接和服务端也不太一样,服务端是监听,客户端就是连接,调用connect()方法发起异步连接,然后再调用同步方法等待连接成功。最后就是连接关闭后释放线程组的资源。代码也比较简单,如下:

//配置客户端的线程组,客户端只有一个线程组
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    //放入自己的业务Handler
                    socketChannel.pipeline().addLast(new TCPClientHandler());
                }
            });
    //发起异步连接操作,同步阻等待结果
    ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
    //等待客户端链路关闭
    channelFuture.channel().closeFuture().sync();
} finally {
    //释放NIO线程组
    eventLoopGroup.shutdownGracefully();
}

​ 不要以为到这里就结束了,上面这个只是一个简单的demo,自己写着玩还可以,但是到了生产环境或者实际用的时候,很多东西没有考虑到。断线重连问题、线程管理的问题、连接多服务端的情况。这些问题是我开发时遇到的问题,下面就一一讲一下实现的逻辑,最后则给出整体的代码。

  • 断线重连。断线重连一般时客户端要做的事情,也分为两种情况,一开始连接不上和连接上过一段时间断了。这两种情况需要不同处理,一开始连接不上需要添加监听器,异步执行,失败就调用两秒重连。断线则需要再handler中的channelInactive()方法做相应的业务处理。
  • 线程管理和连接多服务端的问题其实是一个场景。就是需要连接多个服务端的情况下,一个连接创建一个线程显然是不太合理的,同时也要对这些线程进行管理。这里暂时的办法是单独启动一个线程,线程的run()方法就是启动netty连接。后续在这个线程里面去完成多个客户端的建立或者一个netty线程去连接多个服务端。目前水平有限,只能想到这个地步。涉及到线程和NIO还有Netty。后续慢慢完善吧。

​ 所以最后的代码就如下:因为一些原因,没有给出全部的代码,给出了部分核心的代码,其他的细节和本次的内容关系不大。也可以按照自己的需求进行修改。(最好按照自己的想法修改一下,每个人的需求都不太一样,我的代码一定不合适所有人)

public class ConnectThread extends ConnectThread {
    private Logger logger = LoggerFactory.getLogger(ConnectThread.class);

    private Bootstrap bootstrap;

    private EventLoopGroup eventLoopGroup;

    private AtomicBoolean running = new AtomicBoolean(true);

    @Override
    public void run() {
        beginConnect();
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                shutDown();
            }
        });
        //阻塞住
        while (running.get()) {
            try {
                Thread.sleep(200l);
            } catch (Exception e) {
                logger.error("程序运行发生错误");
                e.printStackTrace();
                shutDown();
            }
        }
    }

    public void beginConnect() {
        try {
            eventLoopGroup = new NioEventLoopGroup();
            bootstrap = new Bootstrap();
            assembleBootStrap();
            reConnect();
        } catch (Exception e) {
            logger.error("客户端连接发送错误");
            e.printStackTrace();
            shutDown();
        }
    }

    /**
     * Description: 组装BootStrap
     *
     * @param
     * @return void
     * @Author: Peng Shiquan
     * @Date: 2020/12/9
     */
    public void assembleBootStrap() {
        ConnectThread ConnectThread = this;
        bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class);
        bootstrap.remoteAddress(host, port);
        bootstrap.option(ChannelOption.TCP_NODELAY, true);
        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 6000);
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) {
                socketChannel.pipeline().addFirst(new IdleStateHandler(10, 0, 0))
                        .addLast(new Decoder())
                        .addLast(new Handler(ConnectThread));
            }
        });
    }

    @Override
    public void reConnect() {
        super.reConnect();
        logger.info("雷达开始重试,雷达信息为:{}:{}", host, port);
        bootstrap.connect(host, port).addListener(new ConnectListener(this));
    }

    public void shutDown() {
        running.set(false);
        if (closeChannel != null && closeChannel.isActive()) {
            closeChannel.close().awaitUninterruptibly();
            closeChannel = null;
        }
        bootstrap = null;
        if (eventLoopGroup != null) {
            eventLoopGroup.shutdownGracefully();
        }
        eventLoopGroup = null;
        logger.error("客户端关闭连接,雷达服务端信息为:{} : {}", host, port);
    }
}

​ 来讲一下上面的代码大致的思路。连接类继承了线程,重写了run()方法,run方法里面只有三个部分的代码,一个是启动连接,一个是程序运行中有异常执行关闭方法,释放资源。最后一个就是将线程阻塞住,因为启动连接是异步的,如果不阻塞,代码直接就执行完毕了,后续也没法进行重连的操作。启动连接方法里面只有两步,一个是组装BootStrap启动类,另外一个就是执行重连操作,之前说过,断线重连发生在两个地方,最开始没有连接上和中间断掉,所以这里直接掉同一个方法,不用再重复写方法了。组装启动类按照自己的需求组装就可以了,设置超时事件、设置长时间无数据返回、设置解码器,业务处理类。重连方法就是调用启动类的connect()方法,这里和上面的demo有一点不一样,这里是异步返回的,没有阻塞住,添加了一个监听器,也是为了做重连用的。如果和demo一样的话,这里发生断连,程序就直接关闭了,就没法重连了。想要重连必须重新创建线程组、启动类、通道等,这个显然不是我们想看到了,因为断连后,线程组和和启动类还是可以继续使用的,只不过要换一个连接。

​ 下面就继续附上监听器和handler的代码。(handler中有一部分断线重连的方法)

public class ConnectListener implements ChannelFutureListener {
    private Logger logger = LoggerFactory.getLogger(ConnectListener.class);

    private final ConnectThread ConnectThread;

    public ConnectListener(ConnectThread ConnectThread) {
        this.ConnectThread = ConnectThread;
    }

    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        if (future.isSuccess()) {
            logger.info("[" + ConnectThread.getHost() + ":" + ConnectThread.getPort() + "] 启动成功!!!");
            ConnectThread.setTime(0);
            ConnectThread.setCloseChannel(future.channel());
        } else {
            if (ConnectThread.getTime() >= 1800) {
                logger.error(ConnectThread.getHost() + ":" + ConnectThread.getPort() + "重试超过一小时,不再重试");
                return;
            }
            logger.warn("[" + ConnectThread.getHost() + ":" + ConnectThread.getPort() + "]Channel失联,2秒后重连。。。 ");
            future.channel().eventLoop().schedule(new Runnable() {
                @Override
                public void run() {
                    ConnectThread.reConnect();
                }
            }, 2l, TimeUnit.SECONDS);

        }
    }
}

​ 监听器的代码就是判断是否连接上,没有的话进行重试,连接上了就重置次数,同时返回通道信息,为后面关闭通道做准备。剩下句没有什么可以说的了。最好就是handler的代码。

public class XYDHandler extends ChannelInboundHandlerAdapter {
    private Logger logger = LoggerFactory.getLogger(XYDHandler.class);

    private XYDConnectThread xydConnectThread;

    public XYDHandler(XYDConnectThread xydConnectThread) {
        this.xydConnectThread = xydConnectThread;
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.error("连接断开,IP端口为:{}", ctx.channel().remoteAddress());
        ctx.channel().eventLoop().schedule(() -> {
            xydConnectThread.reConnect();
        }, 2, TimeUnit.SECONDS);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = Unpooled.copiedBuffer((byte[]) msg);
        logger.info("接收到消息为:{}", ByteBufUtil.hexDump(byteBuf));
    }

​ 重连的方法就是channelInactive(),和监听器里的代码也是差不多。剩下就没有什么可以说的了。

​ 上面的代码就是完成一个断线重连的Netty的客户端,代码直接粘贴是不能用的,需要进行一些修改,我现在也比较烦直接将代码粘贴过来,上来就开始测试。还是要了解一下具体如何实现的,要不然改代码都不知道如何修改。就像这次编写客户端,最开始写很吃力,因为之前是一知半解,不知道如何修改,不知道如何排查,所以走了很多的弯路,代码完全不能看,也不起用。现在了解了一些后,慢慢能够按照自己的想法将代码改造成自己想要的样子,我觉得这样才算一个东西你彻底掌握了,与君共勉。

​ 就这样吧,结束。

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值