
抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)
1. Main Reactor处理OP_ACCEPT事件
OP_ACCEPT事件活跃.png
当Main Reactor轮询到NioServerSocketChannel上的OP_ACCEPT事件就绪时,Main Reactor线程就会从JDK Selector上的阻塞轮询APIselector.select(timeoutMillis)调用中返回。转而去处理NioServerSocketChannel上的OP_ACCEPT事件。
•处理IO就绪事件的入口函数processSelectedKey中的参数AbstractNioChannel ch正是Netty服务端NioServerSocketChannel。因为此时的执行线程为main reactor线程,而main reactor上注册的正是netty服务端NioServerSocketChannel负责监听端口地址,接收客户端连接。
•通过ch.unsafe()获取到的NioUnsafe操作类正是NioServerSocketChannel中对底层JDK NIO ServerSocketChannel的Unsafe底层操作类。
Unsafe接口是Netty对Channel底层操作行为的封装,比如NioServerSocketChannel的底层Unsafe操作类干的事情就是绑定端口地址,处理OP_ACCEPT事件。
这里我们看到,Netty将OP_ACCEPT事件处理的入口函数封装在NioServerSocketChannel里的底层操作类Unsafe的read方法中。
image.png
而NioServerSocketChannel中的Unsafe操作类实现类型为NioMessageUnsafe定义在上图继承结构中的AbstractNioMessageChannel父类中。
下面我们到NioMessageUnsafe#read方法中来看下Netty对OP_ACCPET事件的具体处理过程:
2. 接收客户端连接核心流程框架总览
我们还是按照老规矩,先从整体上把整个OP_ACCEPT事件的逻辑处理框架提取出来,让大家先总体俯视下流程全貌,然后在针对每个核心点位进行各个击破。
接收客户端连接.png
main reactor线程是在一个do...while{...}循环read loop中不断的调用JDK NIO serverSocketChannel.accept()方法来接收完成三次握手的客户端连接NioSocketChannel的,并将接收到的客户端连接NioSocketChannel临时保存在List<Object> readBuf集合中,后续会服务端NioServerSocketChannel的pipeline中通过ChannelRead事件来传递,最终会在ServerBootstrapAcceptor这个ChannelHandler中被处理初始化,并将其注册到Sub Reator Group中。
这里的read loop循环会被限定只能读取16次,当main reactor从NioServerSocketChannel中读取客户端连接NioSocketChannel的次数达到16次之后,无论此时是否还有客户端连接都不能在继续读取了。
因为我们在?《一文聊透Netty核心引擎Reactor的运转架构》一文中提到,netty对reactor线程压榨的比较狠,要干的事情很多,除了要监听轮询IO就绪事件,处理IO就绪事件,还需要执行用户和netty框架本省提交的异步任务和定时任务。
所以这里的main reactor线程不能在read loop中无限制的执行下去,因为还需要分配时间去执行异步任务,不能因为无限制的接收客户端连接而耽误了异步任务的执行。所以这里将read loop的循环次数限定为16次。
如果main reactor线程在read loop中读取客户端连接NioSocketChannel的次数已经满了16次,即使此时还有客户端连接未接收,那么main reactor线程也不会再去接收了,而是转去执行异步任务,当异步任务执行完毕后,还会在回来执行剩余接收连接的任务。
Reactor线程运行时结构.png
main reactor线程退出read loop循环的条件有两个:
1.在限定的16次读取中,已经没有新的客户端连接要接收了。退出循环。
2.从NioServerSocketChannel中读取客户端连接的次数达到了16次,无论此时是否还有客户端连接都需要退出循环。
以上就是Netty在接收客户端连接时的整体核心逻辑,下面笔者将这部分逻辑的核心源码实现框架提取出来,方便大家根据上述核心逻辑与源码中的处理模块对应起来,还是那句话,这里只需要总体把握核心处理流程,不需要读懂每一行代码,笔者会在文章的后边分模块来各个击破它们。
这里首先要通过断言assert eventLoop().inEventLoop()确保处理接收客户端连接的线程必须为Main Reactor 线程。
而main reactor中主要注册的是服务端NioServerSocketChannel,主要负责处理OP_ACCEPT事件,所以当前main reactor线程是在NioServerSocketChannel中执行接收连接的工作。
所以这里我们通过config()获取到的是NioServerSocketChannel的属性配置类NioServerSocketChannelConfig,它是在Reactor的启动阶段被创建出来的。
同理这里通过pipeline()获取到的也是NioServerSocketChannel中的pipeline。它会在NioServerSocketChannel向main reactor注册成功之后被初始化。
ServerChannelPipeline完整结构.png
前边提到main reactor线程会被限定只能在read loop中向NioServerSocketChannel读取16次客户端连接,所以在开始read loop之前,我们需要创建一个能够保存记录读取次数的对象,在每次read loop循环之后,可以根据这个对象来判断是否结束read loop。
这个对象就是这里的 RecvByteBufAllocator.Handle allocHandle专门用于统计read loop中接收客户端连接的次数,以及判断是否该结束read loop转去执行异步任务。
当这一切准备就绪之后,main reactor线程就开始在do{....}while(...)循环中接收客户端连接了。
在 read loop中通过调用doReadMessages函数接收完成三次握手的客户端连接,底层会调用到JDK NIO ServerSocketChannel的accept方法,从内核全连接队列中取出客户端连接。
返回值localRead表示接收到了多少客户端连接,客户端连接通过accept方法只会一个一个的接收,所以这里的localRead正常情况下都会返回1,当localRead <= 0时意味着已经没有新的客户端连接可以接收了,本次main reactor接收客户端的任务到这里就结束了,跳出read loop。开始新的一轮IO事件的监听处理。
随后会将接收到的客户端连接占时存放到List<Object> readBuf集合中。
调用allocHandle.incMessagesRead统计本次事件循环中接收到的客户端连接个数,最后在read loop末尾通过allocHandle.continueReading判断是否达到了限定的16次。从而决定main reactor线程是继续接收客户端连接还是转去执行异步任务。
main reactor线程退出read loop的两个条件:
1.在限定的16次读取中,已经没有新的客户端连接要接收了。退出循环。
2.从NioServerSocketChannel中读取客户端连接的次数达到了16次,无论此时是否还有客户端连接都需要退出循环。
当满足以上两个退出条件时,main reactor线程就会退出read loop,由于在read loop中接收到的客户端连接全部暂存在List<Object> readBuf集合中,随后开始遍历readBuf,在NioServerSocketChannel的pipeline中传播ChannelRead事件。
最终pipeline中的ChannelHandler(ServerBootstrapAcceptor)会响应ChannelRead事件,并在相应回调函数中初始化客户端NioSocketChannel,并将其注册到Sub Reactor Group中。此后客户端NioSocketChannel绑定到的sub reactor就开始监听处理客户端连接上的读写事件了。
Netty整个接收客户端的逻辑过程如下图步骤1,2,3所示。
netty中的reactor.png
以上内容就是笔者提取出来的整体流程框架,下面我们来将其中涉及到的重要核心模块拆开,一个一个详细解读下。
