type
status
date
urlname
summary
tags
category
icon
password
catalog
sort
前言
从这一小节我们开始深入源码学习,我们的源码分析会围绕着《Netty源码剖析与应用》这本书来学习,并且结合自己分析看源码,毕竟只有自己看到才是真的。这一小节我们要分析梳理核心组件NioEventLoop的源码,从源码角度看看NioEventLoop到底做了哪些事。第一次深入的研究源码学习,源码看得的确挺头大的,但是通过源码角度对Netty又有了一个全新的认知收获很多,那我们就一起来分析以下NioEventLoop吧。
我们分析解读的Netty版本为 4.1.68.Final
NioEventLoopGroup 源码分析
我们既然是分析NioEventLoop,那自然是离不开NioEventLoopGroup的分析,我们前面提到了NioEventLoop用于处理Channel的I/O操作,而NioEventLoopGroup则是这样一个操作的集合。一个NioEventLoopGroup里面包含一个或多个NioEventLoop。在前面的Demo中,我们还创建了辅助启动类ServerBootStrap。它也包括了两个NioEventLoopGroup,用于构建主从Reactor结构。
NioEventLoopGroup类主要完成了下面的3件事:
- 创建一定数量的NioEventLoop线程组并完成初始化。
- 创建线程选择器,当获取线程时候,通过选择器来选取线程。
- 创建线程工厂并构造线程执行器。
创建过程分析
NioEventLoopGroup的父类为
MultithreadEventLoopGroup
,父类继承了抽象类MultithreadEventExecutorGroup
。在初始化NioEventLoopGroup时,会调用其父类的构造方法。其中DEFAULT_EVENT_LOOP_GROUP
会决定生成多少NioEventLoop线程,默认值是CPU核数的两倍,同时这个值会先从系统配置中读取。在构造方法会传入这个参数,如果这个参数不传使用默认构造器构造线程数,否则按照传递的参数构造线程数。线程组的生产分两步:第一步,创建一定数量的 EventExecutor 数组;第二步,通过调用子类的
newChild()
方法完成这些 EventExecutor数组的初始。为了提高可拓展性,Netty的线程组除了NioEventLoopGroup,还有Netty通过JNI方式提供的一套由epoll模型实现的EpollEventLoopGroup 线程组,以及其他I/O多路复用模型线程组,因此
newChild()
方法由具体的线程组子类来实现。MultithreadEventExecutorGroup
的构造方法和newChild()
方法的解读如下:在newChild()方法中,NioEventLoop的初始化参数有7个:
- 第1个参数为NioEventLoopGroup线程组本身。
- 第2个参数为线程执行器,用于启动线程,在SingleThreadEventExecutor的
doStartThread()
方法中被调用。
- 第3个参数为NIO的Selector选择器的提供者。
- 第4个参数主要在NioEventLoop的run()方法中用于控制选择循环。
- 第5个参数为非I/O任务提交被拒时的处理Handler。
- 第6个和第7个参数是两个队列工厂分别是
taskQueue
和tailTaskQueue
。
ExecutorChooser 分析
NioEventLoopGroup通过
next()
方法获取NioEventLoop线程,最终会调用其父MultithreadEventLoopGroup
的next()
方法,委托父类构造EventExecutorChooser。具体使用哪些对象取决于MultithreadEventLoopGroup
的构造方法中使用的策略模式。初始化过程中,通过
newChooser()
来创建选择器,根据线程数是否为2的幂次来选择策略,如果是,选择PowerOfTwoEventExecutorChooser
其使用与运算计算下一个选择器,否则选择GenericEventExecutorChooser
其选择策略是通过取余方式来计算出下一个选择器。其中
PowerOfTwoEventExecutorChooser
有着更好的性能。ThreadFactory源码分析
Netty的NioEventLoop线程被包装成了FastThreadLocalThread线程,同时,NioEventLoop线程的状态由它自身管理,因此每个NioEventLoop线程都需要一个线程执行器,并且在线程执行前需要通过
io.netty.util.concurrent.DefaultThreadFactory
将其包装成FastThreadLocalThread
线程。执行器
ThreadPerTaskExecutor
与DefaultThreadFactory
的newThread()
方法源码如下:NioEventLoop源码分析
NioEventLoop源码相比于NioEventLoopGroup源码就复杂多了,每个NioEventLoop对象都与NIO中的多路复用 Selector 一样,要管理成千上万条链路,所有链路数据的读/写事件都有它发起。NioEventLoop有以下5个核心功能:
- 开启 Selector 并初始化。
- 把 ServerSocketChannel 注册到 Selector 上。
- 处理各种I/O事件,例如OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE事件。
- 执行定时调度任务。
- 解决JDK NIO空轮询的bug。
NioEventLoop这些功能的具体实现大部分都是委托其他类来完成的,其本身只完成数据流的接入工作。这样的设计减轻了NioEventLoop的负担,同时增强了其拓展性。NioEventLoop的整体功能如下图:

