带你玩转Netty(二)

概述

上篇给大家介绍了 Netty 的基本原理,这次主要给大家讲一下具体怎么用 Netty,主要分成两个方面客户端以及服务端实现,我们将实现一个简单的 Echo 程序。

服务端

服务端的实现,主要从这几个方面考虑:
1. 最佳线程模型,实现高并发,高稳定性
2. 容错机制
3. 业务处理
4. 心跳监测

那么用Netty怎么一一实现这些呢,废话不多说,直接上代码。

public class NormalNettyServer {
    private int serverPort = 9000;
    private String serverIp = "192.168.2.102";

    public  NormalNettyServer(int port){
        serverPort = port;
    }

    public void start() throws Exception {
        // 创建Accpet线程池 (1)
        EventLoopGroup bossGroup = new NioEventLoopGroup(10);

        // 创建Work线程池
        EventLoopGroup workGroup = new NioEventLoopGroup(10);

        try{
            // 创建ServerBootstrap (2)
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workGroup).      // (3)
            channel(NioServerSocketChannel.class). //(4)
            childHandler(new ChannelInitializer<SocketChannel>() {  // 初始化处理handler (5)
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    // 加入用户心跳监测机制 读时间超时 60s 写时间超时 10s 读写都没有超时 10s
                    ch.pipeline().addLast("timeout", new IdleStateHandler(60, 10, 10, TimeUnit.SECONDS));
                    // 加入业务处理handler
                    ch.pipeline().addLast("echo", new EchoHandler());
            }
            }).option(ChannelOption.SO_BACKLOG, 128) // (6)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); 
            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(serverIp, serverPort).sync(); // (7)
            f.channel().closeFuture().sync();
        } finally {
            // 释放资源
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
  1. EventLoopGroup 是用来处理I/O操作多线程事件循环池, 我们知道Netty是基于Reactor模型的,其中一种是双线程池,这里就创建了两个线程循环池,管理10个线程,bossGroup主要用于处理accpet请求,建立连接后的处理主要是由workGroup负责。
  2. ServerBootstrap 是一个启动NIO服务的辅助启动类
  3. 注册两个线程循环池。
  4. NioServerSocketChannel这里直接引用官方说明更合适

    A ServerSocketChannel implementation which uses NIO selector based implementation to accept new connections.

  5. SocketChannel是TCP连接的网络通道,在下面两种情况会创建

    • 打开一个SocketChannel连接某台服务器。
    • 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
      当通道建立,会调用初始化操作,将业务处理的handler加入到pipeline中。这里主要加入应用层心跳监测以及应用层业务处理
  6. 通道支持参数配置,这里配置了两个参数SO_BACKLOG, SO_KEEPALIVE,具体作用大家可以看这里
  7. 绑定host以及监听端口,返回的ChannelFuture, 由于这个过程是异步的,所有执行状态可以通过ChannelFuture中的获取,这将在客户端的实现中重点介绍。

通过以上步骤,我们就可以建立一个高效的服务,对于一个写C++的我,只能说真的很爽。言归正传,下面我们介绍一下两个handler的处理,看代码。

public class EchoHandler extends SimpleChannelInboundHandler<Object> { // (1)

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        ByteBuf buf = (ByteBuf)o; // (2)
        byte[] packet = new byte[buf.readableBytes()];
        buf.readBytes(packet);
        // pb 协议 (3)
        HelloTest.Hello hello = HelloTest.Hello.parseFrom(packet);
        System.out.println(hello.getContent().toString());
        channelHandlerContext.channel().writeAndFlush(buf);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // (4)
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx)
            throws Exception {
        // 新连接active
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx)
            throws Exception {
        // 连接关闭
        super.channelInactive(ctx);
    }
  1. Netty的接受数据处理Handler都是继承 SimpleChannelInboundHandler或者ChannelInboundHandlerAdapter
  2. Netty数据的收发,都采用ByteBuf
  3. 这里采用了PB协议,具体用法,这里不详细讲,大家先看看,之后会有文章介绍。因为是Echo, 所有收到的数据直接原包返回。
  4. Channel异常处理,这里可以做一些容错操作。
    public class HeartBeatHandler extends ChannelInboundHandlerAdapter {

    private ConnectionClient client;
    public HeartBeatHandler(ConnectionClient client) {
       this.client = client;
    }

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

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.WRITER_IDLE) {
                //TODO(1) 可以做监测处理
            } else if (event.state() == IdleState.READER_IDLE) {
                // TODO(2)
            }
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}
  1. 写数据超时,服务端已经有一段时间没有对该连接通道发送数据。
  2. 读数据超时,服务端已经有一段时间没有接受该连接通道的数据。
  3. 通过这两个,我们可以对该通道做检查,比如发送心跳指令,监测客户端是否还存在。

