前言
本来想先写下NioServerSocketChannel以及NioSocketChannel的注册流程的,但是最后发现始终离不开NioEventLoop这个类,所以在这之前必须得先讲解下NioEventLoop这个类到底是用来做啥的。其实在第一篇文章里面有提及到它的,但是没有详细的去讲解,接下来会对它分析一波。
设计模型
在进入正文之前,先简单的了解下NioEventLoop的工作模型(服务端):
假设一个NioEventLoopGroup(这里服务端会用两个Group)里面有4个NioEventLoop,那么netty中的实际工作模型就如上图所示,服务端会用默认的选择规则从Group1中选择出一个NioEventLoop注册ServerChannel,并绑定一个OP_ACCEPT用于监听客户端发起的连接请求,一旦有新的连接进来,服务端则会从Group2中按一定的规则选出一个NioEventLoop来注册SocketChannel,并绑定OP_READ兴趣事件,这里注意,一个NioEventLoop可以绑定多个SocketChannel。具体的注册流程我会在下一篇文章中写出来。
下面进入正题。
构造流程
NioEventLoop具体的构造流程大家可以去我的Netty系列(一):NioEventLoopGroup源码解析中去看一下,里面说的还算蛮详细的。下面是其调用的构造函数,咱们可以观察到其身上会绑定一个选择器Selector
,供后期channel注册的时候使用的,这一块是JAVA NIO相关的知识点。
内部还维护着一个executor
去开启执行线程的,以及taskQueue
任务队列和一个tailTasks
尾部队列(这个队列里面的任务是在每次执行taskQueue
任务队列中的任务结束后都会去调用的,不多介绍)。上面介绍的三个四个内部结构Selector,executor,taskQueue,tailTasks
会在后面多次提起。
下图是NioEventLoop的简单的层级结构(下图取之于Netty in Action):
NioEventLoop.execute
这里我们先看一下NioEventLoop的execute
方法。实际上这个方法是在其父类SingleThreadEventExecutor
中。这个方法的功能就是将任务丢到taskQueue
中。
1 public void execute(Runnable task) {
2 if (task == null) {
3 throw new NullPointerException("task");
4 }
5
6 boolean inEventLoop = inEventLoop();
7 addTask(task);
8 if (!inEventLoop) {
9 // 开启工作线程,实际上也就是执行NioEventLoop中的run方法,下面会介绍
10 startThread();
11 if (isShutdown()) {
12 boolean reject = false;
13 try {
14 if (removeTask(task)) {
15 reject = true;
16 }
17 } catch (UnsupportedOperationException e) {
18 // The task queue does not support removal so the best thing we can do is to just move on and
19 // hope we will be able to pick-up the task before its completely terminated.
20 // In worst case we will log on termination.
21 }
22 if (reject) {
23 reject();
24 }
25 }
26 }
27
28 if (!addTaskWakesUp && wakesUpForTask(task)) {
29 wakeup(inEventLoop);
30 }
31 }
复制代码
- 添加task到执行队列中,也就是咱们上文提起的
taskQueue
中。 - 判断这个NioEventLoop中的是否已经开启过线程。
- 若未开启,则必须先启动线程任务,也就是我们下文会介绍的
run
方法。 - 首次初始化会在
taskQueue
中丢一个空任务去唤醒线程。
NioEventLoop的工作模式实际上就是开启一个单线程跑一个死循环,然后一直轮询taskQueue
队列是否有任务添加进来,然后就去处理任务,还有就是如果注册在selector上的channel有兴趣事件进来,也会去处理selectorKeys
,这一块下面会做介绍。
NioEventLoop.run
现在看看NioEventLoop中的run
方法
1 protected void run() {
2 for (;;) {
3 try {
4 try {
5 // 按默认配置的话要么返回select.selectNow(),
6 //要么返回SelectStrategy.SELECT
7 switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
8 case SelectStrategy.CONTINUE:
9 continue;
10
11 case SelectStrategy.BUSY_WAIT:
12 case SelectStrategy.SELECT:
13 select(wakenUp.getAndSet(false));
14
15 if (wakenUp.get()) {
16 selector.wakeup();
17 }
18 // fall through
19 default:
20 }
21 } catch (IOException e) {
22 rebuildSelector0();
23 handleLoopException(e);
24 continue;
25 }
26
27 cancelledKeys = 0;
28 needsToSelectAgain = false;
29 final int ioRatio = this.ioRatio;
30 if (ioRatio == 100) {
31 try {
32 // IO操作,根据selectedKeys去处理
33 processSelectedKeys();
34 } finally {
35 // 保证执行完所有的任务
36 runAllTasks();
37 }
38 } else {
39 final long ioStartTime = System.nanoTime();
40 try {
41 // IO操作,根据selectedKeys去处理
42 processSelectedKeys();
43 } finally {
44 // 按一定的比例去处理任务,有可能遗留一部分任务下次进行处理
45 final long ioTime = System.nanoTime() - ioStartTime;
46 runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
47 }
48 }
49 } catch (Throwable t) {
50 handleLoopException(t);
51 }
52 // Always handle shutdown even if the loop processing threw an exception.
53 try {
54 // 释放资源,将注册的channel全部关闭掉。
55 if (isShuttingDown()) {
56 closeAll();
57 if (confirmShutdown()) {
58 return;
59 }
60 }
61 } catch (Throwable t) {
62 handleLoopException(t);
63 }
64 }
65 }
复制代码
- 这里有个默认的计算策略:
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT
1.有任务便会直接返回
select.selectNow()
,则会直接去跑任务或者是去处理selectorKeys
;
2.若没任务,则会走select(wakenUp.getAndSet(false))
方法,里面会有一个timeout超时处理,selector.select(timeoutMillis)
,超时后也会去跑任务或者是去处理selectorKeys
;
这一块具体细节也很多,只是说一下处理流程。
- 注意上面有个
ioRatio == 100
这个判断条件,如果为100,则会将任务全部处理完成;否则会与io操作按一定的比例去执行任务。
这里的IO操作就是
processSelectedKeys()
方法,代码虽然很长,但是干的活就是根据不同的兴趣事件干不同的活,里面有对OP_READ OP_ACCEPT OP_WRITE OP_CONNECT
等等不同兴趣事件的不同处理方法,这一块应该是JAVA NIO里面的相关知识点。感兴趣的朋友可以debug针对某个触发事件研究一下。
runAllTasks
执行任务的代码如下(下面是runAllTasks
的代码):
1 protected boolean runAllTasks() {
2 assert inEventLoop();
3 boolean fetchedAll;
4 boolean ranAtLeastOne = false;
5
6 do {
7 // 这里会从定时任务队列中将达到执行事件的task丢到taskQueue中去
8 fetchedAll = fetchFromScheduledTaskQueue();
9 // 执行taskQueue中所有的task
10 if (runAllTasksFrom(taskQueue)) {
11 ranAtLeastOne = true;
12 }
13 } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
14
15 if (ranAtLeastOne) {
16 lastExecutionTime = ScheduledFutureTask.nanoTime();
17 }
18 // 这个是执行上面所说的tailTasks中的task
19 afterRunningAllTasks();
20 return ranAtLeastOne;
21 }
复制代码
这一块的逻辑是:
先执行fetchFromScheduledTaskQueue
方法,将到期的定时任务丢到taskQueue
队列中,这个fetchFromScheduledTaskQueue
方法里面有个小细节,当taskQueue
队列满了之后,它就会重新塞到scheduledTaskQueue
队列中,然后再外圈循环,taskQueue
队列消费完毕,则继续执行fetchFromScheduledTaskQueue
方法,直到把所有到期的任务都丢到taskQueue
队列中执行完毕为止。如下图所示:
这一部分到这里就结束了,下一篇会对NioServerSocketChannel的注册以及服务端创建NioSocketChannel进行分析。