前言:为了让大家对Netty有个整体认知,本文首先会对Netty的整个运作过程捋一遍,先不管什么异步、NIO、零拷贝之类的,细节的东西后面再说,直接淦图:
结合图示进行全过程讲解:
1. ServerBootStrap作为Netty的服务端入口,会对BossGroup和WorkGroup进行相关初始化操作,在BossGroup中,主要是对客户端的新连接请求进行处理(即OP_ACCEPT事件,但其实OP_ACCEPT事件的具体处理也会涉及到读写事件,因为数据不是读就是写),在WorkGroup中,则负责处理IO读写、编解码、业务逻辑等(即OP_READ事件、OP_WRITE事件)。服务端启动的时候会绑定一个端口,作为后续客户端连接入口,绑定端口的时候会在BossGroup(由NioEventLoopGroup类创建的对象)的其中一个NioEventLoop的Selector(多路复用器)上注册一条NioServerSocketChannel通道,后面的连接处理就是在通道中进行的。
2. BootStrap则作为Netty的客户端入口,会对ClientGroup进行相关初始化操作,在ClientGroup中,第一就是创建与服务端的连接(即OP_CONNECT事件),第二就是进行IO读写、编解码、业务逻辑等操作(即OP_READ事件、OP_WRITE事件)。
3. 服务端和客户端启动之后,当服务端收到客户端发来的连接请求,由于属于OP_ACCEPT事件,在BossGroup中处理。BossGroup(由NioEventLoopGroup类创建的对象)管理着若干个NioEventLoop,每个NioEventLoop持有一个线程(就好比线程池中的一组线程并发处理若干个连接请求),每个NioEventLoop上会创建一个Selector,一个Selector上可以注册多个通道(所以叫多路复用器),且它会以不断轮询的方式同时监听每个通道上是否有IO事件发生,每个通道里都会有个ChannelPipeline管道,管道里全是Handler,包括管道头Head和管道尾Tail,以及进行IO读写、编解码,业务处理的若干个Handler,Handler也可以自定义,把需要的Handler注册进管道就可以执行了。当请求到达Head时,代表“请求数据”已准备好,OP_ACCEPT事件已就绪,Selector监听到事件已就绪,就会让持有的线程对事件进行处理,处理过程是在Handler中进行。首先会创建一个NioSocketChannel实例,然后交给ServerBootStrapAcceptor这个Handler,它是Netty底层代码注册的,Acceptor具体操作就是向WorkGroup中的某个Selector注册刚才创建好的NioSocketChannel,自此客户端连接请求处理结束。
4. 客户端发出连接请求的同时会自己创建一条NioSocketChannel通道与服务端NioSocketChannel进行互通,连接完之后就是WorkGroup的事了,不需要BossGroup管了,一个客户端连接对应一条服务端NioSocketChannel。比如现在客户端要进行一个远程方法的调用,将方法参数传给服务端后,服务端处理完将结果返回给客户端。首先请求从客户端通道传输到WorkGroup中的对应通道,然后Head会申请一块堆外内存来缓冲请求内容,缓冲完之后,代表数据已准备好,OP_READ事件已就绪,selector监听到就绪事件之后,让持有的线程对事件进行处理,这里我定义了Decode解码,Compute方法调用处理和Encode编码三个Handler进行操作,其中Inbound入站Handler包括Decode和Compute(从Head到Tail就是入站),Outbound出站Handler包括Encode(从Tail到Head就是出站),每一个Handler被注册到Pipeline中的时候都会创建一个与之对应的ChannelHandlerContext,它包含着Handler的上下文信息,主要负责管理和其他在同一管道里的Handler之间的交互,它有一个前指针和后指针,可以与其他ChannelHandlerContext关联,这样Handler处理就变得更加灵活,比如这次请求需要三个Handler,而下次请求只涉及到Decode和Encode,那下次就可以执行完Decode然后指针直接指向Encode,next指针具体指向谁是依靠ChannelHandlerContext中的数据类型与其他Handler类型进行匹配得出的。在处理完读事件之后,接着处理Handler中涉及到的写事件,将处理结果写到ByteBuf中,回到Head,执行flush操作将ByteBuf内容写到SocketBuffer中,然后再到网卡buffer,通过互联网把结果传回给客户端,客户端拿到结果之后同样要进行解码,反序列化等操作,那么回过头发现客户端在发送调用请求之前在Pipeline中也进行了Encode处理的。(Head的主要作用:从SocketBuffer读请求内容到ByteBuf,从ByteBuf写返回结果到SocketBuffer)
5. 假设又有另外一个客户端连接了服务端,且和之前那个NioSocketChannel注册到了同一个Selector上,当线程正在处理另一个通道上的事件的时候,这时该客户端也发起了一个处理请求,请求到达服务端通道之后会被Head读到堆外内存中缓冲着,此时OP_READ事件已就绪,Selector监听到了就绪事件,但由于线程正在处理另外一个通道上的事件,所以就要等当前通道的事件处理完,下一轮循环监听再处理了(这也是堆外内存的作用体现之一,数据可以先在缓冲区放着)。当两个通道被注册在不同的Selector上的时候就互不影响了,因为是在不同的线程中并行处理的。另外补充两点,第一个TaskQueue任务队列中的任务都是非IO任务,从性能上来考虑,千万不要将一个需要长时间来运行的任务放入到任务队列中,因为事件任务在一个线程中是串行执行的,这样会阻塞其他任务。解决方案是使用一个专门的EventExecutor来执行它(ChannelPipeline提供了带有EventExecutorGroup参数的addXXX()方法,该方法可以将传入的ChannelHandler绑定到你传入的EventExecutor之中),这样它就会在另一条线程中执行,与其他任务隔离。第二个Channel注册到Selector后返回的是一个SelectionKey,这个SelectionKey有以下几个重要属性:
- interest set,通道感兴趣的事件集,就是会把该通道可能执行的事件类型都告诉Selector
- ready set,感兴趣的事件集中的“就绪事件集”
- 保存着的Channel
- 保存着的Selector
IO事件类型:
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_CONNECT
因此Selector每次循环监听的其实就是SelectionKey中的就绪事件集,看是否存在已就绪的事件,存在就进行处理。SelectionKey相当于是Selector和Channel之间的桥梁。
关于Netty的几个问题
以下部分问题已经在上述流程中体现了,所以理解起来应该容易一些了
- Netty是基于NIO实现的,那么什么是NIO?Netty中的NIO相比原始NIO的优势在哪儿?戳:如何从BIO演进到NIO,再到Netty
- Netty的线程模型是基于Reactor线程模型的,那么什么是Reactor线程模型呢?戳:Netty源码分析系列之Reactor线程模型
- Netty是异步非阻塞的,异步和非阻塞体现在什么地方?首先Netty非阻塞就是因为NIO中的Selector,可以轮询监听多条通道(一个客户端对应一条通道),而不像传统的BIO,一个线程只能负责一个客户端,只要客户端没发请求过来,线程就一直阻塞着傻傻等待,浪费资源。第二个异步的概念是当一个异步过程调用发出后会立即返回,调用者不能立刻得到结果。实际处理完成后,通过状态、通知或者回调的形式来告诉调用者,异步的优势是在高并发情形下会更稳定,具有更高吞吐量。Netty中的IO操作都是异步的,异步的实现是依靠ChannelFuture,它可以添加ChannelFutureListener监听器,当IO操作真正完成的时候,客户端会得到操作成功或失败的通知。
- Netty对于TCP粘包、半包问题的解决,戳:Netty源码分析系列之TCP粘包、半包问题以及Netty是如何解决的
- Netty中的零拷贝是如何体现的?
传统数据拷贝方式:
(1)数据从存储设备或网卡缓冲区,拷贝到内核的received buffer
(2)数据从received buffer读到堆外内存,再拷贝到用户缓冲区
(3)数据从用户缓冲区拷贝到堆外内存,再写到内核的send buffer
(4)数据从send buffer再拷贝到网卡buffer
Netty零拷贝方式:
(1)调用java的FileChannel.transferTo(),数据从存储设备或网卡buffer利用DMA引擎拷贝到received buffer
(2)数据从received buffer读到堆外内存
(3)数据从堆外内存写到send buffer
(4)数据从send buffer再利用DMA引擎拷贝到网卡buffer
由于以上操作都不需要CPU参与,所以就达到了“零拷贝”的效果,传统拷贝都是需要CPU参与的,就会占用cpu资源,DMA拷贝是不需要CPU的。
除此之外,对于传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们就需要创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf(CompositeByteBuf),就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,这也是“零拷贝”的另一个体现。
本人鄙学,如果以上内容哪里写的不对,大家也可以留言、私信,会及时进行修正~