Netty框架学习(一)-------时间服务器

一.前言

学习Netty最好的方法就是读官方文档http://netty.io/wiki/user-guide-for-4.x.html#wiki-h3-10。
下面我就文档里面的时间服务器例子,对Netty的基本应用进行一下总结。

二.与NIO的对比

Netty是基于NIO的通讯框架,用它开发应用的步骤要比直接使用NIO简洁很多。我们先来回忆一下NIO开发TimeServer的步骤:
(1)创建ServerSocketChannel,配置它为非阻塞模式。

private ServerSocketChannel serverChannel;
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);

(2)绑定监听,配置TCP参数,例如backlog大小

serverChannel.socket().bind(new InetSocketAddress(port), 1024);

(3)创建一个独立的IO线程,用于轮询多路复用器(选择器)Selector

(4)创建Selector,将之前注册的ServerSocketChannel注册到Selector上,监听SelectionKey.ACCEPT

serverChannel.register(selector, SelectionKey.OP_ACCEPT);

(5)启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的Channel

while (!stop) {
          try {
              selector.select(1000);
              Set selectionKeys = selector.selectedKeys();
              Iterator it = selectionKeys.iterator(); 
              SelectionKey key = null;
              while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

(6)当轮询到处于就绪状态的Channel是,需要对其进行判断,如果是OP_ACCEPT状态,说明是新的客户端接入,则调用ServerSocketChannel.accept()方法接收新的客户端

(7)设置新接入的客户端链路SocketChannel为非阻塞模式,配置其他的一些TCP参数

(8)把SocketChannel注册到Selector,并监听OP_READ操作位

if (key.isAcceptable()) {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel sc = ssc.accept();
        sc.configureBlocking(false);
        sc.register(selector, SelectionKey.OP_READ);
    }

(9)如果轮询的Channel为OP_READ,则说明SocketChannel中有新的就绪的数据包需要读取,则构造ByteBuf对象,读取数据包

if (key.isReadable()) {
       SocketChannel sc = (SocketChannel) key.channel();
       ByteBuffer readBuffer = ByteBuffer.allocate(1024);
       int readBytes = sc.read(readBuffer);
       if (readBytes > 0) {
           readBuffer.flip();
           byte[] bytes = new byte[readBuffer.remaining()];
           readBuffer.get(bytes);
           String body = new String(bytes, "UTF-8");
           System.out.println("The time server recieve order:" + body);
           String currentTime = "QUERY".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
           doWrite(sc, currentTime);
           } else if (readBytes < 0) {
              //对端链路关闭
              key.channel();
              sc.close();
           } else {
                 ;//读到0字节,忽略
                  }
          }

(10)如果轮询的Channel为OP_WRITE,说明还有数据没有发送完成,需要继续发送。

So,NIO完成上面最基本的消息发送的步骤还是挺多的。下面我们来看一下Netty是怎样简化的。

三.Netty—TimeServer服务端源码

TimeServer:

public final class TimeServer {
    static int PORT = 8989;
    static final boolean SSL = System.getProperty("ssl") != null;

    public static void main(String[] args) throws Exception {
        //Configure SSL
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        //配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .childHandler(new ChannelInitializer() { //为accept channel的pipeline预添加的inboundhandler
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline(); 
                            if (sslCtx != null) {
                                p.addLast(sslCtx.newHandler(ch.alloc()));
                            }
                            //p.addLast(sslCtx.newHandler(LogLevel.INFO))
                            p.addLast(new TimeServerHandler());//为当前的channel的pipeline添加自定义的处理函数  
                        }
                    });
            //绑定端口,同步等待成功,服务器启动
            ChannelFuture f = b.bind(PORT).sync();
            //等待服务器监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            //优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

四.TimeServer步骤分析

同样地,我们一步步的看TimeServer的搭建过程。在现在没有深入了解源码的情况下,对一些Netty的类库和用法进行基础的介绍。

(1)创建两个NioEventLoopGroup实例。NioEventLoopGroup是一个线程组,它包含了一组NIO线程,专门用于网络事件的处理,实际上它们就是Reactor线程组。这里创建两个的原因是一个用于服务端接收客户端的连接,一个用于进行SocketChannel的读写。

(2)创建ServerBootstrap对象,它是netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度。当然,这里没看源码也不知道它怎么降低的。

(3)调用ServerBootstrap的group()方法,将两个NIO线程传入到ServerBootstrap中。

(4)设置创建的Channel为NioServerSocketChannel,它的功能对应于JDK NIO类库中的ServerSocketChannel类。

(5)然后配置NIOServerSocketChannel的TCP参数,此处将它的backlog(最大队列长度)设置为100。

(6)配置ChildHandler,作用类似于Reactor模式中的Handler类,主要用于处理网络IO事件,例如记录日志,对消息进行编解码等。

(7)服务端启动辅助类完成配置后,调用它的bind()方法就可以绑定监听端口,随后,调用它的同步阻塞方法sync等待绑定操作完成

(8)完成后Netty会返回一个ChannelFuture,主要用于异步操作的通知回调。

(9)f.channel().closeFuture().sync()进行阻塞,直到服务端链路关闭之后才推出main函数。

(10)释放资源

五.TimeServerHandler源码

TimeServerHandler

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
        final ChannelFuture f = ctx.writeAndFlush(time);
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                assert f == future;
                ctx.close();
            }
        });
    }

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

六.TimeServerHandler步骤分析

(1)继承ChannelInboundHandlerAdapter:
Abstract base class for ChannelInboundHandler implementations which provide implementations of all of their methods.

This implementation just forward the operation to the next ChannelHandler in the ChannelPipeline. Sub-classes may override a method implementation to change this.

(2)重写方法:
channelActive():通道激活时触发,当客户端connect成功后,服务端就会接收到这个事件,从而可以把客户端的Channel记录下来,供后面复用

channelRead():当收到对方发来的数据后,就会触发,参数msg就是发来的信息,可以是基础类型,也可以是序列化的复杂对象.

channelReadComplete():channelRead执行后触发

exceptionCaught():出错时会触发,做一些错误处理

七.TimeClient源码

public class TimeClient {
    public static void main(String[] args) throws InterruptedException {
        final String host = "127.0.0.1";
        final int port = 8989;
        //配置客户端NIO线程组
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            //发起异步连接操作
            ChannelFuture f = b.connect(host, port).sync();
            //等待客户端关闭
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

八.TimeClient分析

可以看到,客户端的代码和服务端的代码很像,不同点如下:
(1)客户端只创建了一个NioEventLoopGroup对象,用以相应IO事件
(2)设置创建的Channel为NioSocketChannel,它的功能对应于JDK NIO类库中的SocketChannel类。
(3)然后配置NIOSocketChannel的TCP参数为ChannelOption.SO_KEEPALIVE,服务端是SO_BACKLOG

九.总结

使用netty框架步骤确实简单了很多,但要深入学习netty,就必须研读它类库的源码,清楚它们的原理,所以后面我会陆续记录的。

你可能感兴趣的:(NIO通讯框架)