前言

    NioEventLoop的run方法,是netty中最核心的方法,没有之一。在该方法中,完成了对已注册的channel上来自底层操作系统的socket事件的处理(在服务端时事件包括客户端的连接事件和读写事件,在客户端时是读写事件)、单线程任务队列的处理(服务端的注册事件、客户端的connect事件等),当然还包括对NIO空轮询的规避、消息的编解码等。下面一起来探究一番,首先奉上run方法的源码:

 protected void run() {
for (;;) {
try {
try {
// 1、确定处理策略
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 2、表示有socket事件,需要进行处理
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
} catch (IOException e) {
// selector有异常,则重新创建一个
rebuildSelector0();
handleLoopException(e);
continue;
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
// 3、处理来自客户端或者服务端的socket事件
processSelectedKeys();
} finally {
// 4、处理队列中的task任务
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
// 3、处理来自客户端或者服务端的socket事件
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
// 4、处理队列中的task任务
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// 执行shutdown后的善后逻辑
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}

run方法中有四个主要的方法,已在上面注释中标出,主要逻辑概括起来就是:先通过select方法探知是否当前channel上有就绪的事件(方法1和方法2),然后处理这些事件(方法3),最后再处理队列中的任务(方法4)。

一、selectStrategy.calculateStrategy方法

selectStrategy只有一个默认实现类DefaultSelectStrategy,实现方法如下,如果判断有任务,则走selectSupplier.get()方法,否则直接返回SELECT -1,进入方法2-select方法。

 public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}

然后看一下匿名类selectSupplier.get方法中的逻辑,如下,可以看到它直接调的非阻塞select方法。

 private final IntSupplier selectNowSupplier = new IntSupplier() {
@Override
public int get() throws Exception {
return selectNow();
}
};

总结一下calculateStrategy方法这么做的用意。从run方法的整体顺序中可以看到,每次循环中都是先执行方法3处理channel事件,再执行方法4处理队列中的任务,即处理channel事件的优先级更高。但如果队列中有任务待处理,那么为提高框架处理性能,就不允许执行阻塞的select方法,而是执行非阻塞的selectNow方法,这样就能快速处理完channel事件后去处理队列中的任务。

二、select(boolean)方法

要理解该方法,需先理解wakenUp变量和wakeup方法的作用。wakenUp是AtomicBoolean类型的变量,如果是true,则表示最近调用过wakeup方法,如果是false,则表示最近未调用wakeup方法,另外每次进入select(boolean)方法都会将wakenUp置为false。而wakeup方法是针对selector.select方法设计的,如果调用wakeup方法时处于selector.select阻塞方法中,则会直接唤醒处于selector.select阻塞中的线程,而如果调用wakeup方法时selector不处于selector.select阻塞方法中,则效果是在下一次调selector.select方法时不阻塞(有点像LockSupport.park/unpark的效果)。下面是select(boolean)方法逻辑:

 private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
// 重点1:在调用阻塞的select方法前再判断一遍是否有任务需要处理,此处逻辑虽然不多,但有深意 ***
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
// 调用阻塞的select方法,但设置了超时时间
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++; if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
// 有事件;wakenUp之前是true(说明有新任务进入了队列中);wakenUp现在是true(说明有新任务在本方法执行的过程中进来了),有任务 满足以上任意一个都退出循环
break;
}
if (Thread.interrupted()) {
// 省略异常日志打印
selectCnt = 1;
break;
} long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// timeoutMillis elapsed without anything selected.
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 重点2: 说明触发了空轮训,需要做处理
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
// catch 异常处理
}

该方法有两处重点,均已标出。

重点1

该处逻辑需结合wakenUp变量和wakeup方法来理解。

首先,对wakenUp变量的操作除了run方法外,还有SingleThreadEventExecutor的execute方法。execute中添加完task后,会调用NioEventLoop中的重写方法wakeup:

 protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
}

注:selector.wakenUp方法用于唤醒被selector.select()或者selector.select(long time)阻塞的selector,让其立马返回key的数量。

它做了两件事,1是通过cas将wakenUp由false变为true,2是调用selector.wakeup方法。

再来看select(boolean)方法的入口处,通过wakenUp.getAndSet(false)方法将wakenUp设为false,然后将原值作为入参传入select(boolean)方法。

一切条件就绪,然后再回过头看重点1(如下)。它想实现的功能就是如果队列中有新的任务来了,能不调selector.select的阻塞方法,有任务等待执行时能不阻塞就不阻塞,提高效率。

 if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}

但细究一下会发现这个方法的两个判断逻辑存在一个矛盾,首先进入当前select(boolean)方法时,wakenUp被置为false,而在添加完任务后,NioEventLoop中的wakeup方法又会将wakenUp置为true,即如果hasTasks()方法返回true时,因为wakenUp已经被置为true了所以第二个条件肯定判断为false,那if里面的逻辑什么场景下才会走到呢?

