Netty4.1源码分析—— 服务端启动过程

本文只代表笔者一人的理解和叙述,笔者功力尚浅,如有错误,还请各位大神斧正。

一、引言与结论

先说结论,NettyServer启动过程简单来说是加载配置以及为即将到来的连接做接收的准备,也就是为客户端连接服务端这一步做好准备。因为Netty的架构是Reactor模式的实现,所以这一步其实是让ServerSockerChannel注册上OP_ACCEPT事件,即接受连接事件。

谈及ServerSockerChannel,我们需要理解Netty中的sockerChannel分为服务端的ServerSocketChannelSocketChannel以及客户端的SocketChannel。不同的SocketChannel对不同的事件感兴趣,下面表格给出了各自SocketChannel感兴趣的事件:

client/serverchannelOP_ACCEPTOP_CONNECTOP_WRITEOP_READ
clientSocketChannelNYYY
serverServerSocketChannelYNNN
serverSocketChannelNNYY

这三个不同的SockerChannel关系可以这么理解:

  • ServerSocketChannel只对即将到来的OP_ACCEPT感兴趣
  • 客户端发起连接,ServerSocketChannel响应OP_ACCEPT事件创建出服务端的SocketChannel
  • 客户端的SocketChannel与服务端的SocketChannel是一对一的关系,是这次连接的抽象体现

为了方便以及篇幅的原因,此次源码分析只采用NIO的实现进行分析,其它如Epoll、OIO等读者可自行按照该篇分析去源码里查看。

二、NioEventLoopGroup、NioEventLoop

NioEventLoopGroup是一个事件循环组,它的作用是创建并管理其内部的NioEventLoop。其内部采用了池化技术的思想,所以我们可以简单的将它理解为一个线程池,只不过这个线程池并和我们熟知的JDK线程池ThreadPoolExecutor不怎么相同,NioEventLoopGroup里面的每个线程都拥有单独的任务队列,同时它也没有JDK线程池那套回收线程的机制。

NioEventLoopGroup里线程具体表现类体现为NioEventLoop类(EventLoop并不等同Thread,可以理解一个EventLoop伴随着一个Thread),在创建NioEventLoopGroup时就已经定好了Group里有多少个NioEventLoop,同时在创建NioEventLoop时调用JDKNIOopenSelector方法创建一个Selector

对于两者的关系我们可以简单的认定为是线程池和线程的关系,但是要注意的是,NioEventLoopGroup没有ThreadPoolExecutor的回收、创建线程策略,而NioEventLoop也不是一个线程,它只是绑定一个线程的Executor

2.1 NioEventLoopGroup

首先来看NioEventLoopGroup类的继承关系:


从图中可以看出,NioEventLoopGroup类最上面实现的是Executor接口,这也从侧面证明了NioEventLoopGroup类其实是一个线程池。从NioEventLoopGroup类点进它的构造器:

public NioEventLoopGroup(ThreadFactory threadFactory) {
    // 创建传入Selector,单例所有的EventLoopGroup共用一个Selector
    this(0, threadFactory, SelectorProvider.provider());
}

...SelectorProvider类
private static SelectorProvider provider = null;
public static SelectorProvider provider() {
    synchronized (lock) {
        if (provider != null)
            return provider;
        return AccessController.doPrivileged(
            //...
                provider = sun.nio.ch.DefaultSelectorProvider.create();
                return provider;
            }
        });
    }
}

在创建一个NioEventLoopGroup时,调用SelectorProvider类提供一个SelectorProvider,而provider()方法采用synchronized加单例的方式创建一个SelectorProvider。因为Boss GroupWork Group都是该类创建SelectorProvider,所以这两者共用一个SelectorProvider。而且此处也是Netty跨平台的实现之一(其实此处调用的是NIO的实现),进入DefaultSelectorProvider.create()源码你会发现如下:

public class DefaultSelectorProvider {
    private DefaultSelectorProvider() {
    }

    public static SelectorProvider create() {
        return new WindowsSelectorProvider();
    }
}

直接创建一个windows版本的Selector Provider,但是如果你查看Mac或者Linux下的源码你会发现,这里被替换成KQueueEpoll的实现。

继续查看NioEventLoopGroup的调用栈,NioEventLoopGroup的构造函数最终调用的是MultithreadEventExecutorGroup的构造方法,在该方法里面创建了一个执行器Executor,并且实例化了多个NioEventLoop:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
    //如果外界未传进一个executor,自己实例化一个......
    if (executor == null) {
        executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
    }
    // 创建线程组,并且实例化每一个
    children = new EventExecutor[nThreads];
    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
            children[i] = newChild(executor, args);
            success = true;
        } 
        //......
    }
    // 初始化一个选择器工厂
    chooser = chooserFactory.newChooser(children);
    //......
}

