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件事:
  1. 创建一定数量的NioEventLoop线程组并完成初始化。
  1. 创建线程选择器,当获取线程时候,通过选择器来选取线程。
  1. 创建线程工厂并构造线程执行器。

创建过程分析

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个参数是两个队列工厂分别是taskQueuetailTaskQueue

ExecutorChooser 分析

NioEventLoopGroup通过next()方法获取NioEventLoop线程,最终会调用其父MultithreadEventLoopGroupnext()方法,委托父类构造EventExecutorChooser。具体使用哪些对象取决于MultithreadEventLoopGroup的构造方法中使用的策略模式。
初始化过程中,通过newChooser()来创建选择器,根据线程数是否为2的幂次来选择策略,如果是,选择PowerOfTwoEventExecutorChooser其使用与运算计算下一个选择器,否则选择GenericEventExecutorChooser其选择策略是通过取余方式来计算出下一个选择器。
其中PowerOfTwoEventExecutorChooser有着更好的性能

ThreadFactory源码分析

Netty的NioEventLoop线程被包装成了FastThreadLocalThread线程,同时,NioEventLoop线程的状态由它自身管理,因此每个NioEventLoop线程都需要一个线程执行器,并且在线程执行前需要通过io.netty.util.concurrent.DefaultThreadFactory将其包装成FastThreadLocalThread线程。
执行器ThreadPerTaskExecutorDefaultThreadFactorynewThread() 方法源码如下:

NioEventLoop源码分析

NioEventLoop源码相比于NioEventLoopGroup源码就复杂多了,每个NioEventLoop对象都与NIO中的多路复用 Selector 一样,要管理成千上万条链路,所有链路数据的读/写事件都有它发起。NioEventLoop有以下5个核心功能:
  1. 开启 Selector 并初始化
  1. 把 ServerSocketChannel 注册到 Selector 上
  1. 处理各种I/O事件,例如OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE事件。
  1. 执行定时调度任务。
  1. 解决JDK NIO空轮询的bug。
NioEventLoop这些功能的具体实现大部分都是委托其他类来完成的,其本身只完成数据流的接入工作。这样的设计减轻了NioEventLoop的负担,同时增强了其拓展性。NioEventLoop的整体功能如下图:
notion image
其中上图中,第二层为NioEventLoop的4个核心方法。对于每条EventLoop线程来说,由于链路注册到Selector上的具体实现都是委托给Unsafe方法来完成,因此register()方法存在其父类SingleThreadEventLoop中。

开启 Selector

在初始化NioEventLoop时,通过openSelector()方法开启Selector。在rebuildSelector() 方法中也可以调用openSelector()方法。
在NIO中开启Selector,只需要调用Selector.open()SelectorProvideropenSelector()方法即可。Netty为Selector设置了优化开关,如果开启优化开关,则通过反射加载sun.nio.ch.SelectorImpl对象,并通过已经优化过的SelectorSelectionKeySet替换sun.nio.ch.SelectorImpl对象中的selectedKeyspublicSelectedKeys两个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都具有对应的权限集合时,当前正在运行的代码才能存取某个ResourcedoPrivileged方法是对这个规则的一种补充。他类似于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的事件类型触发AbstractNioChannelunsafe()的不同方法。这些方法主要是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执行父类AbstractUnsaferegister()模板方法。

总结

这一小节我们分析了NioEventLoop的源码包括两个部分分别是NioEventLoopGroupNioEventLoop,其中一个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进行分析。
Netty核心组件源码剖析 — Channel网络编程 — Reactor模型与Netty
Loading...
Honesty
Honesty
人道洛阳花似锦,偏我来时不逢春
最新发布
Java IO — NIO Buffer
2024-10-21
Java IO — NIO Channel
2024-10-21
Java IO — IO/NIO模型
2024-10-21
Java异步编程方式介绍
2024-10-21
Elasticsearch — 索引(Mapping Index)
2024-10-19
Elasticsearch — 如何存储数据并保持一致性?
2024-10-19