这就是最简单服务端实现,麻雀虽小,五脏俱全,实现了我们最开始说的几个方面的内容。按照这个顺序,我们接下来介绍客户端实现。

客户端

客户端的实现往往都会有这样几个要求:
* 断线重连
* 心跳维持
* 业务处理

接下来,我们将会用Netty来实现这些功能,由于代码比较多,下面主要截取重要函数做说明。

public boolean connect() {
    Bootstrap b = new Bootstrap(); // (1)
    final HeartBeatHandler hearthandler = new HeartBeatHandler(this);
    final ClientHandler handler = new ClientHandler(this);

    EventLoopGroup loop = new NioEventLoopGroup(); // (2)
    b.group(loop).channel(NioSocketChannel.class);
    b.handler(new ChannelInitializer<Channel>() {
        @Override
        protected void initChannel(Channel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            // idle状态处理,主要是用于与服务端发送心跳 (3)
            pipeline.addLast(new IdleStateHandler(60, 20, 0, TimeUnit.SECONDS));
            pipeline.addLast("hearthandler", hearthandler);

            // 业务处理
            pipeline.addLast("handler",  handler);
        }
    });
    b.option(ChannelOption.SO_KEEPALIVE, true);
    b.option(ChannelOption.TCP_NODELAY, true);
    ChannelFuture future = b.connect(host, port);
    future.addListener(new ConnectionListener(this)); // (4)
    return true;
}
  1. Bootstrap客户端启动辅助类,与ServerBootstrap相对。
  2. 客户端不需要处理连接请求,所有只需定义一个多线程事件循环池来处理channel事件就可以。
  3. 客户端的业务处理handler,包含心跳处理,以及业务处理。
  4. ChannelFuture添加连接情况监听,用于实现重连。

    其他的含义与服务端相同,请参考服务端说明,这里重点介绍重连机制的实现,重连主要是由两个点来触发的,一个是ChannelFuture, 一个Handler的InActive事件。

 public class ConnectionListener implements ChannelFutureListener {

    // 此处省略多行代码 .......

    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
            // 连接失败
            System.out.println("connect reconnect");
            this.client.reconnect(future.channel());
        } else {
            // 连接成功
            System.out.println("connect success");
            this.client.setChannel(future.channel());
        }
    }
}
public class ClientHandler extends ChannelInboundHandlerAdapter {
    // 此处省略多行代码 .......

    @Override
    public void channelInactive(ChannelHandlerContext ctx)
            throws Exception {
        // 断开连接
        System.out.println("SuperServer is disconnect " +  ctx.channel().remoteAddress().toString());
        client.reconnect(ctx.channel());
        super.channelInactive(ctx);
    }
}
public void reconnect(final Channel ch) {
    final EventLoop eventLoop = ch.eventLoop();
    eventLoop.schedule(new Runnable() {
        @Override
        public void run() {
            connect();
            System.out.println("reconnect server:" + host + ", Port:" + port);
        }
    }, 10L, TimeUnit.SECONDS);
}

以上就是具体的重连的实现过程,两个触发点,大家记住即可。另外心跳的实现与服务端类似,只不过在业务的处理有可能不同。比如 服务要考虑是否断开客户端,回收资源。而客户端要考虑的是是否要重连。

欢迎关注公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值