代码并不难,需要注意的是此处的chooserFactory,该工厂的作用是提供多种选择策略,该策略是在进行一个连接建立时,执行其策略类的next()方法来选择一个EventLoop。选出来的EventLoop会和新创建的SocketChannel绑定,关于这点后面在详细分析。

我们接着看看Netty是如何实例化一个NioEventLoop的,接着进入newChild(executor, args)方法的实现,该方法是个抽象方法,有着Epoll、KQueue、NIO的多种实现,此处我们只分析NIO的实现:

protected abstract EventExecutor newChild(Executor executor, Object... args) throws Exception;

protected EventLoop newChild(Executor executor, Object... args) throws Exception {
    SelectorProvider selectorProvider = (SelectorProvider) args[0];
    SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
    RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
    EventLoopTaskQueueFactory taskQueueFactory = null;
    EventLoopTaskQueueFactory tailTaskQueueFactory = null;

    int argsLength = args.length;
    if (argsLength > 3) {
        taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
    }
    if (argsLength > 4) {
        tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
    }
    return new NioEventLoop(this, executor, selectorProvider,
            selectStrategyFactory.newSelectStrategy(),
            rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
}

需要注意的SelectStrategyFactory是构造Selector进行轮询IO事件的一个策略实现类工厂,这个放在讲轮询IO事件时再详细介绍。

RejectedExecutionHandler,拒绝执行处理handler,其作用是在NioEventLoop所专属的任务队列满了时候拒绝处理的一种策略,这点和JDK线程池的拒绝策略作用是差不多相同的。在Netty中默认队列里超过16个任务就会执行拒绝策略:

protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                        boolean addTaskWakesUp, int maxPendingTasks,
    //...
    this.maxPendingTasks = Math.max(16, maxPendingTasks);
    //...
    taskQueue = newTaskQueue(this.maxPendingTasks);
    rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}

protected void addTask(Runnable task) {
    ObjectUtil.checkNotNull(task, "task");
    //队列满,拒绝任务
    if (!offerTask(task)) {
        reject(task);
    }
}

NioEventLoopGroup和JDK线程池最大的区别在于,在Netty中每个NioEventLoop(一个线程)都有其专属的一个任务队列,而JDK里线程池所有的线程都共享一个任务队列。

2.2 NioEventLoop

同样的,先看NioEventLoop的继承关系:


从类图中看出,NioEventLoop继承自SingleThreadEventLoop,通时该类继承自SingleThreadEventExecutor并且实现了ScheduledExecutorService接口。从字面意义上理解是单线程事件处理器,表明NioEventLoop是单线程的架构,并且作用是处理事件。从它实现的接口来看,它不仅可以执行任务,还能进行任务调度。

2.2.1 如何开启Selector?

从NioEventLoopGroup最终调用NioEventLoop的构造器的处开始阅读:

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
             EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) {
    super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory),
            rejectedExecutionHandler);
    this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
    this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
    final SelectorTuple selectorTuple = openSelector();
    //......
}

private SelectorTuple openSelector() {
    final Selector unwrappedSelector;
    try {
        unwrappedSelector = provider.openSelector();
    } catch (IOException e) {
        throw new ChannelException("failed to open a new selector", e);
    }
    //.....
    selectedKeys = selectedKeySet;
}

在这一步,自然最重要的方法就是openSelector了,简明扼要的打开了一个Selector,在windows下,这里就是WindowsSelectorProvider调用openSelector方法创建一个Selector,而openSelector方法是JDK NIO的实现,就不在此展开讨论了。

2.2.2 NioEventLoop的启动

既然NioEventLoop是一个Executor,自然就应该从它的execute提交任务方法开始着手进行分析,可以从它的父类SingleThreadEventLoop中找到它的execute方法:

private void execute(Runnable task, boolean immediate) {
    boolean inEventLoop = inEventLoop();
    // 增加到任务队列
    addTask(task);
    // 不是则开启此线程
    if (!inEventLoop) {
        startThread();
        //....
    }
    //....
}

从代码里可以看出execute方法首先判断当前线程是不是EventLoop绑定的线程,然后把任务加入该NioEventLoop对应的任务队列里。对于一个新的NioEventLoop而言,它的线程上下文肯定是在其它的线程环境中,所以这一步inEventLoop一定是false。也就是说,EventLoop伴随的线程的启动是该EventLoop第一次执行任务的时候。

startThread方法是调用CAS确保NioEventLoop对应的线程没有被启动过,所以略过startThread直接看它调用的doStartThread方法:

