文章目录
系类文章:
《Netty服务端启动源码分析(一)整体流程》
《Netty服务端启动源码分析(二)服务端Channel的端口绑定》
《Netty核心组件之NioEventLoop(一)创建》
《Netty核心组件之NioEventLoop(二)处理消息》
1. 开篇
-
NioEventLoop是Netty框架的Reactor线程;
-
NioEventLoop负责处理注册在其上面的所有Channel的IO事件,通常情况下一个NioEventLoop会下挂多个Channel,但一个Channel只和唯一的NioEventLoop对应
例如NioEventLoop A B 2个,A和c1、c2对应,B和c3、c4对应,不会交叉。
一个NioEventLoop对应一个selector多路复用器,为了负载均衡,新的Channel会平摊到每个NioEventLoop上,一旦确定后,就不会更改。 -
NioEventLoop同时会负责通过execute方法提交的任务,以及通过schedule方法提交的定时任务;
在接下来几篇文章,我会通过Netty的源码深入讲解NioEventLoop的实现机制。
特别说明:基于4.1.52版本的源码(很新的版本,和之前4.xx版本还是有些不同的)
2. 类继承关系以及重要的成员变量
先来看下NioEventLoop的类关系图和重要的属性,对其有一个整体的感知,便于后面详细分析。
2.1 类继承关系
可以看到NioEventLoop
的继承关系非常复杂,最上层是JDK的Executor
接口,说明它归根到底是一个执行器,是用来执行任务的。另外,它实现了EventLoop
接口、EventExecutorGroup
接口和ScheduledExecutorService
接口,继承了SingleThreadEventExecutor
类,这些接口和类为这个执行器添加了十分繁复的功能特性,要搞清楚NioEventLoop的具体实现机制就要不停的在这些父类和接口中来回跳转。
-
ScheduledExecutorService
接口表示是一个定时任务接口,EventLoop可以接受定时任务。 -
EventLoop
接口: 一旦Channel注册了,就处理该Channel对应的所有I/O操作。 -
SingleThreadEventExecutor
表示这是一个单个线程的线程池。 -
EventLoop
是一个单例的线程池,里面含有一个死循环的线程不断地做着三件事情:监听端口,处理端口事件,处理队列事件。每个Eventloop都可以绑定多个Channel,而每个Channel始终只能由一个EventLoop来处理也就是说,创建了NioEventLoop后,拥有一个线程池类型的成员变量,紧接着会往该线程池加入一个线程,该线程就负责阻塞监听IO事件等操作。添加线程由SingleThreadEventExecutor的doStartThread()完成,线程具体的干的事情由NioEventLoop的run()实现
2.2 重要的成员变量
private Selector selector;
private SelectedSelectionKeySet selectedKeys;
private volatile Thread thread;
private final EventExecutorGroup parent;
private final Queue<Runnable> taskQueue;
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
private final Queue<Runnable> tailTasks;
selector
:作为NIO框架的Reactor线程,NioEventLoop需要处理网络IO事件,因此它需要有一个多路复用器,即Java NIO的Selector对象;selectedKeys
:每次select操作选出来的有事件就绪的SelectionKey集合,在NioEventLoop的run方法中会处理这些事件;thread
:即每个NioEventLoop绑定的线程,它们是一对一的关系,一旦绑定,在整个生命周期内都不会改变;parent
:即当前的NioEventLoop所属的EventExecutorGroup;taskQueue
:NioEventLoop中三大队列之一,用于保存需要被执行的任务。scheduledTaskQueue
:NioEventLoop中三大队列之一,是一个优先级队列(内部其实是一个按照任务的下次执行时间排序的小顶堆),用于保存定时任务,当检测到定时任务需要被执行时,会将任务从scheduledTaskQueue中取出,放入taskQueue;tailTasks
:NioEventLoop中三大队列之一,用于存储当前或下一次事件循环结束后需要执行的任务;
2.3 构造函数
在调用构造函数创建实例之前,有必要回顾下NioEventLoop是何时被创建的,是在创建NioEventLoopGroup的时候就已经创建了!详情参见《Netty服务端启动源码分析(一)》 中"1.1.2 构造函数干了什么"章节。也就是说NioEventLoop是被预创建的,发生在通道的创建和与通道的绑定之前。
首先来看NioEventLoop的构造函数:
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory queueFactory) {
// 设置parent、executor、addTaskWakesUp(添加任务时是否唤醒select)、创建taskQueue和tailTask队
// 列
super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
rejectedExecutionHandler);
this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
// selector初始化
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
2.3.1 在构造函数中,会创建任务队列和tailTask队列
private static Queue<Runnable> newTaskQueue(
EventLoopTaskQueueFactory queueFactory) {
if (queueFactory == null) {
return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
}
return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
}
private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
: PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
}
默认情况下,会创建MPSC,即多生产者单消费者的队列,这里最终会用到JCTools库,这里不过多介绍,感兴趣的可以自己去了解。
2.3.2 多路复用器Selector
Netty的实现是基于Java原生的NIO的,其对原生的NIO做了很多优化,避免了某些bug,也提升了很多性能。但是底层对于网络IO事件的监听和处理也是离不开多路复用器Selector的。在NioEventLoop的构造方法中进行了Selector的初始化:
final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;
构造函数中还会初始化selector和根据配置对selectedKeys进行优化
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
// 如果优化选项没有开启,则直接返回
if (DISABLE_KEY_SET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}
-
调用openSelector();,初始化一个epoll,并持有这个selector
等价于 原生nio语法:Selector selector = Selector.open();更多的nio知识,可以参见:《bio、nio、aio–Netty笔记》中<3.2 NIO引入多路复用器代码示例>的架构示意图部分
如果设置了优化开关(默认优化选项是开启的),则通过反射的方式从Selector中获取selectedKeys和publicSelectedKeys,将这两个成员设置为可写,通过反射,使用Netty构造的selectedKeySet将原生JDK的selectedKeys替换掉。
我们知道使用Java原生NIO接口时,需要先调Selector的select方法,再调selectedKeys方法才可以获得有IO事件准备好的SelectionKey集合。这里优化过后,只通过一步select调用,就可以从selectedKeySet获得需要的SelectionKey集合。
另外,原生Java的SelectionKey集合是一个HashSet,这里优化过后的SelectedSelectionKeySet底层是一个数组,效率更高。
2.4 NioEventLoop执行流程
也就是说,创建了NioEventLoop后,拥有一个线程池类型的成员变量,紧接着会往该线程池加入一个线程,该线程就负责阻塞监听IO事件等操作。添加线程由SingleThreadEventExecutor的doStartThread()完成,线程具体的干的事情由NioEventLoop的run()实现。
2.4.1 添加一个线程
我们先来看下添加线程的代码,定义在父类中的,为了方便,我进行了简化,仅列出重要代码:
public abstract class SingleThreadEventExecutor {
private void doStartThread() {
//添加一个Runnable作为线程任务到executor(线程池)中
executor.execute(new Runnable() {
@Override
public void run() {
try {
//Runnable匿名内部类调用外部类SingleThreadEventExecutor的run(),实际会调用子类NioEventLoop对象实例的run()
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
} finally {
}
}
});
}
代码比较简单,但是有个不容易理解的地方,即
SingleThreadEventExecutor.this.run();
,这个是匿名表达式引用外部类方法的语法,即Runnable的run()方法想引用外部类的run()方法,详情参见《Lambda表达式里的“陷阱“ 匿名表达式 qualified this》
2.4.2 run方法解析
添加线程后,线程池就会调用该线程,那么线程具体干了哪些事情呢?
EventLoop的职责可以用下面这张图形象的表示:
EventLoop的run方法在一个for死循环中,周而复始的做着三件事:
1、从已注册的Channel监听IO事件;对应select()阻塞监听,当然为了避免一直阻塞,一般带超时时间
2、处理IO事件;
3、从任务队列取任务执行。
public final class NioEventLoop extends SingleThreadEventLoop {
protected void run() {
int selectCnt = 0;
for (;;) {
int strategy;
try {
// 计算本次循环的执行策略
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 调用Java NIO的多路复用器,检查注册在NioEventLoop上的Channel的IO状态
strategy = select(curDeadlineNanos);
}
} catch (IOException e) {
}
// 处理IO事件
processSelectedKeys();
// 处理任务队列中的任务
ranTasks = runAllTasks();
...
}
整个run()方法被包裹在一个for循环中,唯一能够结束循环的条件是状态state为SHUTDOWN或者TERMINATED,NioEventLoop继承了SingleThreadEventExecutor,isShuttingDown()和confirmShutdown()都是SingleThreadEventExecutor中的方法。
可以看到,除去异常处理和一些分支流程,整个run()方法不是特别复杂,重点在与select()和selectNow()方法,run()方法流程如下图所示:
下面详细解析
2.4.2.1 calculateStrategy
calculateStrategy决定了调用selectNow()还是select()。
先来看calculateStrategy:
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}
//成员变量selectNowSupplier
private final IntSupplier selectNowSupplier = new IntSupplier() {
@Override
public int get() throws Exception {
return selectNow();
}
};
protected boolean hasTasks() {
assert inEventLoop();
return !taskQueue.isEmpty() || !tailTasks.isEmpty();
}
-
每次循环,都会检测任务队列和IO事件,如果任务队列中没有任务,则直接返回SelectStrategy.SELECT;
因为任务列表为空,则允许调用select阻塞等待新的消息的到来
-
如果任务队列中有任务,则会调用非阻塞的selectNow检测是否有IO事件准备好的Channel数。
因为任务列表不为空,要选用非阻塞的方法,这样原有的任务可以继续执行
2.4.2. 阻塞的select
当任务队列中没有任务时,直接进入select分支
case SelectStrategy.SELECT:
// 找到下一个将要执行的定时任务的截止时间
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
// 阻塞调用select
strategy = select(curDeadlineNanos);
}
} finally {
nextWakeupNanos.lazySet(AWAKE);
}
// fall through
阻塞调用select:
private int select(long deadlineNanos) throws IOException {
// 如果没有定时任务,直接调Java NIO的select,进入阻塞
if (deadlineNanos == NONE) {
return selector.select();
}
// 如果截止时间小于0.5ms,则timeoutMillis 为0,直接调非阻塞的selectNow()方法
long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}
在调用select之前,再次调用hasTasks()判断从上次调用该方法到目前为止是否有任务加入,多做了一层防护,因为调用select时,可能会阻塞,这时,如果任务队列中有任务就会长时间得不到执行,所以须小心谨慎。
如果任务队列中还是没有任务,则会调用select方法。在这个方法中会根据入参deadlineNanos来选择调用NIO的哪个select方法:
如果deadlineNanos为NONE,即没有定时任务时,直接调用NIO的无参select方法,进入永久阻塞,除非检测到Channel的IO事件或者被wakeup;
如果存在定时任务,且定时任务的截止时间小于0.5ms,则timeoutMillis 为0,直接调非阻塞的selectNow方法,也就是说马上有定时任务需要执行了,不要再进入阻塞了;
其他情况,调用select(timeout),进入有超时时间的阻塞。
2.4.2.3 processSelectedKeys()
参见 《Netty核心组件之NioEventLoop(二)处理消息》
总结
在本文中,对Netty的NioEventLoop进行了深入的解读,并且详细讲解了它的三大职责之一:检测Channel的IO事件的机制。
NioEventLoop是Netty最核心的概念,内部运行机制很复杂,在接下来的两篇文章中会继续分析。