其中上图中,第二层为NioEventLoop的4个核心方法。对于每条EventLoop线程来说,由于链路注册到Selector上的具体实现都是委托给Unsafe方法来完成,因此
register()
方法存在其父类SingleThreadEventLoop中。开启 Selector
在初始化NioEventLoop时,通过openSelector()方法开启Selector。在rebuildSelector() 方法中也可以调用openSelector()方法。
在NIO中开启Selector,只需要调用
Selector.open()
或SelectorProvider
的openSelector()
方法即可。Netty为Selector设置了优化开关,如果开启优化开关,则通过反射加载sun.nio.ch.SelectorImpl
对象,并通过已经优化过的SelectorSelectionKeySet
替换sun.nio.ch.SelectorImpl
对象中的selectedKeys
和publicSelectedKeys
两个HashSet集合。其中,selectedKeys为就绪Key的集合,拥有所有操作事件准备就绪的选择Key;publicSelectedKeys为外部访问就绪Key的集合代理,由selectedKeys集合包装成不可修改的集合。SelectedSelectionKeySet具体做了哪些优化呢?
主要是改变了数据结构,用数组代替了HashSet,重写了
add()
和iterator()
方法,使数组的遍历效率更高,开启优化开关,需要将系统属性io.netty.noKeySetOptimization
设置为true。关于AccessController.doPrivileged来自不同为止的代码可以有一个CodeSource
对象描述其位置和签名证书。根据代码的CodeSource
的不同,代码拥有不同的权限。例如所有Java SDK自带的代码都具有所有的权限,而Applet
中的代码则具有非常受限的权限,用户便携的代码可以自己定制权限(通过SecurityMananger
)。当执行一段代码时,这段代码的StackTrace包含了从Main
开始所有正在被调用而且没有结束的方法。在这个调用过程中,很有可能出现跨多个不同的CodeSource
的调用序列。由于CodeSource
不同,这些代码通常拥有不同的权限集。只有所有途径CodeSource
都具有对应的权限集合时,当前正在运行的代码才能存取某个Resource
。而doPrivileged
方法是对这个规则的一种补充。他类似于Unix
中的setuid
程序。Unix
中的login
程序具有setuid
位,它不管被哪个用户调用,都具有root
权限。调用doPrivileged
的方法不管其StackTrace
中其他方法的权限,而仅仅根据当前方法的权限来判断用户是否能访问某个resource
。也即可以规定用户只能用某种预定的方式来访问其本来不能访问的resource
。使用doPrivileged
方法和使用setuid
位都需要注意的地方,例如仅仅执行必要的操作。否则,可能会带来安全上的问题。关于setuid
setuid
是类unix系统提供的一个标志位,其实际意义是set一个process的euid为这个可执行文件或程序的拥有者(比如root)的uid,也就是当setuid位被设置之后,当文件或程序(统称为executable)被执行时,操作系统会赋予文件所有者的权限,因为其euid是文件所有者的uid。
run() 方法解读
run()
方法是EventLoop的核心方法,EventLoop循环阻塞在这个方法对Channel进行监听并完成后续的各种操作,例如处理轮询到的SelectionKey,执行队列任务等。这个部分的代码解读如下:其实整体的方法也是比较简单的,在这个方法主要可以分为以下几个部分:
select(curDeadlineNanos)
用来轮询就绪的 Channel;
processSelectedKeys
用来处理轮询的SelectKey;
runAllTask
用来执行队列任务。
unexpectedSelectorWakeup(selectCnt)
处理空轮询bug。
接下来我们就针对这几个核心方法进行分析解读。
select轮询就绪Key
首先我们看
select(curDeadlineNanos)
,这个方法用来轮询就绪的Channel。这个方法也非常的简单如果当前传进来的deadlineNanos
是一个无效值直接进行监听就绪的Channel。否则计算一个阻塞的超时时间,如果这个超时时间<=0,直接非阻塞select,否则按照超时时间阻塞select。通过分析上面的代码,我们可以看到阻塞的Select当遇到以下4种情况会返回:
- 当至少有一个Key就绪。
- 当selector的wakeup被调用。
- 当前线程被interrupt。
- 超时自动醒来。
当然这里还有一个空轮询bug。
processSelectedKeys处理就绪Keys
第二个部分,
processSelectedKeys
:主要处理第一部分轮询到的就绪Keys,并取出这些SelectionKey及附件attachment。附件有两种类型:第一中是AbstractNioChannel
,第二种是NioTask
。其中,第二种附件在Netty内部未使用,因此只分析AbstractNioChannel
。根据Key的事件类型触发AbstractNioChannel
的unsafe()
的不同方法。这些方法主要是I/O的读写操作。其具体源码包括附件注册,在剖析Channel源码时会详细讲解。processSelectedKeys的核心代码解读如下:runAllTasks 执行队列中任务
第三个部分,
runAllTasks
:主要目的是执行taskQueue队列和定时任务队列中的任务,如心跳检测,异步写操作等。首先NioEventLoop会根据ioRatio(I/O事件与taskQueue运行时间占比)执行任务时长。这里有一个点,就是如果IO占比达到100%不应该是所有的时间都会用来执行I/O时间,不会来执行队列任务么?后来从注释中发现当ioRatio
这个参数达到100%时,将不在平衡I/O任务和任务队列之间的占比,会处理所有的I/O事件和所有的任务队列。由于一个NioEventLoop线程需要管理很多Channel,这些Channel的任务非常多,若要全部执行完,则I/O事可能得不到及时的处理,因此每次执行64个任务后就会检测执行任务的时间已经用完,如果任务执行的时间用完了,就不再执行后续的任务了。代码解析如下:
处理空轮询bug
最后一个部分是对空轮询bug的处理,Netty通过重新构建Selector的方式去规避空轮询的bug。Netty是使用计算空轮询的次数来处理这个bug。如果当前轮询未执行任何I/O任务或处理任何队列任务,则selectCnt累加,当空轮询次数达到了阈值则重新构建Selector。
run()方法小结
最后回到NioEventLoop的
run()
方法,在这个方法中首先调用select(curDeadlineNanos)
方法轮训就绪的Channel;然后调用processSelectedKeys()
方法处理I/O事件;最后执行runAllTasks()
方法处理任务队列。如果当前是空轮询,则进行空轮询校验unexpectedSelectorWakeup(selectCnt)
,如果当前空轮训的次数大于或等于阈值(默认512)则重新构建selector。以上就是关于整个run方法的梳理。重新构建Selector和Channel的注册
从selector函数的代码解读中发现,Netty在空轮询次数大于或等于阈值(默认512)时,需要重新构建Selector。重新构建的过程为,重新打开一个新的Selector,将旧的Selector上的key和Attachment复制过去,同时关闭旧的selector,具体代码如下:
注册方法
register()
在两个地方被调用:一是端口绑定前,需要把NioServerSocketChannel注册到Boss线程的Selector上;二是当NioEventLoop监听到有链路接入时,把链路SocketChannel包装成NioSocketChannel,并注册到Worker线程中。最终调用NioSocketChannel的辅助对象unsafe的register方法,unsafe执行父类AbstractUnsafe
的register()
模板方法。总结
这一小节我们分析了NioEventLoop的源码包括两个部分分别是
NioEventLoopGroup
和NioEventLoop
,其中一个NioEventLoopGroup中包含一个或多个NIoEventLoop。其中NioEventLoopGroup的源码较为简单,包括创建NioEventLoop并完成初始化、创建线程选择器提供获取线程的策略、创建线程工厂并构造线程执行器。创建时候通过指定或是系统默认的线程数循环创建NioEventLoop。调用newChild()
初始化,这个方法也很简单,初始化了一些必要的参数之后,构造了一个NioEventLoop
。选择器也很简单,依据EventLoop的个数有两种策略,偶数使用&的策略,基数使用取余的策略。随后是ThreadFactory将新创建的线程包装成FastThreadLocalThread
。随后我们剖析了NioEventLoop的源码。NioEventLoop的源码可比NioEventLoopGroup 的源码复杂多了。首先我们分析了NioEventLoop是如何开启一个Selector的。在开启Selector,我们可以使用io.netty.noKeySetOptimization
选择开启优化Selector。随后我们梳理了NioEventLoop核心的run方法,run方法整体的逻辑上包括select轮询就绪Key,processSelectedKeys处理就绪Keys,runAllTask执行队列中的任务。run方法的最后校验判断当前是否存在空轮询的bug,如果空轮询达到一定次数则重新构建select和注册channel。下一小节我们将对Channel进行分析。- 作者:Honesty
- 链接:https://blog.hehouhui.cn/archives/1050c7d0-9e17-8033-b8b3-fa719b6a3114
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章