private void doStartThread() {
    assert thread == null;
    // 创建一个线程
    executor.execute(new Runnable() {
        @Override
        public void run() {
            // 赋值当前线程给EventLoop
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }

            boolean success = false;
            updateLastExecutionTime();
            try {
                // 执行事件循环
                SingleThreadEventExecutor.this.run();
                success = true;
            } 
            //......

在这一步,调用了一个executorexecute方法。如果读者记忆好的话,应该会记得上文分析NioEventLoopGroup创建的时候,会传入一个ThreadPerTaskExecutor类,也就是此处的executor。来看它源码:

public final class ThreadPerTaskExecutor implements Executor {
    private final ThreadFactory threadFactory;

    public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
        this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory");
    }

    @Override
    public void execute(Runnable command) {
        threadFactory.newThread(command).start();
    }
}

该类的execute方法是直接创建一个线程并开启该线程,也就是在这一步伴随NioEventLoop一生的线程才被真正的创建好并执行,执行的主要逻辑则是 SingleThreadEventExecutorrun方法,即Netty中的事件循环。

NioEventLoop从创建到启动小结一下:

  • 第一步:调用JDKNIO方法开启一个Selector,这个Selector是当前操作系统所支持的Selector,在Linux下就是Epoll的Selector,在Mac下就是KQueue的Selector。
  • 第二步:在第一次执行任务时,判断当前线程是否是EventLoop线程,不管是否是都将任务加入任务队列,如果不是则进行第三步。
  • 第三步:调用ThreadPerTaskExecutor类创建一个线程,将事件循环作为任务传入,并启动该线程。

2.2.3 NioEventLoop的事件循环

对于已经绑定线程的EventLoop来说,除非它被销毁,不然这个线程会伴随它的整个生命周期。Netty EventLoop线程模型带来的最终好处就是减少了线程切换上下文带来的开销。Netty中,一个SocketChannel只能绑定一个EventLoop,也就是这个channel上产生的所有IO事件都会由EventLoop所完成,也就是在一个线程中完成。

run方法内部是一个死循环,不断地从Selector中取出事件来执行。因为这个方法比较长,在分析这个方法前,先分步骤介绍该方法。

2.2.3.1 SelectStrategy

SelectStrategy表明此次Selector该怎么执行的策略,它有三个值:

  • CONTINUE:表明此次循环直接跳过,不执行后续步骤,这一步也意味着IO事件没有发生。
  • BUSY_WAIT:忙等待是NIO所不支持的。
  • SELECT:表明EventLoop的任务队列里没有任务,于是采用select(timeout)来等待IO事件,timeout的值取决于到下一次定时任务被调度的时间差值。

明白了上述三个值,可以来看看EventLoop中是如何计算这个策略的:

//计算select策略
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());

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

//3
final class DefaultSelectStrategy implements SelectStrategy {
    static final SelectStrategy INSTANCE = new DefaultSelectStrategy();
    
    private DefaultSelectStrategy() { }
    
    @Override
    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }
}

调用DefaultSelectStrategycalculateStrategy方法,判断队列中是否有任务。如果有任务,则调用selectNow方法,没有任务则返回SELECT状态。关于selectNow方法其实调用的是NIO Selector的几个select方法,它们分别如下:

  • select(): 阻塞到至少有一个通道在你注册的事件上就绪。返回的int值表示有多少通道已经就绪。
  • select(long timeout):与select()一样,但是最长会阻塞timeout毫秒来等待。
  • selectNow(): 不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。

了解了这些后再来看EventLoop是如何对于上述状态进行处理的:

int strategy;
try {
    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
    switch (strategy) {
    // 继续循环
    case SelectStrategy.CONTINUE:
        continue;
    // 忙等待,NIO不支持
    case SelectStrategy.BUSY_WAIT:
        // fall-through to SELECT since the busy-wait is not supported with NIO
    // 代表需要等待一段时间,调用select(timeout)
    // timeout是到下一次调度任务执行时间的间隔
    case SelectStrategy.SELECT:
        // 计算到下一次任务的调度死亡时间线
        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
        // 等于-1则调用select()方法进行阻塞
        if (curDeadlineNanos == -1L) {
            curDeadlineNanos = NONE; // nothing on the calendar
        }
        nextWakeupNanos.set(curDeadlineNanos);
        try {
            // 如果没有任务,执行select(timeout)方法
            if (!hasTasks()) {
                // 根据curDeadlineNanos的值调用select(timeout)或者select()
                strategy = select(curDeadlineNanos);
            }
        } finally {
            // This update is just to help block unnecessary selector wakeups
            // so use of lazySet is ok (no race condition)
            nextWakeupNanos.lazySet(AWAKE);
        }
        // fall through。默认表示有IO事件产生
    default:
    }
} catch (IOException e) {
    // 出现IO异常则重构selector
    // If we receive an IOException here its because the Selector is messed up. Let's rebuild
    // the selector and retry. https://github.com/netty/netty/issues/8566
    rebuildSelector0();
    selectCnt = 0;
    handleLoopException(e);
    continue;
}

