记一次有趣的 Netty 源码问题
wow20008
8年前
<h2><strong>背景</strong></h2> <p>起因是一个朋友问我的一个关于 ServerBootstrap 启动的问题 .</p> <p>他的问题我复述一下:</p> <p>ServerBootstrap 的绑定流程如下:</p> <pre> <code class="language-javascript">ServerBootstrap.bind -> AbstractBootstrap.bind -> AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister -> AbstractChannel#AbstractUnsafe.register -> eventLoop.execute( () -> AbstractUnsafe.register0) doBind0() -> channel.eventLoop().execute( () -> channel.bind) -> AbstractUnsafe.bind</code></pre> <p>在 <strong>AbstractUnsafe.register0</strong> 中可能会调用 <strong>pipeline.fireChannelActive()</strong> , 即:</p> <pre> <code class="language-javascript">private void register0(ChannelPromise promise) { try { ... boolean firstRegistration = neverRegistered; doRegister(); ... if (firstRegistration && isActive()) { pipeline.fireChannelActive(); } } catch (Throwable t) { ... } }</code></pre> <p>并且在 <strong>AbstractUnsafe.bind</strong> 中也会有 <strong>pipeline.fireChannelActive()</strong> 的调用, 即:</p> <pre> <code class="language-javascript">public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { ... boolean wasActive = isActive(); try { doBind(localAddress); } catch (Throwable t) { ... } if (!wasActive && isActive()) { invokeLater(new OneTimeTask() { @Override public void run() { pipeline.fireChannelActive(); } }); } ... }</code></pre> <p>那么有没有可能造成了两次的 <strong>pipeline.fireChannelActive()</strong> 调用?</p> <p>我的回答是不会. 为什么呢? 对于直接想知道答案的朋友可以直接阅读到最后面的 <strong>回答</strong> 与 <strong>总结</strong> 两节..</p> <p>下面我们就来根据代码详细分析一下.</p> <h2><strong>分析</strong></h2> <p>首先, 根据我们上面所列出的调用流程, 会有 <strong>AbstractBootstrap.doBind</strong> 的调用, 它的代码如下:</p> <pre> <code class="language-javascript">private ChannelFuture doBind(final SocketAddress localAddress) { // 步骤1 final ChannelFuture regFuture = initAndRegister(); ... // 步骤2 if (regFuture.isDone()) { ... doBind0(regFuture, channel, localAddress, promise); ... } else { regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { ... doBind0(regFuture, channel, localAddress, promise); } }); } }</code></pre> <p>首先在 doBind 中, 执行步骤1, 即调用 <strong>initAndRegister</strong> 方法, 这个方法会最终调用到 <strong>AbstractChannel#AbstractUnsafe.register</strong> . 而在 <strong>AbstractChannel#AbstractUnsafe.register</strong> 中, 会通过 eventLoop.execute 的形式将 AbstractUnsafe.register0 的调用提交到任务队列中(即提交到 eventLoop 线程中, 而当前代码所在的线程是 main 线程), 即:</p> <pre> <code class="language-javascript">@Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { // 当前线程是主线程, 因此这个判断是 false if (eventLoop.inEventLoop()) { register0(promise); } else { try { eventLoop.execute(new OneTimeTask() { @Override public void run() { // register0 在 eventLoop 线程中执行. register0(promise); } }); } catch (Throwable t) { ... } } }</code></pre> <p>接着 AbstractBootstrap.initAndRegister 返回, 回到 <strong>AbstractBootstrap.doBind</strong> 中, 于是执行到步骤2. 注意, 因为 <strong>AbstractUnsafe.register0</strong> 是在 <strong>eventLoop</strong> 中执行的, 因此有可能主线程执行到步骤2 时, <strong>AbstractUnsafe.register0</strong> 已经执行完毕了, 此时必然有 <strong>regFuture.isDone() == true</strong> ; 但也有可能 <strong>AbstractUnsafe.register0</strong> 没有来得及执行, 因此此时 <strong>regFuture.isDone() == false</strong> . 所以上面的步骤2 考虑到了这两种情况, 因此分别针对这两种情况做了区分, 即:</p> <pre> <code class="language-javascript">// 步骤2 if (regFuture.isDone()) { ... doBind0(regFuture, channel, localAddress, promise); ... } else { regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { ... doBind0(regFuture, channel, localAddress, promise); } }); }</code></pre> <p>一般情况下, <strong>regFuture.isDone()</strong> 为 false, 因为绑定操作是比较费时的, 因此很大几率会执行到 else 分支, 并且 if 分支和 else 分支从结果上说没有不同, 而且 if 分支逻辑还更简单一些, 因此我们以 else 分支来分析吧. 在 else 分支中, 会为 <strong>regFuture</strong> 设置一个回调监听器. <strong>regFuture</strong> 是一个 ChannelFuture , 而 ChannelFuture 代表了一个 Channel 的异步 IO 的操作结果, 因此这里 regFuture 代表了 Channel 注册(register) 的这个异步 IO 的操作结果.</p> <p>Netty 这里之所以要为 regFuture 设置一个回调监听器, 是为了保证 register 和 bind 的时序上的正确性: Channel 的注册必须要发生在 Channel 的绑定之前 .</p> <p>(关于时序的正确性的问题, 我们在后面有证明)</p> <p>接下来我们来看一下 AbstractUnsafe.register0 方法:</p> <pre> <code class="language-javascript">private void register0(ChannelPromise promise) { try { .... // neverRegistered 一开始是 true, 因此 firstRegistration == true boolean firstRegistration = neverRegistered; doRegister(); neverRegistered = false; registered = true; safeSetSuccess(promise); pipeline.fireChannelRegistered(); // Only fire a channelActive if the channel has never been registered. This prevents firing // multiple channel actives if the channel is deregistered and re-registered. // firstRegistration == true, 而 isActive() == false, // 因此不会执行到 pipeline.fireChannelActive() if (firstRegistration && isActive()) { pipeline.fireChannelActive(); } } catch (Throwable t) { // Close the channel directly to avoid FD leak. closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); } }</code></pre> <p>注意, 我需要再强调一下, 这里 AbstractUnsafe.register0 是在 eventLoop 中执行的.<br> AbstractUnsafe.register0 中会调用 <strong>doRegister()</strong> 注册 <strong>NioServerSocketChannel</strong> , 然后调用 <strong>safeSetSuccess()</strong> 设置 promise 的状态为成功. 而这个 promise 变量是什么呢? 我将 AbstractBootstrap.doBind 的调用链写详细一些:</p> <pre> <code class="language-javascript">AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister -> MultithreadEventLoopGroup.register -> SingleThreadEventLoop.register -> AbstractChannel#AbstractUnsafe.register -> eventLoop.execute( () -> AbstractUnsafe.register0)</code></pre> <p>在 SingleThreadEventLoop.register 中会实例化一个 DefaultChannelPromise, 即:</p> <pre> <code class="language-javascript">@Override public ChannelFuture register(Channel channel) { return register(channel, new DefaultChannelPromise(channel, this)); }</code></pre> <p>接着调用重载的 SingleThreadEventLoop.register 方法:</p> <pre> <code class="language-javascript">@Override public ChannelFuture register(final Channel channel, final ChannelPromise promise) { if (channel == null) { throw new NullPointerException("channel"); } if (promise == null) { throw new NullPointerException("promise"); } channel.unsafe().register(this, promise); return promise; }</code></pre> <p>我们看到, 实例化的 <strong>DefaultChannelPromise</strong> 最终会以方法返回值的方式返回到调用方, 即返回到 <strong>AbstractBootstrap.doBind</strong> 中:</p> <pre> <code class="language-javascript">final ChannelFuture regFuture = initAndRegister();</code></pre> <p>因此我们这里有一个共识: regFuture 是一个在 <strong>SingleThreadEventLoop.register</strong> 中实例化的 DefaultChannelPromise 对象.</p> <p>再回到 <strong>SingleThreadEventLoop.register</strong> 中, 在这里会调用 <strong>channel.unsafe().register(this, promise)</strong> , 将 promise 对象传递到 <strong>AbstractChannel#AbstractUnsafe.register</strong> 中, 因此在 <strong>AbstractUnsafe.register0</strong> 中的 promise 就是 <strong>AbstractBootstrap.doBind</strong> 中的 regFuture .</p> <p>promise == regFuture 很关键.</p> <p>既然我们已经确定了 promise 的身份, 那么调用的 <strong>safeSetSuccess(promise);</strong> 我们也知道是干嘛的了. <strong>safeSetSuccess</strong> 方法设置一个 Promise 的状态为 成功态 , 而 Promise 的 成功态 是最终状态, 即此时 promise.isDone() == true . 那么 设置 promise 为 成功态 后, 会发生什么呢?</p> <p>还记得不 promise == regFuture , 而我们在 <strong>AbstractBootstrap.doBind</strong> 的 else 分支中设置了一个回调监听器:</p> <pre> <code class="language-javascript">final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause = future.cause(); if (cause != null) { // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an // IllegalStateException once we try to access the EventLoop of the Channel. promise.setFailure(cause); } else { // Registration was successful, so set the correct executor to use. // See https://github.com/netty/netty/issues/2586 promise.executor = channel.eventLoop(); } doBind0(regFuture, channel, localAddress, promise); } });</code></pre> <p>因此当 <strong>safeSetSuccess(promise);</strong> 调用时, 根据 Netty 的 Promise/Future 机制, 会触发上面的 <strong>operationComplete</strong> 回调, 在回调中调用 doBind0 方法:</p> <pre> <code class="language-javascript">private static void doBind0( final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) { // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up // the pipeline in its channelRegistered() implementation. 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()); } } }); }</code></pre> <p>注意到, 有一个关键的地方, 代码中将 **channel.bind** 的调用放到了 eventLoop 中执行 . doBind0 返回后, 代码继续执行 AbstractUnsafe.register0 方法的剩余部分代码, 即:</p> <p>当 AbstractUnsafe.register0 方法执行完毕后, 才执行到 <strong>channel.bind</strong> 方法.</p> <p>而 <strong>channel.bind</strong> 方法最终会调用到 <strong>AbstractChannel#AbstractUnsafe.bind</strong> 方法, 源码如下:</p> <pre> <code class="language-javascript">@Override public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { boolean wasActive = isActive(); logger.info("---wasActive: {}---", wasActive); try { // 调用 NioServerSocketChannel.bind 方法, // 将底层的 Java NIO SocketChannel 绑定到指定的端口. // 当 SocketChannel 绑定到端口后, isActive() 才为真. doBind(localAddress); } catch (Throwable t) { ... } boolean activeNow = isActive(); logger.info("---activeNow: {}---", activeNow); // 这里 wasActive == false // isActive() == true if (!wasActive && isActive()) { invokeLater(new OneTimeTask() { @Override public void run() { pipeline.fireChannelActive(); } }); } safeSetSuccess(promise); }</code></pre> <p>上面的代码中, 调用了 <strong>doBind(localAddress)</strong> 将底层的 Java NIO SocketChannel 绑定到指定的端口. 并且 当 SocketChannel 绑定到端口后, isActive() 才为真.<br> 因此我们知道, 如果 SocketChannel 第一次绑定时, 在调用 doBind 前, wasActive == false == isActive() , 而当调用了 <strong>doBind</strong> 后, isActive() == true , 因此第一次绑定端口时, if 判断成立, 会调用 <strong>pipeline.fireChannelActive()</strong> .</p> <h2><strong>关于 Channel 注册与绑定的时序问题</strong></h2> <p>我们在前的分析中, 直接认定了 Channel 注册 在 Channel 的绑定 之前完成, 那么依据是什么呢?</p> <p>其实所有的关键在于 <a href="/misc/goto?guid=4959724468507349884" rel="nofollow,noindex">EventLoop 的任务队列机制</a> .</p> <p>不要闲我啰嗦哦. 我们需要继续回到 AbstractUnsafe.register0 的调用中(再次强调一下, 在 eventLoop 线程中执行AbstractUnsafe.register0), 这个方法我们已经分析了, 它会调用 safeSetSuccess(promise), 并由 Netty 的 Promise/Future 机制, 导致了AbstractBootstrap.doBind 中的 regFuture 所设置的回调监听器的 operationComplete 方法调用, 而 operationComplete 中调用了 <strong>AbstractBootstrap.doBind0</strong> :</p> <pre> <code class="language-javascript">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()); } } }); }</code></pre> <p>在 <strong>doBind0</strong> 中, 根据 EventLoop 的任务队列机制 , 会使用 eventLoop().execute 将 <strong>channel.bind</strong> 封装为一个 Task, 放到 eventLoop 的 taskQueue 中.</p> <p>如下用一幅图表示上面的过程:</p> <p>(图片插入不了, 晕)</p> <p>而当 channel.bind 被调度时, AbstractUnsafe.register0 早就已经调用结束了.</p> <p>因此由于 EventLoop 的任务队列机制, 我们知道, 在执行 AbstractUnsafe.register0 时, 是在 EventLoop 线程中的, 而 <strong>channel.bind</strong> 的调用是以 task 的形式添加到 taskQueue 队列的末尾, 因此必然是有 EventLoop 线程先执行完 <strong>AbstractUnsafe.register0</strong> 方法后, 才有机会从 taskQueue 中取出一个 task 来执行, 因此这个机制从根本上保证了 Channel 注册发生在绑定 之前.</p> <h2><strong>回答</strong></h2> <p>你的疑惑是, <strong>AbstractChannel#AbstractUnsafe.register0</strong> 中, 可能会调用 <strong>pipeline.fireChannelActive()</strong> , 即:</p> <pre> <code class="language-javascript">private void register0(ChannelPromise promise) { try { ... boolean firstRegistration = neverRegistered; doRegister(); ... if (firstRegistration && isActive()) { pipeline.fireChannelActive(); } } catch (Throwable t) { ... } }</code></pre> <p>并且在 <strong>AbstractChannel#AbstractUnsafe.bind</strong> 中也可能会调用到 <strong>pipeline.fireChannelActive()</strong> , 即:</p> <pre> <code class="language-javascript">public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { ... boolean wasActive = isActive(); try { doBind(localAddress); } catch (Throwable t) { ... } if (!wasActive && isActive()) { invokeLater(new OneTimeTask() { @Override public void run() { pipeline.fireChannelActive(); } }); } ... }</code></pre> <p>我觉得是 不会 . 因为根据上面我们分析的结果可知, Netty 的 Promise/Future 与 EventLoop 的任务队列机制保证了 NioServerSocketChannel 的注册和 NioServerSocketChannel 的绑定的时序: Channel 的注册必须要发生在 Channel 的绑定之前 , 而当一个 NioServerSocketChannel 没有绑定到具体的端口前, 它是 不活跃的(Inactive) , 因此在 register0 中, <strong>if (firstRegistration && isActive())</strong> 就不成立, 进而就不会执行到 <strong>pipeline.fireChannelActive()</strong> 了.</p> <p>而执行完注册操作后, 在 <strong>AbstractChannel#AbstractUnsafe.bind</strong> 才会调用 <strong>pipeline.fireChannelActive()</strong> , 因此最终只有一次 fireChannelActive 调用.</p> <h2><strong>总结</strong></h2> <p>有两点需要注意的:</p> <ul> <li> <p>isActive() == true 成立的关键是此 NioServerSocketChannel 已经绑定到端口上了.</p> </li> <li> <p>由 Promise/Future 与 EventLoop 机制, 导致了 <strong>Channel 的注册</strong> 发生在 <strong>Channel 的绑定</strong> 之前, 因此在 <strong>AbstractChannel#AbstractUnsafe.register0</strong> 中的 isActive() == false, if 判断不成立, 最终就是 register0 中的 <strong>pipeline.fireChannelActive()</strong> 不会被调用.</p> </li> </ul> <p> </p> <p>来自:https://segmentfault.com/a/1190000007422683</p> <p> </p>