前言

上一次只是知道了BIO到NIO和AIO。他们有一个共同的缺点就是代码量太大了,而且NIO提供的那个ByteBuffer有很多BUG,并且开发工作量大,很多问题都要自己处理,比如客户端面临断连重连,网络闪断心跳处理,半包读写,网路拥塞和异常流的处理等等。所以现在的网络开发主要使用的是netty,他其实和AIO一样,是对NIO的API的封装,但他的性能很高,源自于他的线程模型很优秀。Netty的应用场景很多,包括作为RPC框架的网络通信工具,即时通讯系统,消息推送系统。

Netty核心组件

先来说一下Netty的核心组件和功能吧,先来说一下最上层的BootStrap类吧,他的具体实现有两种,分别是ServerBootStrap为服务端的启动引导辅助类,然后是BootStrap为客户端的启动引导辅助类,用他提供的链式编程来绑定其他组件如EventLoopGroup,Channel,ChannelHandler和设置参数,使用bind()绑定本地端口,使用connect()连接远程主机和端口。当bootStrap.bind()时因为Netty的所有IO操作都是异步的,所以不能立刻得到bind是否执行成功,所以要用ChannelFuture组件来作为bind的返回值,他相当于Channel的包装类,用他提供的addListener()来监听回调操作成功后的逻辑比如打印监听端口成功提示信息,作为返回值时还可以通过channel()方法获取关联的Channel,还可以通过sync()让异步操作变为同步,也就不需要addListener的回调函数了。然后是比较容易理解的是Channel组件,他和NIO的Channel一样,可以近似理解为BIO中的Socket。Channel的实现类包括NioServerSocketChannel和NioSocketChannel两个,分别代表服务端和客户端。Channel提供了基本的IO操作如bind(),connect(),read(),write()等等。大大降低了直接使用Socket类的复杂性。然后是EventLoop组件,可以说EventLoop事件循环接口是Netty最核心的概念了,他的主要作用实际就是负责监听网络事件并调用事件处理器也就是ChannelHandler进行IO操作的处理,他和Channel的联系在于,Channel是Netty网络操作如读写操作的抽象类,而EventLoop负责处理注册到其上的Channel处理IO操作,两者配合参与IO操作,Netty为每个Channel分配了一个EventLoop用于处理时间,EventLoop本身只是一个线程驱动,其生命周期内只会绑定一个线程,让该线程处理一个Channel的所有IO事件,而一个EventLoop可以和多个Channel绑定即n:1 ,同时EventLoop与线程的关系是1:1。EventLoop上层还有一个EventLoopGroup,他包含多个EventLoop,前者相当于处理事件的专用线程,后者相当于线程池,且线程池容量默认为CPU核心数的两倍,Netty线程模型中就会有两个NioEventLoopGroup,每个Group底层都有一个selector。之前说EventLoop会调用事件处理器,那么就再说一下ChannelHandler组件吧,他是消息的具体处理器,也是我们最熟悉的非固定代码,负责处理读写操作,客户连接等消息,而他上层还有一个组件就是ChannelPipeline,他是ChannelHandler的链,我们可以通过他的addLast()方法添加一个或者多个ChannelHandler,因为一个消息事件可能被多个Handler处理,他提供了一个容器并定义了用于沿着链传播入站和出站事件流的API,当Channel被创建时,他会被自动地分配到他专属的ChannelPipeline。

Netty使用

再说说他的使用流程,Netty的很多代码是固定的,比如最开始,需要我们定义两个线程组,其中一个初始化为1,也就是说,他只有一个线程,另一个采用默认值,默认值是CPU核心数的两倍。这两个线程组bossGroup和workGroup其实就类似于NIO模型中的selector的作用,也和他一样,在初始化完之后,把这两个线程组,和代表着Server服务器的ServerBootStrap绑定起来,之所以有两个是因为要保证客户端能够成功连接上服务端所以用专门的线程组来处理连接请求。做完这些事情时候,利用这个bootStrap他提供的链式编程,我们可以对这个服务器进行自定义得设置参数,比如之前的两个线程组,然后选定channel通道的实现class分服务端和客户端,初始化服务器连接队列大小,其中最重要的参数是我们需要给他传入一个Handler对象,用来处理信号,类比NIO,这个Handler类就是NIO中的Handler函数,这个Handler需要继承自ChannelInboundHandlerAdapter,他里面的每个函数对应处理一个信号,这也我们主要的代码逻辑所在的地方,可以读取客户端发送的数据和数据读取完毕的处理方法,这两步都是依靠ChannelHandleContext完成的,他是上下文对象含有通道channel和管道pipeline。而且更方便的是他的读事件的处理也不需要ByteBuf了,而是重写读事件处理时单独传入参数String msg。除此之外,甚至于Netty连发生异常时的回调函数也给我们提供好了,我们可以覆写ChannelInboundHandlerAdapter提供的exceptionCaught函数来处理异常。他和AIO还有的相似之处就在于,他们都封装了NIO的channel,也就是说,我们在NIO中从selector按channel这个操作可以省略。