调用selectNow还是调用select(timeout)是取决于任务队列是是否有任务而言的。对于任务队列中有任务来说,假如没有IO事件产生,则策略返回CONTINUE继续下一次循环,有任务则走default分支进行处理IO事件;而对于任务队列中没有任务来说,都是返回SELECT策略,表明调用select(timeout)方法来进行阻塞事件循环,阻塞的时间取决于到下次任务被调度的时间差。而如果没有定时任务,那么就调用select()方法进行阻塞。如图:


执行多种策略的好处就是可以为了避免线程一直循环空跑,浪费CPU资源。在获取新的IO事件或者其它任务要阻塞线程,避免一直循环,等待有新的IO事件或者任务的时候才唤醒线程。

2.2.3.2 EventLoop中的任务队列

EventLoop中的任务队列总共分为三种,也就是不同的任务种类保存在不同的队列中:

  • taskQueue:也就是普通的任务,该队列存储IO任务和非定时的普通任务,比如safeExecute提交的任务。
  • scheduleTaskQueue: 如同上文所说,EventLoop实现了ScheduledExecutorService接口,也就意味着它拥有了定时处理调度任务的能力。
  • tailTaskQueue:目前在Netty的源码中没有看到应用的场景,可能是为了以后扩展。
2.2.3.3 EventLoop的任务执行

在Netty中,任务的种类如果从功能性区分,则分为普通任务和定时任务。而如果从业务类型上区分,那么就分为IO任务和非IO任务,这里的非IO任务指的是不涉及到处理IO操作的一些操作。也就意味着涉及到一个连接的逻辑业务和处理IO事件都是在一个EventLoop内执行,即在一个线程内执行。在这时候分配两者的时间占比就显得尤为重要了,也因此Netty在此处设计了一个ioRatio变量,处理这种情况:

 // ioRatio表示一次循环IO任务的占比,任务分为IO任务和非IO任务,比如doBind之类的
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) {
    try {
        // 有IO事件
        if (strategy > 0) {
            processSelectedKeys();
        }
    } finally {
        // 即便IO 100%,还是要处理非IO任务的
        // Ensure we always run tasks.
        ranTasks = runAllTasks();
    }
} else if (strategy > 0) {
    final long ioStartTime = System.nanoTime();
    try {
        processSelectedKeys();
    } finally {
        // 计算io任务的时间,根据该时间和ioRatio来计算出非io任务的执行时间
        // Ensure we always run tasks.
        final long ioTime = System.nanoTime() - ioStartTime;
        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    }
} else {
    ranTasks = runAllTasks(0); // This will run the minimum number of tasks
}

从源码可以看出,ioRatio变量指的是IO任务在一次EventLoop里所占的时间比例,它默认是50,即占比50%。当ioRatio变为100的时候,Netty将首先执行处理完所有的IO事件,然后再去处理所有的非IO任务,这种场景非常适合于IO密集型的程序,比如文件传输等等。而如果ioRatio不是100,那么Netty还是首先处理IO任务,然后根据处理IO任务的时间来计算出执行非IO任务的限定时间。

Netty通过巧妙的设置ioRatio来平衡IO任务和非IO任务的时间,ioRatio越低则越适合于CPU密集型的程序。但是Netty终究是一个IO框架,必须保证IO事件被处理才能取处理其它的逻辑业务。结合strategy所画的图,将这部分逻辑加上:


2.2.3.4 非IO任务执行

运行非IO任务有两个方法,两者互为重构,即runAllTasks()方法和即runAllTasks(long timeoutNanos)方法。前者负责在一次事件循环中跑完所有队列中的任务,后者负责在给定时间内执行队列中的任务。首先来看runAllTasks()方法:

protected boolean runAllTasks() {
    assert inEventLoop();
    // 表示是否scheduledTask里面还有任务
    boolean fetchedAll;
    boolean ranAtLeastOne = false;

    do {
        // 从scheduledTask中定时任务塞进taskQueue
        fetchedAll = fetchFromScheduledTaskQueue();
        if (runAllTasksFrom(taskQueue)) {
            // 至少运行一次标记
            ranAtLeastOne = true;
        }
    } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.

    if (ranAtLeastOne) {
        lastExecutionTime = ScheduledFutureTask.nanoTime();
    }
    // 执行tailTaskQueue里面的任务
    afterRunningAllTasks();
    return ranAtLeastOne;
}

代码很简单,只要队列中有任务,那么就一直循环将任务跑完。需要注意的是fetchFromScheduledTaskQueue方法,该方法是以当前时间为标准,将scheduledTaskQueue队列里执行时间到达此时间的任务取出放在taskQueue里面:

private boolean fetchFromScheduledTaskQueue() {
    if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) {
        return true;
    }
    long nanoTime = AbstractScheduledEventExecutor.nanoTime();
    for (;;) {
        // 将执行时间到达的定时任务取出
        Runnable scheduledTask = pollScheduledTask(nanoTime);
        if (scheduledTask == null) {
            return true;
        }
        // 添加到taskQueue失败,重新加入scheduledTaskQueue
        if (!taskQueue.offer(scheduledTask)) {
            // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
            scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask);
            return false;
        }
    }
}

runAllTasks(long timeoutNanos)方法和runAllTasks方法的逻辑并无太大区别,只是多了一个时间检测:

// 每执行完64次任务检测一次
// XXX: Hard-coded value - will make it configurable if it is really a problem.
if ((runTasks & 0x3F) == 0) {
    lastExecutionTime = ScheduledFutureTask.nanoTime();
    if (lastExecutionTime >= deadline) {
        break;
    }
}

从这个检测来说,Netty在执行非IO任务给的限定时间并不完全精确,因为有可能业务逻辑导致一次任务运行时间较长,而检测是每运行64个任务检测一次。也正是由于这个原因,Netty中的自定义handler不应该做耗时较长的任务。

2.2.3.5 IO任务执行

IO任务就是对SocketChannel感兴趣的几个事件进行处理,而Netty在这个基础上做了一些优化:

private void processSelectedKeys() {
    // selectedKeys是一个数组,在openSelector时已经关联到selector,调用反射将值放在里面
    if (selectedKeys != null) {
        // 数组遍历,比原生Set集合迭代要快2%左右
        processSelectedKeysOptimized();
    } else {
        // Set集合遍历keys
        processSelectedKeysPlain(selector.selectedKeys());
    }
}

selectedKeys不等于null的时候,采取processSelectedKeysOptimized方法,等于则采取processSelectedKeysPlain方法。事实上这两者的处理逻辑都差不多一样,其内部都是调用的是processSelectedKey方法来处理SocketChannel感兴趣的事件。区别在于前者在遍历selectedKeys时采取的是数组遍历,后者依旧是NIO的默认Set集合遍历(Iterator),据网上统一的说法前者要比后者快2%左右的性能。两者逻辑差不多,就只分析前者了:

private void processSelectedKeysOptimized() {
    for (int i = 0; i < selectedKeys.size; ++i) {
        final SelectionKey k = selectedKeys.keys[i];
        // 方便GC
        selectedKeys.keys[i] = null;
        // 这里一般取出的是ServerSocketChannel/SocketChannel
        final Object a = k.attachment();

        if (a instanceof AbstractNioChannel) {
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            // 一般不走这一步,除非使用者主动注册NioTask到Selector
            @SuppressWarnings("unchecked")
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }
        // needsToSelectAgain在run方法里面是false,暂不清楚这一步意义在哪
        if (needsToSelectAgain) {
            // null out entries in the array to allow to have it GC'ed once the Channel close
            // See https://github.com/netty/netty/issues/2363
            selectedKeys.reset(i + 1);

            selectAgain();
            i = -1;
        }
    }
}

从源码可以看出,processSelectedKeysOptimized直接进行数组遍历将SelectedKeys中就绪的事件一个个的处理,需要注意的是attachment()方法。这个方法是一个取值的操作,从SelectionKey将值取出来,一般没有特殊情况这里就是ServerSocketChannelSocketChannel。而这个值的设置则是在channel在注册感兴趣的事件时将自身设置里进去,这个后面会再讲到。来继续processSelectedKey方法:

rivate void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    // 检验
    if (!k.isValid()) {
    // ....
    }
    try {
        int readyOps = k.readyOps();
        // 对connect事件处理,处理逻辑为取消channel对OP_CONNECt的关注
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }
        // 处理OP_WRITE
        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();
        }
        // 处理OP_READ or OP_ACCEPT
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}

AbstractNioChannel.NioUnsafe类是一个特殊的类,一般用在处理IO最终任务的请求上面,比如注册事件、进行连接等,它有多个实现类,分别对应读取数据、处理注册等功能,目前的话只需要知道它能处理对应的事件就行了,后面会详细的讲解。

另外需要注意的是注册和取消对某一个事件关注的操作,是通过操作一个值来实现的。这点如果熟悉NIO的同学应该非常清楚,就比如上面源码在处理OP_CONNECT事件时取消此channel对OP_CONNECT的关注。

NIO中一共分为四个事件,每个事件代表着一个数字值:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

Selector中,为了精确表达语义又尽可能减少空间的使用,采用了一个整数值来表示某一个channel关注的所有事件。如果一个channel只关注OP_READOP_WRITE事件,那么将两者的值进行或操作得到一个新值(兴趣集)设进Selector里,而Selector只需要将兴趣集和某个OP事件的值进行与操作,判断其是否等于0就知道这个channel有没有对该事件关注了。而上述对就绪集里面的事件处理采取了一样的策略。

