4、Netty那些事 - NioEventLoop源码解析

665 阅读6分钟

上篇文章主要分析了NioEventLoopGroup相关实现,其核心逻辑就是创建一定数量的NioEventLoop线程,本文就为大家分析下NioEventLoop相关实现。

一、NioEventLoop简介

NioEventLoop主要用来开启Selector并处理各种I/O事件,本身其实只完成数据流接入工作,具体的逻辑都是委托给其他类完成的,类图如下: image.png 由于每一个NioEventLoop都是一个线程,因此都会执行自己的run方法,不过在分析其实现之前,先看下其父类SingleThreadEventExecutor实现,二者关系千丝百缕。

二、SingleThreadEventExecutor详解

SingleThreadEventExecutor重写了execute接口,相关源码如下:

    @Override
    public void execute(Runnable task) {
        ObjectUtil.checkNotNull(task, "task");
        execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
    }
    
    private void execute(Runnable task, boolean immediate) {
        boolean inEventLoop = inEventLoop();
        // 添加到taskQueue后,在NioEventLoop run方法中进行处理
        addTask(task);
        if (!inEventLoop) {
            // 调用NioEventLoop线程的run方法启动线程,使其可以自旋处理任务
            startThread();
            if (isShutdown()) {
                boolean reject = false;
                try {
                    if (removeTask(task)) {
                        reject = true;
                    }
                } catch (UnsupportedOperationException e) {
                   ... ...
                }
                if (reject) {
                    reject();
                }
            }
        }

        if (!addTaskWakesUp && immediate) {
            // NioEventLoop的run方法中,自旋的时候如果发现没有任务,会进行阻塞,有新的事件来了之后,再唤醒
            wakeup(inEventLoop);
        }
    }

execute方法主要做了以下几件事:

  • addTask将Runnable对象添加到任务队列taskQueue中
  • startThread启动该NioEventLoop线程 自旋并处理io任务
  • Selector.wakeUp唤醒 select(...)阻塞的worker线程 告诉它有新的感兴趣的事件注册过来了 别睡了要开始干活了

其中,inEventLoop方法很有意思,会有两种情况,第一种:主线程中启动bossGroup线程,bossGroup线程启动workerGroup线程。

三、NioEventLoop详解

分析完其父类,下面就让我们看看NioEventLoop相关实现。

3.1. 开启Selector

    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
                 EventLoopTaskQueueFactory queueFactory) {
        super(parent, executor, false, newTaskQueue(queueFactory), 
        final SelectorTuple selectorTuple = openSelector();
        this.selector = selectorTuple.selector;
        this.unwrappedSelector = selectorTuple.unwrappedSelector;
    }

NioEventLoop主要是在初始化的时候,通过调用openSelector方法开启selector。通过上面代码可以看到,selector元数据中包含两个属性,分别为selectorTuple.selector和selectorTuple.unwrappedSelector,那么二者有什么区别呢?别着急,让我们继续往下看。查看 openSelector源码如下:

    private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        try {
            // jdk原生selector
            unwrappedSelector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }
        
        // 判断是否开启了优化,未开启,则返回jdk原生selector
        if (DISABLE_KEY_SET_OPTIMIZATION) {
            return new SelectorTuple(unwrappedSelector);
        }

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

        ... ... 
        final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
        // 使用优化后的SelectedSelectionKeySet对象替换jdk原生的
        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
                    ... ...
                    // 设置为可访问
                    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;
                }
            }
        });

        // 进行替换,并返回selector元数据
        selectedKeys = selectedKeySet;
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }

查看openSelector源码发现,首先通过jdk原生api,创建了unwrappedSelector对象,如果当前没开启优化开关,则直接返回这个对象;如果开启了优化,则通过反射加载sun.nio.ch.SelectorImpl对象,使用优化后的SelectedSelectionKeySet替换SelectorImpl中的selectedKeys和publicSelectedKeys两个HashSet。 selectedKeys表示就绪key的集合,拥有所有操作事件准备就绪的选择key;publicSelectedKeys为外部防伪就绪key的集合。

最终通过SelectedSelectionKeySetSelector包装java原生的selector对象和优化过后的selectedKeySet,放到selector元数据中。

那么,SelectedSelectionKeySet和jdk原生的集合想必主要做了哪些优化呢?其实就是改变了底层的数据结构,用数组替换了HashSet,重写了add和iterator方法,使得数组的迭代效率更高。