在处理函数中,里面还有netty为我们提供的一个ByteBuf类,他的底层就是一个字节数组,因为网络中不可能传字符串,传的都是字节流,所以我们需要的数据都要从ByteBuf中拿,他里面有两个指针,分别是读指针和写指针,他们把ByteBuf分为了三个部分,分别是已读区域[0,rIndex),可读区域[rIndex,wIndex),可写区域[wIndex,capacity)。当特定的读写API被调用时,这两个指针就会移动,可以通过byteBuf.readIndex()获取这个指针的位置。这也是我们打交道比较多的代码。这就是Server端的大致流程。

再就是Client端,他也是一个BootStrap,和NIO的Client需要selector来监听事件一样,这个bootStrap也需要一个线程组,不是两个的原因是他不需要监听连接信号,这在后面说吧,bootStrap一样用链式编程来设置参数,加入我们编写的Handler类,这个Handler也继承自ChannelInboundHandlerAdapter,覆写方法,处理服务端的信号,编写我们的逻辑代码,这也是很简单的过程,不用关心与业务无关的代码。

总结

Netty之所以高性能,就在于他的线程模型。线程模型的发展是有一个过程的,从NIO提出了selector,channel后,此时NIO是有问题的就是连接请求没有优先处理而导致客户端连接超时,后来出现一个Reactor模型,他是并发大师DoungLea在一本书中提出的,他首先是把NIO中的事件处理交给了线程池,最后由发展成双线程池就和Netty的线程模型很相似了。这也是Netty比NIO优化的地方,对NIO而言,他把连接请求和读写请求一视同仁了,但是往往读写请求的数量远远多于连接请求,而连接请求又十分重要,如果响应慢的话,用户体验不好,而且可能时间太长导致连接失败。如果说NIO是用if把各个信号处理隔离开的话,Netty就是把连接信号和读写信号进一步隔离开,用专门的selector也就是bossGroup来专门处理连接信号,用另一个selector也就是workGroup来处理读写信号,当新的信号来连接是,连接用的selector会注册号channel,然后才会把把这些channel复制给处理读写信号的selector。我们往往用的是一主多从,因为连接的selector任务比较轻,所以一个就够了。而处理读写请求的selector是一个workGroup,里面有很多的selector,默认是有cpu数量两倍的selector,而且还用到线程池,能够支持很多的连接,这也是为什么Netty这么高性能的原因所在。

项目

最后实践一个聊天室,Server要做的事情就是new两个EventLoopGroup,然后new一个启动引导类ServerBootStrap,将其绑定,重点在于添加ChannelHandler时需要多个Handler,所以用到ChannelPipeline加入解码器和编码器,实现类为StringDecoder和StringEncoder,最后加入自己的业务处理ChannelHandler。在自定义的ChatServerHandler中因为肯定有多个Channel,所以用ChannelGroup来维护多个Channel,分别重写处理多个事件函数,包括channelActive()来提示上线,context中获取触发的channel后将其私人信息用channelGroup.writeAndFlush来公布,然后channelGroup.add()将新上线的channel加入到channelGroup中。再比如channelInactive()同理提示离线,不用手动删除group。再比如读写事件,其中写是一个输出行为,调用channel的writeAndFlush()就可以了,并非是一个会被动触发的事件,而读这个被动事件用channelRead0()来实现,获取出发的channel,然后在group中foreach遍历来实现显示自己发送或他人发送,打印参数msg。至此Server就做完了。
Client同上,不过只有一个EventLoopGroup来处理事件,因为没有连接事件,还有不同就是需要BootStrap.connect()来连接远程主机。且客户端需要输出,所以创建一个Scanner,将输入的信息通过channel.writeAndFlush()发送给服务器,其中的channel由ChannelFuture得到。在自定义的ChannelHandler中只需要处理读事件,将msg打印出来即可。至此Client也完成了。