00001(OP_READ) | 00010(OP_WRITE) = 00011 & 00001 > 0 ? 对READ感兴趣 : 对READ不感兴趣

而如果取消对某一个事件的关注呢?也很简单,将兴趣集与该事件值的反码进行与操作得到一个新的兴趣集,在再设进Selector里就好了:

00001(OP_READ) | 00010(OP_WRITE) = [00011 & ~00001 = 00010](取消对READ的关注)

三、Server初始化与启动

Server初始化与注册是在Server调用bind方法来绑定某一个端口并开启服务,追踪其源码,可以看到最终调用了doBind方法,其源码如下:

private ChannelFuture doBind(final SocketAddress localAddress) {
    // 初始化与注册channel到Selector
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }
    // 完成了直接调用
    if (regFuture.isDone()) {
        // At this point we know that the registration was complete and successful.
        ChannelPromise promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        // 没有完成则将doBind0封装成一个promise,等到后面完成了再调用
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                    // ......
                    doBind0(regFuture, channel, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

initAndRegister方法和doBind0方法是doBind做的两件事情,一个对应初始化和注册channel到Selector上,一个负责对应绑定注册channel关注的事件,后文会分为这两大模块进行分析。里面需要注意的是,doBind0的调用是被封装成一个promise传到后续逻辑中,等待初始化成功后再调用的。

3.1 初始化与注册

首先看initAndRegister方法,这个方法负责的任务是创建ServerSocketChannel并初始化、初始化SocketChannel、创建handler链、初始化外部参数以及将ServerSockerChannel注册到Selector上面。看下源码:

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 反射+工厂创建对应的ServerSocketChannel
        channel = channelFactory.newChannel();
        init(channel);
    } catch (Throwable t) {
        //.....
    }
    ChannelFuture regFuture = config().group().register(channel);
    //.....
    return regFuture;
}

3.1.1 如何创建ServerSocketChannel?

首先调用channelFactory来创建一个channel,这个channel类型是由外部决定的,也就是ServerBootstrap的channel(Class<? extends C> channelClass)方法:

public B channel(Class<? extends C> channelClass) {
    return channelFactory(new ReflectiveChannelFactory<C>(
            ObjectUtil.checkNotNull(channelClass, "channelClass")
    ));
}

从类名看出,创建一个创建channel的反射工厂类,也就是initAndRegister()方法调用的channelFactory。而现在是Server端的启动,所以这里传入的是NioServerSocketChannel类,也就是这里的channel是NioServerSocketChannel。在这里也可以得出结论,ServerSockerChannel是由反射+工厂类来创建的。

这里需要注意,此处的是Netty自身的channel类,在创建该类的时候会调用SelectorProvider创建NIO的ServerSocketChannel

private static ServerSocketChannel newSocket(SelectorProvider provider) {
    try {
        return provider.openServerSocketChannel();
    } catch (IOException e) {
        throw new ChannelException(
                "Failed to open a server socket.", e);
    }
}

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

3.1.2 初始化ServerSocketChannel

创建完ServerSocketChannel后,后面继续调用init方法,init方法是一个抽象类,有ServerBootStrapBootStrap两个实现,目前我们只分析前者:

void init(Channel channel) {
// 初始化ServerSocketChannel option和其它的参数
setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, newAttributesArray());

// 初始化并获取pipeline
ChannelPipeline p = channel.pipeline();

// SocketChannel的相关属性
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);
// ChannelInitializer是一个特殊的handler,使用一次后会被移除
p.addLast(new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(final Channel ch) {
        final ChannelPipeline pipeline = ch.pipeline();
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }

        ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                // 该handler是为初始化SocketChannel做准备
                pipeline.addLast(new ServerBootstrapAcceptor(
                        ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
    }
});
}

首先设置ServerSockerChannel的option和其它的参数,然后通过ChannelInitializer类来设置handler。这也是Netty中常用的写法了,在Server的启动程序里就有用ChannelInitializer类了。ChannelInitializer类是一个handler,这个handler有点特殊,它在使用完一次后就会被移除。

出于这样的特性,它就很适合为Channel设置handler,毕竟没人会想每次数据通过handler链的时候都设置一遍handler。接着又在pipeline里加了ServerBootstrapAcceptor类,这个类就是初始化SocketChannel对应连接的存在,后文在分析。可以得出结论,初始化ServerSocketChannel是设置了其option、handler以及其他的参数,特殊的是初始化SockerChannel会以一个handler的方式加入到ServerSocketChannel的handler链中。

3.1.3 初始化客户端连接(SocketChannel)

上述的handler有两种,一种是从ServerBootStrap的handler方法传进来的handler以及ServerBootStrapchildHandler方法传来的childHandler。前者应用于ServerSockerChannel后者应用SocketChannel。特别注意的是ServerSockerChannel还有个handler,也就是ServerBootstrapAcceptor类。