不知道各位园友们走到这里的时候会不会有这样的疑问,反正博主刚开始是被自己难倒了,后来又重新分析了下才找到原因。其实博主刚才对矛盾点的描述就未分清时间先后。因为有新任务来的时候,是先往队列中添加任务,再将wakenUp置为true(selector.wakeup()方法可以认为与置为true是同时发生的),即如果添加了task但还没来得及将wakenUp置为true时才会进入这个if中。

那么新的问题来了,为什么将wakenUp置为true了就不用进if中呢?是因为如果wakenUp已经是true了,那么可以认为已经执行了selector.wakeup方法了,既然如此,selector.select虽然是阻塞方法也就不会再阻塞了,而是直接返回结果,所以没必要再进if中。

此处还有一个容易让人迷糊的地方就是下面的四个或的逻辑判断:

 if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}

即满足这四个条件中的任意一个就退出循环,这四个条件各代表什么意思?

第一个:channel中有socket事件需处理,这个肯定是要跳出循环处理的;

第二个:oldWakenUp为true,即进select(boolean)方法之前wakenUp为true,说明队列中有新任务来了,所以也要跳出循环,出去处理;

第三个:wakenUp现在为true,说明在进入select(boolean)方法之后队列中有新任务来了,需跳出循环处理;

第四/五个:两个队列中有任务,需出去处理。

其实就是说,如果当前没有事件过来,队列中又没有任务处理,那么就继续走select(boolean)的无限for循环(反正没事做),否则说明来菜了需要跳出循环出去处理。

重点2:

对于空轮训的处理其实没有太多花哨的地方,netty开发者设置了一个阈值512,如果selectCnt计数达到了512,说明触发了空轮训,此时 selectRebuildSelector 方法会创建一个新的selector,将原selector上的全部事件重新注册到新selector上。

注:空轮训即调select(time)/select()阻塞方法的时候,由于出现了bug导致不阻塞而是直接返回空结果,并且后面每次都这样,仿佛螺丝滑了丝一般顺滑,,,

三、processSelectedKeys()方法

点进去看到里面的逻辑,第一个方法是优化之后的处理,第二个是未优化的处理,一般都是走优化的逻辑。

private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

processSelectedKeysOptimized方法如下:

 private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a); // 从attachment中取出之前放入的AbstractNioChannel对象,进行处理
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}

继续跟进针对单个SelectionKey的处理:

 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
// 针对无效key的处理
} try {
int readyOps = k.readyOps(); // 获取已经就绪的操作类型
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// 1、针对连接事件的处理
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
} if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// 2、针对写事件的处理
ch.unsafe().forceFlush();
} ///3、针对读事件/接受连接事件的处理
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}

可以看到,在此方法中按不同的事件类型调用unsafe方法对其进行处理,再往后追溯就是pipeline的相关处理了,具体内容较多,有兴趣可自行查看,后面有机会博主也会继续更新。

有一点需要着重提的是对ACCEPT事件的处理(服务端在接收到客户端的连接请求时触发该事件),因为是服务端,所以进入AbstractNioMessageChannel.NioMessageUnsafe#read方法,

可以看到有段do/while循环,如下:

 do {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
} allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());

doReadMessages方法的实现位于NioServerSocketChannel中,可以看到第五行往buf中添加了一个NioSocketChannel对象。

 protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t);
try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
}
return 0;
}

再跳出来回到read方法,往下看有个for循环,开始了pipeline的调用,结合前面【https://www.cnblogs.com/zzq6032010/p/13034608.html】bind方法的博文可以知道,此时pipeline中除了头尾两个节点以外,还有一个ServerBootstrapAcceptor,此处最终就会调到ServerBootstrapAcceptor的channelRead方法,该方法很重要,最终将上面生成的NioSocketChannel中的pipeline、channelOption、attr初始化,然后注册到childGroup上。至此,服务端具备了与客户端通信的能力,可正常处理read、write事件了。

 int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}

四、runAllTasks()

再粘贴一下runAllTasks附近的代码:

 final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}

首先说一下ioRatio变量,此变量控制的是当前线程中处理channel事件和处理任务队列所用的时间比,如果为50(即50%),则二者用的时间相同,从上面代码中可以看出,ioTime即处理channel事件所用的时间,当ioRatio=50时,runAllTasks的入参就是ioTime;而如果ioRatio=10,则runAllTasks入参为9*ioTime,即处理任务队列的最大时间是处理channel事件的9倍。

下面是runAllTasks方法代码:

 protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
safeExecute(task);
runTasks ++;
if ((runTasks & 0x3F) == 0) { // 每隔64次计算一下超时时间
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}

整体逻辑不难,用一个for循环来依次取出任务处理,并且为了提高效率,每隔64次计算一下超时时间(对netty开发者来说,获取系统纳秒时间也是一笔性能开支,能少获取就少获取)。

 总结

netty中最核心的run方法就介绍到这里,至此,netty进行数据传输前的准备工作都已经过了一遍,但对于netty具体发送、接收数据的流程还未涉及到。netty具体发送、接收数据是借助pipeline和在childHandler中添加的处理器完成的,这部分将不定期的在后面博文中讲述,具体看缘分吧。