3.2. run方法分析

    protected void run() {
        int selectCnt = 0;
        for (;;) {
            try {
                int strategy;
                try {
                    // 当前有任务时,返回selector.selectNow
                    // 当前无任务时,返回SelectStrategy.SELECT,
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                    switch (strategy) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.BUSY_WAIT:
                    case SelectStrategy.SELECT:
                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                        if (curDeadlineNanos == -1L) {
                            curDeadlineNanos = NONE; // nothing on the calendar
                        }
                        nextWakeupNanos.set(curDeadlineNanos);
                        try {
                            // 轮训就绪的channel
                            if (!hasTasks()) {
                                strategy = select(curDeadlineNanos);
                            }
                        } finally {
                            nextWakeupNanos.lazySet(AWAKE);
                        }
                    default:
                    }
                } catch (IOException e) {
                    // 当出现异常的时候,需要重新构建selector
                    rebuildSelector0();
                    selectCnt = 0;
                    handleLoopException(e);
                    continue;
                }
                // 轮训次数++,用来解决jdk空轮训bug
                selectCnt++;
                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                boolean ranTasks;
                if (ioRatio == 100) {
                    try {
                        // I/O操作,根据selectedKey进行出炉
                        if (strategy > 0) {
                            processSelectedKeys();
                        }
                    } finally {
                        // 执行完所有任务
                        ranTasks = runAllTasks();
                    }
                } else if (strategy > 0) {
                    final long ioStartTime = System.nanoTime();
                    try {
                        // I/O操作,根据selectedKey进行出炉
                        processSelectedKeys();
                    } finally {
                        // 按照一定比例执行任务,可能会遗留一部分任务等待下次执行
                        final long ioTime = System.nanoTime() - ioStartTime;
                        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                } else {
                    ranTasks = runAllTasks(0); 
                }

                if (ranTasks || strategy > 0) {
                    selectCnt = 0;
                } else if (unexpectedSelectorWakeup(selectCnt)) { // 解决jdk空轮训bug
                    selectCnt = 0;
                }
            } catch  {

            } 
        }
    }

以上就是run方法主要逻辑,总结来说,run方法主要分为以下三个步骤:

  • 通过select方法轮训就绪的channel
  • 通过processSelectedKeys方法,处理轮训到的selectionkey
  • 通过runAllTasks方法,用来执行队列任务

3.3. select方法分析

run方法中通过轮询调用select方法,看看是否有就绪的channel,轮训的过程中,主要通过调用nio selector的selectNow和select(timeOut)方法,查看源码如下:

    private int select(long deadlineNanos) throws IOException {
        if (deadlineNanos == NONE) {
            return selector.select();
        }
        // Timeout will only be 0 if deadline is within 5 microsecs
        long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
        return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
    }

3.4. processSelectedKeys分析

本方法主要处理select轮训到的就绪key,取出这些SelectionKey以及附件attachment

    private void processSelectedKeys() {
        // 是否使用优化的key
        if (selectedKeys != null) {
            processSelectedKeysOptimized();
        } else {
            // 原始key处理
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }
    
    private void processSelectedKeysOptimized() {
        for (int i = 0; i < selectedKeys.size; ++i) {
            final SelectionKey k = selectedKeys.keys[i];
            // 设置为null,争取被jvm快速回收
            selectedKeys.keys[i] = null;
            // 附件
            final Object a = k.attachment();
            // Netty内部事件
            if (a instanceof AbstractNioChannel) {
                // 根据key的就绪事件,触发对应的事件方法
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }

            if (needsToSelectAgain) {
                // 清空i+1之后的selectedKeys
                selectedKeys.reset(i + 1);
                // 重新选择就绪的key
                selectAgain();
                i = -1;
            }
        }
    }
    
    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        ... ...

        try {
            int readyOps = k.readyOps();
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                ... ...
                unsafe.finishConnect();
            }
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                ... ... 
                ch.unsafe().forceFlush();
            }
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
           ... ... 
        }
    }

processSelectedKeysPlain方法源码就不贴了,处理方式和processSelectedKeysOptimized类似,区别在于一个是hashSet,一个是数组,遍历方式和remove有点区别。最终调用processSelectedKey进行OP_CONNECT、OP_WRITE、OP_READ、OP_ACCEPT等事件处理

3.5. runAllTasks方法分析

本方法主要用来执行taskQueue队列和定时任务队列中的任务,如心跳检测、异步读写等,NioEventLoop会根据I/O事件与taskQueue运行的时间占比计算任务执行的时常,由于一个NioEventLoop线程会管理很多channel,会对应很多任务,一次性都执行可能执行不完,因此每执行64个任务后就会检测是否到了截止时间,到了就不会继续执行了。

    protected boolean runAllTasks(long timeoutNanos) {
        // 从定时任务中获取将要执行的任务,丢到taskQueue中
        fetchFromScheduledTaskQueue();
        // 从taskQueue中获取任务
        Runnable task = pollTask();
        if (task == null) {
            // 执行tailTask中的任务,首位
            afterRunningAllTasks();
            return false;
        }
        // 获取执行的截止时间
        final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            // 执行task的run方法
            safeExecute(task);

            runTasks ++;

            // 每执行64个任务就进行一次是否到了截止时间检查,防止一次执行任务过多,影响其他I/O事件
            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的职责已经很清晰了,简单总结下:

  • run方法,通过自旋selector.select(timeoutMillis)实现阻塞,轮训就绪的key
  • 当轮训到有就绪的key后,通过processSelectedKeys方法,进行不同的事件处理。
  • 同时通过runAllTasks处理taskQueue队列里的任务和其他定时任务等。

其中,唤醒阻塞的方式有两种一是bossGroup接收了新的客户端,将SocketChannel注册到workerGroup的selector之上时,通过selector.wakeUp唤醒;二是selector上感兴趣的事件就绪了有返回值也会唤醒。

NioEventLoop其实还解决了jdk nio空轮训问题,通过维护一个selectCnt变量,当这个变量由于各种异常情况导致大于512时候,会重新构建selector,代码再unexpectedSelectorWakeup方法中,有兴趣的同学可以自行研究下。