ServerBootstrapAcceptor类的构造器传入了SocketChannel相关的属性,先看下源码:

// SocketChannel绑定EventLoop,设定配置、handler等
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;

    child.pipeline().addLast(childHandler);

    setChannelOptions(child, childOptions, logger);
    setAttributes(child, childAttrs);

    try {
        // next 一个EventLoop绑定
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}
}

ServerBootstrapAcceptor类继承了ChannelInboundHandlerAdapter类,表明它是一个入栈的handler。在连接请求来领时,它就和ServerSockerChannel一样设置对应的handler、option等,这也表明ServerBootstrapAcceptor类是负责接收客户端连接,并初始化的handler类。需要注意的是childGroup.register(child)方法,该方法是将channel与EventLoopGroup里某一个EventLoop所绑定,在讲解ServerSockerChannel与EventLoop里会详细提到。

3.1.4 Channel如何绑定EventLoop?

上面的步骤全部都执行完了以后,就来到了initAndRegister方法的最后一步:

ChannelFuture regFuture = config().group().register(channel);

这一步是将channel注册到EventLoopGoup里,也就是将channel和EventLoop所绑定。进入register方法,可以查看到NioEventLoopGroup的父类MultithreadEventLoopGroup的调用:

// 选择一个EventLoop
public EventLoop next() {
    return (EventLoop) super.next();
}

@Override
public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

调用next方法选择一个EventLoop,如果读者对上文有映象的话,创建EventLoopGroup的时候传入过一个ChooserFactory,该工厂就是来创建一个进行选择EventLoop策略的工厂。在Netty中有两种实现:

private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
    private final AtomicInteger idx = new AtomicInteger();
    private final EventExecutor[] executors;

    PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
        this.executors = executors;
    }

    @Override
    public EventExecutor next() {
        return executors[idx.getAndIncrement() & executors.length - 1];
    }
}

private static final class GenericEventExecutorChooser implements EventExecutorChooser {
    private final AtomicLong idx = new AtomicLong();
    private final EventExecutor[] executors;

    GenericEventExecutorChooser(EventExecutor[] executors) {
        this.executors = executors;
    }

    @Override
    public EventExecutor next() {
        return executors[(int) Math.abs(idx.getAndIncrement() % executors.length)];
    }
}

PowerOfTwoEventExecutorChooser选择器提供一种与操作来选择其中一个EventLoop,这也是常用的一种操作了。假如m是2的n次方,令n&(m - 1)则会得到一个小于m的一个值,且随着n递增,得到的值会一直在[0, (m-1)]范围内循环。在JDK的HashMap中同样也是这种方法实现,但是这种策略需要EventLoop数量是2的n次方的值。它的策略示例如下:

size = 4 = 0100
1: 0001 & (0100 - 1) = 0001 = 1
3: 0011 & (0100 - 1) = 0011 = 3
5: 0101 & (0100 - 1) = 0001 = 1
6: 0110 & (0100 - 1) = 0010 = 2

GenericEventExecutorChooser的选择策略就是普通的递增取模。这种策略要比上面采取与操作效率要低,从这里也可以看出Netty是一个很注重效率的框架。这两种策略的择取取决于指定传入EventLoopGroup内的EventLoop数量是不是2的n次方:

public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTwoEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}

3.1.5 Channel如何注册到Selector?

从EventLoopGroup中选择了一个EventLoop后,接着调用了它的register方法,在EventLoop的register方法内调用了Unsafe类的register方法,上文提到unsafe类就是Netty中专门处理IO任务的类:

private void register0(ChannelPromise promise) {
    try {
        // ...
        boolean firstRegistration = neverRegistered;
        doRegister();
        neverRegistered = false;
        registered = true;

        // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
        // user may already fire events through the pipeline in the ChannelFutureListener.
        pipeline.invokeHandlerAddedIfNeeded();
        // 设置promise success, 注册ServerSocketChannel那么这里就是调用doBind0
        safeSetSuccess(promise);
        // 通知注册事件
        pipeline.fireChannelRegistered();
        // 注册ServerSocketChannel在这步没有激活,不会往下走
        if (isActive()) {
         //....
        }
    } catch (Throwable t) {
       //....
    }
}

首先直接doRegister进行将ServerSocketChannel进行注册到Selector里面,然后进行广播通知该channel已被注册。safeSetSuccess(promise)这一步是将传进来的promise(参考本文第三节的开头)设为success,读者如果对上文有映象的话,该promise设为success调用的就是doBind0方法。来看最后一步注册:

protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
            return;
        } catch (CancelledKeyException e) {
            //....
        }
    }
}

