Netty中NioEventLoop源码分析

版本

本次源码分析基于Netty的版本为4.1

源码分析

NioEventLoop可以视为java中的一个线程,只不过NioEventLoop处理的事件,以及内部的处理逻辑会有所不同。先看一下类的继承关系:


可以看到NioEventLoop实现了很多接口,特别是EventLoop和ScheduledExecutorService,所以NioEventLoop不仅能实现普通的task,还能实现定时task。

Selector

Netty的实现是基于Java原生的NIO的,其对原生的NIO做了很多优化,避免了某些bug,也提升了很多性能。但是底层对于网络IO事件的监听和处理也是离不开多路复用器Selector的。在NioEventLoop的构造方法中进行了Selector的初始化:

final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;

关键还是openSelector()方法,这里我删除了一些分支代码,剩下的做了注释,其中常量 DISABLE_KEY_SET_OPTIMIZATION 的定义如下,可以手工配置是否开启优化,默认是开启优化的,具体优化做了什么事可以查看下面的openSelector()分析。

private static final boolean DISABLE_KEY_SET_OPTIMIZATION = SystemPropertyUtil.getBoolean("io.netty.noKeySetOptimization", false);

netty在创建selector的时候就尝试了优化,具体优化其实是将底层的数据结构从HashSet改为了数组,可以从SelectedSelectionKeySet和SelectorImpl的源码看到这一点,这里就不列了。

private SelectorTuple openSelector() {
    final Selector unwrappedSelector;
    try {
        // 根据底层的IO模型来创建一个selector,这里的selector就是java中NIO的selector
        unwrappedSelector = provider.openSelector();
    } catch (IOException e) {
        throw new ChannelException("failed to open a new selector", e);
    }
    // 如果未开启优化则直接就返回了,SelectorTuple可以视为一个持有selector引用的句柄
    if (DISABLE_KEY_SET_OPTIMIZATION) {
        return new SelectorTuple(unwrappedSelector);
    }

    Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
        @Override
        public Object run() {
            try {
                // 通过反射创建一个selector的具体实例
                return Class.forName(
                        "sun.nio.ch.SelectorImpl",
                        false,
                        PlatformDependent.getSystemClassLoader());
            } catch (Throwable cause) {
                return cause;
            }
        }
    });

    final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
    // netty自己包装的一个selectKey的集合类
    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");

                if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
                    // java 9 以上版本会用Unsafe类直接底层替换SelectionKeySet
                }
                // 利用反射将原生selector中的两个属性替换为netty自己的包装类
                selectedKeysField.set(unwrappedSelector, selectedKeySet);
                publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                return null;
            } catch (NoSuchFieldException e) {
                return e;
            } catch (IllegalAccessException e) {
                return e;
            }
        }
    });

    selectedKeys = selectedKeySet;
    return new SelectorTuple(unwrappedSelector, new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}

run()方法

前面也说了,NioEventLoop其实其实可以类比java中的线程,是一个任务执行单元,所以run()方法是其中的关键,接下来就来分析一下run()方法,源码如下。