Netty源码学习系列之5-NioEventLoop的run方法的更多相关文章

  1. Netty源码学习系列之4-ServerBootstrap的bind方法

    前言 今天研究ServerBootstrap的bind方法,该方法可以说是netty的重中之重.核心中的核心.前两节的NioEventLoopGroup和ServerBootstrap的初始化就是为b ...

  2. Netty源码学习系列之1-netty的串行无锁化

    前言 最近趁着跟老东家提离职之后.到新公司报道之前的这段空闲时期,着力研究了一番netty框架,对其有了一些浅薄的认识,后续的几篇文章会以netty为主,将近期所学记录一二,也争取能帮未对netty有 ...

  3. Netty源码学习系列之1-NioEventLoopGroup的初始化

    前言 NioEventLoopGroup是netty对Reactor线程组这个抽象概念的具体实现,其内部维护了一个EventExecutor数组,而NioEventLoop就是EventExecuto ...

  4. Netty源码学习(三)NioEventLoop

    0. NioEventLoop简介 NioEventLoop如同它的名字,它是一个无限循环(Loop),在循环中不断处理接收到的事件(Event) 在Reactor模型中,NioEventLoop就是 ...

  5. Netty源码学习系列之2-ServerBootstrap的初始化

    前言 根据前文我们知道,NioEventLoopGroup和NioEventLoop是netty对Reactor线程模型的实现,而本文要说的ServerBootstrap是对上面二者的整合与调用,是一 ...

  6. Netty 源码学习——EventLoop

    Netty 源码学习--EventLoop 在前面 Netty 源码学习--客户端流程分析中我们已经知道了一个 EventLoop 大概的流程,这一章我们来详细的看一看. NioEventLoopGr ...

  7. Netty 源码学习——客户端流程分析

    Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...

  8. Netty 源码解析(七): NioEventLoop 工作流程

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第七篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  9. Netty 源码分析系列(二)Netty 架构设计

    前言 上一篇文章,我们对 Netty做了一个基本的概述,知道什么是Netty以及Netty的简单应用. Netty 源码分析系列(一)Netty 概述 本篇文章我们就来说说Netty的架构设计,解密高 ...

随机推荐

  1. 一、kafka 介绍 && kafka-client

    一.kafka 介绍 1.1.kafka 介绍 Kafka 是一个分布式消息引擎与流处理平台,经常用做企业的消息总线.实时数据管道,有的还把它当做存储系统来使用. 早期 Kafka 的定位是一个高吞吐 ...

  2. Tomcat 配置必备的 10 个小技巧

    现在开发Java Web应用,建立和部署Web内容是一件很简单的工作.使用Jakarta Tomcat作为Servlet和JSP容器的人已经遍及全世界.Tomcat具有免费.跨平台等诸多特性,并且更新 ...

  3. 涨见识了,在终端执行 Python 代码的 6 种方式!

    原作:BRETT CANNON 译者:豌豆花下猫@Python猫 英文:https://snarky.ca/the-many-ways-to-pass-code-to-python-from-the- ...

  4. Centos网络配置文件详解

    配置文件位置: 根目录下面的etc下面的sysconfig下面的network-scripts下面的网卡名称配置文件. /etc/sysconfig/network-scripts/网卡名称 如图:我 ...

  5. 简述hadoop安装步骤

    简述hadoop安装步骤 安装步骤: 1.安装虚拟机系统,并进行准备工作(可安装- 一个然后克隆) 2.修改各个虚拟机的hostname和host 3.创建用户组和用户 4.配置虚拟机网络,使虚拟机系 ...

  6. 详解 Flink DataStream中min(),minBy(),max(),max()之间的区别

    解释 官方文档中: The difference between min and minBy is that min returns the minimum value, whereas minBy ...

  7. 2 个步骤为 VSCode 配置工程头文件路径!

    我用 VSCode 来 Coding,这个编辑器需要自己配置头文件路径,就是自动建立一个 c_cpp_properties.json 文件来管理头文件路径,然后需要用哪些库就手动加上即可,方法很简单, ...

  8. PAT 1041 Be Unique (20分)利用数组找出只出现一次的数字

    题目 Being unique is so important to people on Mars that even their lottery is designed in a unique wa ...

  9. EIGRP-13-弥散更新算法-停滞在活动状态

    如果一台路由器参与到了针对某个目的地的弥散计算中(即将相应路由置为活动状态,并发送查询包),它必须首先等待所有邻居都返回响应包,之后它才能执行自已的弥散计算,接着选出新的最优路径,最后开始发送自已的响 ...

  10. mysql字符串类型(TEXT 类型)

    TEXT 类型 TEXT 列保存非二进制字符串,如文章内容.评论等.当保存或查询 TEXT 列的值时,不删除尾部空格. TEXT 类型分为 4 种:TINYTEXT.TEXT.MEDIUMTEXT 和 ...