这里需要注意的是两点。第一点ServerSocketChannel在这里注册到Selector上时并没有注册它感兴趣的事件,只是注册为0,也就是说关于ServerSocketChannel的兴趣集是在初始化ServerSocketChannel后面再设置的。第二点就是此处将ServerSocketChannel作为一个参数注册上去,这也就是在上文叙述到IO任务执行这一小节时,channel的值可以在SelectionKey就续集中用attachment方法拿出来的原因。

3.2. 绑定端口并注册事件

上文提到safeSetSuccess(promise)最终调用了是doBind0方法。在Netty中doBind0方法是绑定Server端一个端口和本地的IP来为客户端提供服务,而也就是这一步会将ServerSocketChannel关注的OP_ACCEPT注册到兴趣集里。

private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {
    
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

将channel的bind方法作为非IO任务提交到事件循环里异步执行:

public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    return pipeline.bind(localAddress, promise);
}

可以看到是调用了pipeline的bind方法,我们知道pipeline可以认作是对应channel的handler链,这一步其实就是将该channel所对应要执行的handler链上所有的handler的bind方法都调用一遍。需要注意的是在Netty的pipeline上有着HeadContext和TailContext的handler节点,表示开始和结束handler,如果读者启动Netty的例子EchoServer服务,DEBUG在这里,就会得到Server端的pipeline handler调用链是下面的构造:


在初始化的时候新加入的ServerBootStrapAcceptor handler类也会在这条链路上,而Netty对pipeline的handler链处理采用的是双向链表来表示的:

abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {
    volatile AbstractChannelHandlerContext next;
    volatile AbstractChannelHandlerContext prev;
    //......
}

public class DefaultChannelPipeline implements ChannelPipeline {
    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;
    //......
}

而在doBind时,Netty的处理是从TailContext往HeadContext处理的,最重要的注册OP_ACCEPT事件则发生在HeadContext的bind方法里:

 public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();

    //....
    // 判断是否channel激活,这一步尚未激活
    boolean wasActive = isActive();
    try {
        doBind(localAddress);
    } catch (Throwable t) {
       //...
    }
    // 到这一步ServerSocketChannel激活
    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    safeSetSuccess(promise);
}

一个channel是否被激活的条件是该channel有没有连接到它的远程节点,也就是是否可以接受和发送数据。在未doBind前ServerSocketChannel肯定是没有被激活的,先进入doBind方法:

protected void doBind(SocketAddress localAddress) throws Exception {
    if (PlatformDependent.javaVersion() >= 7) {
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}

从源码可见,Netty对不同的JDK版本做了兼容的处理,也就是在这一步ServerSocketChannel绑定了一个ip和端口组成的节点。再回到bind方法,当ServerSocketChannel绑定成功激活后,pileline会把ServerSocketChannel激活的消息传递出去,也就是pipeline会从TailContext到HeadContext走一遍调用channelActive方法,它最终调用的是HeadContext的channelActive方法:

public void channelActive(ChannelHandlerContext ctx) {
    ctx.fireChannelActive();
    // 注册读事件,读事件包括创建连接/读取数据
    readIfIsAutoRead();
}

在HeadContext的channelActive方法内,不仅将channel激活的消息广播出去,还调用了readIfIsAutoRead方法,该方法最终也会从TailContext到HeadContext走一遍,只不过这次调用的是read方法,而且最终也还是来到了HeadContext的read方法:

public void read(ChannelHandlerContext ctx) {
    // 实际上是注册 READ/ACCEPT事件
    unsafe.beginRead();
}

protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    final int interestOps = selectionKey.interestOps();
    // OP_ACCEPT = 1 << 4 = 16
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

最终会调用抽象类AbstractNioChannel的doBeginRead方法将OP_ACCEPT事件注册进ServerSockerChannel的兴趣集,在注册前还会判断一下是否被注册过了,等于0则表示channel没有被注册过(0值时初始化的时候注册上去的),而readInterestOp的值在创建NioServerSockerChannel时就已经被传进:

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

四、总结

  1. 在创建NioEventLoopGroup时创建调用NIO实现来创建一个SelectorProvider,此处Netty为不同的平台做了兼容处理。
  2. 在创建NIoEventLoop的时候调用SelectorProvider.openSelector()方法创建一个Selector。
  3. 采用工厂+反射的实现创建NioServerSocketChannel,在创建NioServerSocketChannel时传入其关注的OP_ACCEPT事件,并调用selector.openServerSocketChannel()创建一个NIO的ServerSocketChannel。
  4. 初始化NioServerSocketChannel时,将初始化客户端连接(NioSocketChannel)作为handler加入到NioServerSocketChannel对应的pipeline中。
  5. 初始化的时候调用NIO的register方法将ServerSocketChannel注册进Selector,注意此处设置ServerSocketChannel的感兴趣事件为0。
  6. 绑定节点是最终通过pipeline中的HeadContext handler类完成的,也通过HeadContext的read方法将OP_ACCEPT事件注册进ServerSocketChannel的兴趣集。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值