Netty源码学习2——NioEventLoop的执行
零丶引入
在《Netty源码学习1——NioEventLoopGroup的初始化》中,我们学习了NioEventLoopGroup和NioEventLoop的初始化,在下面netty服务端启动的demo中
会在ServerBootStrap中指定Channel为Nio类型的Channel,然后启动的时候绑定端口,之前我们解释道NioEventLoop是事件循环,这里的事件是指客户端连接,数据准备就绪等IO事件,循环是指会有一个线程不断的处理任务。so 这篇我们一起看下NioEventLoop是如何产生IO事件并执行各种任务的。
一丶NioEventLoop的初始化
这部分在[Netty源码学习1——NioEventLoopGroup的初始化#第四节](Netty源码学习1——NioEventLoopGroup的初始化 - Cuzzz - 博客园 (cnblogs.com))中进行了说明,其中比较有趣的是Netty对JDK原生Selector的优化,将原本基于HashSet的SelectionKey集合,修改为基于数组的SelectionKey集合,优化了迭代和修改的效率,再次就不在过多赘述了。
二丶NioEventLoop的启动
NioEventLoop中存在一个run方法,该方法是一个死循环,这便是事件循环中的循环,它由一个线程专门去执行,我们先分析下NioEventLoop#run方法是什么时候开始执行的。
如上分别是客户端和服务端启动部分的源码,在bind 和 connect中都会触发——Channel的初始化+Channel的注册。
-
Channel的初始化:由对应的Factory进行创建(默认使用反射)
-
Channel的注册:这里的注册将调用NioEventLoopGroup#register方法,
这里自然是通过EventExecutorChooser#next方法选择一个NioEventLoop进行注册
在excute方法中会将EventLoop#run方法包装成一个Runnable提交到线程池中,这里的线程池是被包装后的ThreadPerTaskExecutor,每一个任务的提交都会新建一个线程去执行
因此可以任务NioEventLoop#run方法由一个线程单独去执行
三丶NioEventLoop#run
1.SelectStrategy控制循环
和Tomcat的Poller类似,NioEventLoop需要使用Selector去进行IO多路复用,Netty抽象出SelectStrategy使用其calculateStrategy来控制循环
其中selectNow即使用JDK Selector#selectNow方法进行非阻塞select
DefaultSelectStrategy#calculateStrategy的逻辑是,如果当前EventLoop中存在任务那么直接返回就绪的就绪SelectionKey数目,如果不存哎任务,那么返回常量SELECT(-1),根据返回的数字会进入不同的分支。
2.执行阻塞select
NioEventLoop支持调度任务,因此内部使用了优先队列保存调度任务,并且调度任务具备一个deadlineNanos属性
3.执行任务
NioEventLoop支持ioRatio指定IO事件的处理事件,和其他任务执行时间的比例
其中处理IO时间的部分涉及到Netty的ChannelPipeline的执行,这部分在后续的博客中进行讲解
3.1 runAllTasks
无参的runAllTask会在IO事件处理结束后执行,首先会将scheduledTaskQueue中的任务(未到执行时间的任务,or deadlineNanos早于当前时间的任务 不会转移到taskQueue)转移到taskQueue,然后执行taskQueue中的任务,执行完taskQueue的任务后,执行tailTasks中的任务
3.2 runAllTasks(超时时间)
大流程和上面runAllTasks类似,依旧是将调度任务从scheduledTaskQueue转移到taskQueue,然后执行taskQueue任务,然后执行tailTasks中的任务
感觉比较有趣的是,对调度任务的处理,移动到taskQueue后并不会再次判断其deadlineNanos和当前时间的关系,不会再关注其是否过期。
3.3 处理NIO空轮询的bug
Java NIO在linux下基于epoll,但是因为poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为EPOLLHUP或者EPOLLERR,eventSet事件集合发生了变化,这会导致Selector会被唤醒,唤醒之后却没有感兴趣的事件,导致IO事件处理的循环不会执行,陷入到循环中.
下面我们看看netty是如何处理此bug的
可以看到Netty通过重新构建Selector的方式去规避空轮询的bug,其中rebuildSelector最终调用rebuildSelector0,会将原本selector中的SelectionKey拷贝到新的selector,然后关闭旧的Selector
四丶总结
此节我们学习了NioEventLoop的执行原理,本质上还是通过Selector进行IO多路复用,理解了什么是事件循环,其中的事件指通过IO多路复用监听多路IO事件,循环是指循环的监听IO事件,并处理IO事件和其他任务,但是对于IO事件的处理我们并未做更多的学习,这部分将在下一篇中进行学习。