@Override
protected void run() {
    for (;;) {
        try {
            try {
                // 如果任务队列非空,那么执行selectNowSupplier代表的方法,也就是selectNow(),否则返回SelectStrategy.SELECT
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                case SelectStrategy.CONTINUE:
                    continue;
                // NioEventLoop不支持,用于EpollEventLoop,理论上不会走到这里
                case SelectStrategy.BUSY_WAIT:
                // 任务队列为空的时候,会执行本逻辑
                case SelectStrategy.SELECT:
                    select(wakenUp.getAndSet(false));
                    // 源码中有注释为什么这里要调用如下逻辑,感兴趣可以查看源码,因为原因描述太长,这里就省略了
                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                    // fall through
                default:
                }
            } catch (IOException e) {
                // 出现IOException则新建selector,将原有的所有channel重新注册到新的selector,然后关闭老的selector
                rebuildSelector0();
                // 异常处理
                handleLoopException(e);
                continue;
            }

            cancelledKeys = 0;
            needsToSelectAgain = false;
            // 控制IO处理时间的一个变量,默认是50(代表50%)
            final int ioRatio = this.ioRatio;
            if (ioRatio == 100) {
                try {
                    // 处理IO事件
                    processSelectedKeys();
                } finally {
                    // 运行非IO任务,就算ioRatio设置了100,非IO任务还是要执行的
                    runAllTasks();
                }
            } else {
                final long ioStartTime = System.nanoTime();
                try {
                    // 处理IO事件
                    processSelectedKeys();
                } finally {
                    // 根据设置的时间占比运行非IO任务
                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
        // Always handle shutdown even if the loop processing threw an exception.
        try {
            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    return;
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
    }
}

整个run()方法被包裹在一个for循环中,唯一能够结束循环的条件是状态state为SHUTDOWN或者TERMINATED,NioEventLoop继承了SingleThreadEventExecutor,isShuttingDown()和confirmShutdown()都是SingleThreadEventExecutor中的方法。

可以看到,除去异常处理和一些分支流程,整个run()方法不是特别负责,重点在与select()和selectNow()方法,run()方法流程如下图所示:


接下来看一下两个关键方法select()和selectNow()

  1. selectNow()

selectNow会立即出发selector的选择操作,如果有准备就绪的channel,就会返回相应的int值(代表了不同的selectKey的集合),否则返回0。之后如果发现用户手动调用了selector的wakeup()方法,会执行selector.wakeup()操作。

int selectNow() throws IOException {
    try {
        return selector.selectNow();
    } finally {
        // restore wakeup state if needed
        if (wakenUp.get()) {
            selector.wakeup();
        }
    }
}
  1. select()

同样去掉了一些无关主流程的代码,netty在select()方法中的处理逻辑跟java线程池有相似的地方,没有任务的时候都是阻塞的,阻塞时间以最近的任务距离当前时间为准,如果一旦有就绪的channel,则立即进行退出循环进行处理。这里netty还解决了epoll的空轮询bug,如果触发了空轮询判断会重建selector。

private void select(boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        // 计算定时任务队列中最早的任务距离现在的时间,没有任务默认1秒
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

        for (;;) {
            // 如果最早的任务开始时间距离当前时间不足0.5毫秒或者已超时,执行selectNow()方法
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
            if (timeoutMillis <= 0) {
                if (selectCnt == 0) {
                    selector.selectNow();
                    selectCnt = 1;
                }
                break;
            }

            // 队列中有任务,并且selector从false设置为true成功则执行selectNow()方法
            // 源码描述了原因,简单来说,往NioEventLoop中提交任务的时候如果selector未wakeup会调用selector.wakeup()
            // 但如果提交task的时候selector已经wakeup,则不会调用
            // 任务可能被挂起知道selector超时,所以这里做了检测
            if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                selector.selectNow();
                selectCnt = 1;
                break;
            }

            // 为select方法设置超时,防止定时任务饿死
            int selectedKeys = selector.select(timeoutMillis);
            selectCnt ++;

            // 退出循环的条件
            // 1. 存在就绪的channel
            // 2. 老的wakeup状态是true
            // 3. 进入select方法后用户调用了wakeup()方法
            // 4. 有新的定时任务需要处理
            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                break;
            }
            if (Thread.interrupted()) {
                // 线程中断处理
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely because " +
                            "Thread.currentThread().interrupt() was called. Use " +
                            "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                }
                selectCnt = 1;
                break;
            }

            long time = System.nanoTime();
            // 这里有一个处理epoll空轮询bug的逻辑
            // 超过了timeoutMillis时间不认为是空轮询
            // 当select轮询超过设定的次数上限时视为触发空轮询bug,重建selector
            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) {
                // The code exists in an extra method to ensure the method is not too big to inline as this
                // branch is not very likely to get hit very frequently.
                selector = selectRebuildSelector(selectCnt);
                selectCnt = 1;
                break;
            }

            currentTimeNanos = time;
        }
    } catch (CancelledKeyException e) {
        // 略
    }

现在真的有channel就绪了,NioEventLoop会怎么处理呢?回到run()方法,有一段根据设定的时间比例处理IO事件和用户任务的逻辑,分别对应两个方法processSelectedKeys和runAllTasks

  1. processSelectedKeys()

从源码可以看到,processSelectedKeysOptimized和processSelectedKeysPlain的大部分处理逻辑是相同的,区别就在于对selectedKey的迭代逻辑,记得一开始说过如果开启了优化,netty对selectedKey的底层集合进行了优化,将HashSet改为了数组,HashSet底层用HashMap实现,迭代的效率是没有数组高的。

private void processSelectedKeys() {
    // 看文章最开头是否启用优化的设置,如果启用了会走这里
    if (selectedKeys != null) {
        processSelectedKeysOptimized();
    } else {
        processSelectedKeysPlain(selector.selectedKeys());
    }
}

private void processSelectedKeysOptimized() {
    for (int i = 0; i < selectedKeys.size; ++i) {
        final SelectionKey k = selectedKeys.keys[i];
        // 方便GC回收
        selectedKeys.keys[i] = null;

        final Object a = k.attachment();

        // 根据类型不同执行不同的处理逻辑
        if (a instanceof AbstractNioChannel) {
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            // 一般不会走这个分支,除非用户主动注册NioTask到selector,netty单元测试里有案例
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }
        // 如果为true,则重置之后的所有selectKey,并调用selectNow()方法
        // 因为run()方法执行本方法前已经置为false,所以不会进这里
        if (needsToSelectAgain) {
            selectedKeys.reset(i + 1);
            selectAgain();
            i = -1;
        }
    }
}

// 处理逻辑基本与processSelectedKeysOptimized相同
private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
    if (selectedKeys.isEmpty()) {
        return;
    }

    Iterator<SelectionKey> i = selectedKeys.iterator();
    for (;;) {
        final SelectionKey k = i.next();
        final Object a = k.attachment();
        i.remove();

        if (a instanceof AbstractNioChannel) {
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            @SuppressWarnings("unchecked")
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }

        if (!i.hasNext()) {
            break;
        }

        if (needsToSelectAgain) {
            selectAgain();
            selectedKeys = selector.selectedKeys();

            if (selectedKeys.isEmpty()) {
                break;
            } else {
                i = selectedKeys.iterator();
            }
        }
    }
}

既然内部逻辑类似,重点看一下processSelectedKeysOptimized()方法,NioTask分支一般不会走,感兴趣可以看一下netty的单元测试。重点看一下AbstractNioChannel分支,如果attachment是AbstractNioChannel类型,说明它是NioServerSocketChannel或者NioSocketChannel,需要进行IO读写相关的操作。

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    // SelectionKey无效的处理
    if (!k.isValid()) {
        final EventLoop eventLoop;
        try {
            eventLoop = ch.eventLoop();
        } catch (Throwable ignored) {
            // channel没有关联的eventLoop直接返回
            return;
        }
        // channel关联的eventLoop不是本eventLoop,直接返回,不应关闭channel
        if (eventLoop != this || eventLoop == null) {
            return;
        }
        // 关闭channel
        unsafe.close(unsafe.voidPromise());
        return;
    }

    try {
        int readyOps = k.readyOps();
        // 对于NioSocketChannel,连接需要先finishConnect才能继续读写
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            // 下面3行的操作只是将OP_CONNECT从感兴趣选项中移除,防止重复触发
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }

        // 说明有半包消息未发送完成,调用flush发送即可
        if ((readyOps & SelectionKey.OP_WRITE) != 0) {
            // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
            ch.unsafe().forceFlush();
        }

        // unsafe是多态,对于NioServerSocketChannel,read就是接受客户端TCP连接
        // 对于NioSocketChannel,就是从channel中读取ByteBuffer
        // 同时检测readyOps == 0 是解决JDK的循环bug
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}
  1. runAllTasks()
    这个是执行用户任务也就是非IO处理的方法,分为不指定时间和指定时间的两个重载方法。最大的不同就是带时间的方法是有执行时间限制的,防止用户任务长时间阻塞IO事件。
protected boolean runAllTasks() {
    assert inEventLoop();
    boolean fetchedAll;
    boolean ranAtLeastOne = false;

    do {
        // 取一定时间段内的定时任务到普通任务队列里
        fetchedAll = fetchFromScheduledTaskQueue();
        // 运行任务队列里的任务
        if (runAllTasksFrom(taskQueue)) {
            ranAtLeastOne = true;
        }
    } while (!fetchedAll); // 取完所有定时任务为止

    if (ranAtLeastOne) {
        // 记录上次执行完任务的时间
        lastExecutionTime = ScheduledFutureTask.nanoTime();
    }
    afterRunningAllTasks();
    return ranAtLeastOne;
}

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 ++;

        // nanoTime()是耗时的操作,所以这里每执行64个任务才检测一次是否超过时间
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            if (lastExecutionTime >= deadline) {
                break;
            }
        }

        // 执行任务
        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }

    afterRunningAllTasks();
    this.lastExecutionTime = lastExecutionTime;
    return true;
}

总结

从NioEventLoop的源码可以看到,netty在很多地方做了优化,还避免了很多JDK自带的bug

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容