一文聊透Netty IO事件的编排利器pipeline |详解所有IO事件(八)

lemonvita
发布于 2022-7-18 17:24
浏览
0收藏

8. 事件传播


在本文第三小节《3. pipeline中的事件分类》中我们介绍了 Netty 事件类型共分为三大类,分别是 Inbound类事件,Outbound类事件,ExceptionCaught事件。并详细介绍了这三类事件的掩码表示,和触发时机,以及事件传播的方向。

 

本小节我们就来按照 Netty 中异步事件的分类从源码角度分析下事件是如何在 pipeline 中进行传播的。

 

8.1 Inbound事件的传播


在第三小节中我们介绍了所有的 Inbound 类事件,这些事件在 pipeline 中的传播逻辑和传播方向都是一样的,唯一的区别就是执行的回调方法不同。

 

本小节我们就以 ChannelRead 事件的传播为例,来说明 Inbound 类事件是如何在 pipeline 中进行传播的。

 

第三小节中我们提到过,在 NioSocketChannel 中,ChannelRead 事件的触发时机是在每一次 read loop 读取数据之后在 pipeline 中触发的。

               do {
                          ............               
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));

                          ............
       
                    // 在客户端NioSocketChannel的pipeline中触发ChannelRead事件
                    pipeline.fireChannelRead(byteBuf);
  
                } while (allocHandle.continueReading());

从这里可以看到,任何 Inbound 类事件在 pipeline 中的传播起点都是从 HeadContext 头结点开始的。

public class DefaultChannelPipeline implements ChannelPipeline {

    @Override
    public final ChannelPipeline fireChannelRead(Object msg) {
        AbstractChannelHandlerContext.invokeChannelRead(head, msg);
        return this;
    }
    
                    .........
}

ChannelRead 事件从 HeadContext 开始在 pipeline 中传播,首先就会回调 HeadContext 中的 channelRead 方法。

 

在执行 ChannelHandler 中的相应事件回调方法时,需要确保回调方法的执行在指定的 executor 中进行。

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        //需要保证channelRead事件回调在channelHandler指定的executor中进行
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    }

    private void invokeChannelRead(Object msg) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRead(this, msg);
            } catch (Throwable t) {
                invokeExceptionCaught(t);
            }
        } else {
            fireChannelRead(msg);
        }
    }

在执行 HeadContext 的 channelRead 方法发生异常时,就会回调 HeadContext 的 exceptionCaught 方法。并在相应的事件回调方法中决定是否将事件继续在 pipeline 中传播。

final class HeadContext extends AbstractChannelHandlerContext
            implements ChannelOutboundHandler, ChannelInboundHandler {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ctx.fireChannelRead(msg);
        }

       @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            ctx.fireExceptionCaught(cause);
        }
    }

在 HeadContext 中通过 ctx.fireChannelRead(msg) 继续将 ChannelRead 事件在 pipeline 中向后传播。

abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {

    @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
        return this;
    }

}

这里的 findContextInbound 方法是整个 inbound 类事件在 pipeline 中传播的核心所在。

 

因为我们现在需要继续将 ChannelRead 事件在 pipeline 中传播,所以我们目前的核心问题就是通过 findContextInbound 方法在 pipeline 中找到下一个对 ChannelRead 事件感兴趣的 ChannelInboundHandler 。然后执行该 ChannelInboundHandler 的 ChannelRead 事件回调。

 static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        //需要保证channelRead事件回调在channelHandler指定的executor中进行
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    } 

 

ChannelRead 事件就这样循环往复的一直在 pipeline 中传播,在传播的过程中只有对 ChannelRead 事件感兴趣的 ChannelInboundHandler 才可以响应。其他类型的 ChannelHandler 则直接跳过。

 

如果 ChannelRead 事件在 pipeline 中传播的过程中,没有得到其他 ChannelInboundHandler 的有效处理,最终会被传播到 pipeline 的末尾 TailContext 中。而在本文第二小节中,我们也提到过 TailContext 对于 inbound 事件存在的意义就是做一个兜底的处理。比如:打印日志,释放 bytebuffer 。

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
            onUnhandledInboundMessage(ctx, msg);
    }

    protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
        onUnhandledInboundMessage(msg);
        if (logger.isDebugEnabled()) {
            logger.debug("Discarded message pipeline : {}. Channel : {}.",
                         ctx.pipeline().names(), ctx.channel());
        }
    }

    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
                            "Please check your pipeline configuration.", msg);
        } finally {
            // 释放DirectByteBuffer
            ReferenceCountUtil.release(msg);
        }
    }

}

8.2 findContextInbound


本小节要介绍的 findContextInbound 方法和我们在上篇文章《一文聊透 Netty 发送数据全流程》中介绍的 findContextOutbound 方法均是 netty 异步事件在 pipeline 中传播的核心所在。

 

事件传播的核心问题就是需要高效的在 pipeline 中按照事件的传播方向,找到下一个具有响应事件资格的 ChannelHandler 。

 

比如:这里我们在 pipeline 中传播的 ChannelRead 事件,我们就需要在 pipeline 中找到下一个对 ChannelRead 事件感兴趣的 ChannelInboundHandler ,并执行该 ChannelInboudnHandler 的 ChannelRead 事件回调,在 ChannelRead 事件回调中对事件进行业务处理,并决定是否通过 ctx.fireChannelRead(msg) 将 ChannelRead 事件继续向后传播。

 private AbstractChannelHandlerContext findContextInbound(int mask) {
        AbstractChannelHandlerContext ctx = this;
        EventExecutor currentExecutor = executor();
        do {
            ctx = ctx.next;
        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));

        return ctx;
    }

参数 mask 表示我们正在传播的 ChannelRead 事件掩码 MASK_CHANNEL_READ 。

static final int MASK_EXCEPTION_CAUGHT = 1;
    static final int MASK_CHANNEL_REGISTERED = 1 << 1;
    static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;
    static final int MASK_CHANNEL_ACTIVE = 1 << 3;
    static final int MASK_CHANNEL_INACTIVE = 1 << 4;
    static final int MASK_CHANNEL_READ = 1 << 5;
    static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;
    static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;
    static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8;

 

通过 ctx = ctx.next 在 pipeline 中找到下一个 ChannelHandler ,并通过 skipContext 方法判断下一个 ChannelHandler 是否具有响应事件的资格。如果没有则跳过继续向后查找。

 

比如:下一个 ChannelHandler 如果是一个 ChannelOutboundHandler,或者下一个 ChannelInboundHandler 对 ChannelRead 事件不感兴趣,那么就直接跳过。

 

8.3 skipContext


该方法主要用来判断下一个 ChannelHandler 是否具有 mask 代表的事件的响应资格。

 private static boolean skipContext(
            AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) {

        return (ctx.executionMask & (onlyMask | mask)) == 0 ||
                (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0);
    }

 •参数 onlyMask 表示我们需要查找的 ChannelHandler 类型,比如这里我们正在传播 ChannelRead 事件,它是一个 inbound 类事件,那么必须只能由 ChannelInboundHandler 来响应处理,所以这里传入的 onlyMask 为 MASK_ONLY_INBOUND ( ChannelInboundHandler 的掩码表示)

 

 •ctx.executionMask 我们已经在《5.3 ChanneHandlerContext》小节中详细介绍过了,当 ChannelHandler 被添加进 pipeline 中时,需要计算出该 ChannelHandler 感兴趣的事件集合掩码来,保存在对应 ChannelHandlerContext 的 executionMask 字段中。
 

•首先会通过 ctx.executionMask & (onlyMask | mask)) == 0 来判断下一个 ChannelHandler 类型是否正确,比如我们正在传播 inbound 类事件,下一个却是一个 ChannelOutboundHandler ,那么肯定是要跳过的,继续向后查找。

 

 •如果下一个 ChannelHandler 的类型正确,那么就会通过 (ctx.executionMask & mask) == 0 来判断该 ChannelHandler 是否对正在传播的 mask 事件感兴趣。如果该  ChannelHandler 中覆盖了 ChannelRead 回调则执行,如果没有覆盖对应的事件回调方法则跳过,继续向后查找,直到 TailContext 。

 

以上就是 skipContext 方法的核心逻辑,这里表达的核心语义是:

 

 •如果 pipeline 中传播的是 inbound 类事件,则必须由 ChannelInboundHandler 来响应,并且该 ChannelHandler 必须覆盖实现对应的 inbound 事件回调。

 

 •如果 pipeline 中传播的是 outbound 类事件,则必须由 ChannelOutboundHandler 来响应,并且该 ChannelHandler 必须覆盖实现对应的 outbound 事件回调。

 

这里大部分同学可能会对 ctx.executor() == currentExecutor 这个条件感到很疑惑。加上这个条件,其实对我们这里的核心语义并没有多大影响。

 

 •当 ctx.executor() == currentExecutor 也就是说前后两个 ChannelHandler 指定的 executor 相同时,我们核心语义保持不变。

 

 •当 ctx.executor() != currentExecutor 也就是前后两个 ChannelHandler 指定的 executor 不同时,语义变为:只要前后两个 ChannelHandler 指定的 executor 不同,不管下一个ChannelHandler有没有覆盖实现指定事件的回调方法,均不能跳过。 在这种情况下会执行到 ChannelHandler 的默认事件回调方法,继续在 pipeline 中传递事件。我们在《5.3 ChanneHandlerContext》小节提到过 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 会分别对 inbound 类事件回调方法和 outbound 类事件回调方法进行默认的实现。

public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelOutboundHandler {

    @Skip
    @Override
    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
            ChannelPromise promise) throws Exception {
        ctx.bind(localAddress, promise);
    }

    @Skip
    @Override
    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
            SocketAddress localAddress, ChannelPromise promise) throws Exception {
        ctx.connect(remoteAddress, localAddress, promise);
    }

    @Skip
    @Override
    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
            throws Exception {
        ctx.disconnect(promise);
    }

    @Skip
    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise)
            throws Exception {
        ctx.close(promise);
    }

    @Skip
    @Override
    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        ctx.deregister(promise);
    }

    @Skip
    @Override
    public void read(ChannelHandlerContext ctx) throws Exception {
        ctx.read();
    }

    @Skip
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        ctx.write(msg, promise);
    }

    @Skip
    @Override
    public void flush(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
}

而这里之所以需要加入 ctx.executor() == currentExecutor 条件的判断,是为了防止 HttpContentCompressor 在被指定不同的 executor 情况下无法正确的创建压缩内容,导致的一些异常。但这个不是本文的重点,大家只需要理解这里的核心语义就好,这种特殊情况的特殊处理了解一下就好。

 

8.4 Outbound事件的传播


关于 Outbound 类事件的传播,笔者在上篇文章《一文搞懂 Netty 发送数据全流程》中已经进行了详细的介绍,本小节就不在赘述。

 

8.5 ExceptionCaught事件的传播


在最后我们来介绍下异常事件在 pipeline 中的传播,ExceptionCaught 事件和 Inbound 类事件一样都是在 pipeline 中从前往后开始传播。

 

ExceptionCaught 事件的触发有两种情况:一种是 netty 框架内部产生的异常,这时 netty 会直接在 pipeline 中触发 ExceptionCaught 事件的传播。异常事件会在 pipeline 中从 HeadContext 开始一直向后传播直到 TailContext。

 

比如 netty 在 read loop 中读取数据时发生异常:

 try {
               ...........

               do {
                          ............               
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));

                          ............
       
                    //客户端NioSocketChannel的pipeline中触发ChannelRead事件
                    pipeline.fireChannelRead(byteBuf);
  
                } while (allocHandle.continueReading());

                         ...........
        }  catch (Throwable t) {
                handleReadException(pipeline, byteBuf, t, close, allocHandle);
       } 

这时会 netty 会直接从 pipeline 中触发 ExceptionCaught 事件的传播。

private void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf, Throwable cause, boolean close,
                RecvByteBufAllocator.Handle allocHandle) {
             
                    .............

            pipeline.fireExceptionCaught(cause);

                    .............

        }

和 Inbound 类事件一样,ExceptionCaught 事件会在 pipeline 中从 HeadContext 开始一直向后传播。

 @Override
    public final ChannelPipeline fireExceptionCaught(Throwable cause) {
        AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
        return this;
    }

 

第二种触发 ExceptionCaught 事件的情况是,当 Inbound 类事件或者 flush 事件在 pipeline 中传播的过程中,在某个 ChannelHandler 中的事件回调方法处理中发生异常,这时该 ChannelHandler 的 exceptionCaught 方法会被回调。用户可以在这里处理异常事件,并决定是否通过 ctx.fireExceptionCaught(cause) 继续向后传播异常事件。

 

比如我们在 ChannelInboundHandler 中的 ChannelRead 回调中处理业务请求时发生异常,就会触发该 ChannelInboundHandler 的 exceptionCaught 方法。

 private void invokeChannelRead(Object msg) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRead(this, msg);
            } catch (Throwable t) {
                invokeExceptionCaught(t);
            }
        } else {
            fireChannelRead(msg);
        }
    }

 

private void invokeExceptionCaught(final Throwable cause) {
        if (invokeHandler()) {
            try {
                //触发channelHandler的exceptionCaught回调
                handler().exceptionCaught(this, cause);
            } catch (Throwable error) {
                  ........
        } else {
                  ........
        }
    }


再比如:当我们在 ChannelOutboundHandler 中的 flush 回调中处理业务结果发送的时候发生异常,也会触发该 ChannelOutboundHandler 的 exceptionCaught 方法。

private void invokeFlush0() {
        try {
            ((ChannelOutboundHandler) handler()).flush(this);
        } catch (Throwable t) {
            invokeExceptionCaught(t);
        }
    }

我们可以在 ChannelHandler 的 exceptionCaught 回调中进行异常处理,并决定是否通过 ctx.fireExceptionCaught(cause) 继续向后传播异常事件。

 @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {

        .........异常处理.......

        ctx.fireExceptionCaught(cause);
    }
 @Override
    public ChannelHandlerContext fireExceptionCaught(final Throwable cause) {
        invokeExceptionCaught(findContextInbound(MASK_EXCEPTION_CAUGHT), cause);
        return this;
    }

   static void invokeExceptionCaught(final AbstractChannelHandlerContext next, final Throwable cause) {
        ObjectUtil.checkNotNull(cause, "cause");
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeExceptionCaught(cause);
        } else {
            try {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        next.invokeExceptionCaught(cause);
                    }
                });
            } catch (Throwable t) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Failed to submit an exceptionCaught() event.", t);
                    logger.warn("The exceptionCaught() event that was failed to submit was:", cause);
                }
            }
        }
    }

8.6 ExceptionCaught 事件和 Inbound 类事件的区别


虽然 ExceptionCaught 事件和 Inbound 类事件在传播方向都是在 pipeline 中从前向后传播。但是大家这里注意区分这两个事件的区别。

 

在 Inbound 类事件传播过程中是会查找下一个具有事件响应资格的 ChannelInboundHandler 。遇到 ChannelOutboundHandler 会直接跳过。

 

而 ExceptionCaught 事件无论是在哪种类型的 channelHandler 中触发的,都会从当前异常 ChannelHandler 开始一直向后传播,ChannelInboundHandler 可以响应该异常事件,ChannelOutboundHandler 也可以响应该异常事件。

 

由于无论异常是在 ChannelInboundHandler 中产生的还是在 ChannelOutboundHandler 中产生的, exceptionCaught 事件都会在 pipeline 中是从前向后传播,并且不关心 ChannelHandler 的类型。所以我们一般将负责统一异常处理的 ChannelHandler 放在 pipeline 的最后,这样它对于 inbound 类异常和 outbound 类异常均可以捕获得到。

一文聊透Netty IO事件的编排利器pipeline |详解所有IO事件(八)-鸿蒙开发者社区

 异常事件的传播.png
 


                                                                                                                             总结
本文涉及到的内容比较多,通过 netty 异步事件在 pipeline 中的编排和传播这条主线,我们相当于将之前的文章内容重新又回顾总结了一遍。

 

本文中我们详细介绍了 pipeline 的组成结构,它主要是由 ChannelHandlerContext 类型节点组成的双向链表。ChannelHandlerContext 包含了 ChannelHandler 执行上下文的信息,从而可以使 ChannelHandler 只关注于 IO 事件的处理,遵循了单一原则和开闭原则。

 

此外 pipeline 结构中还包含了一个任务链表,用来存放执行 ChannelHandler 中的 handlerAdded 回调和 handlerRemoved 回调。pipeline 还持有了所属 channel 的引用。

 

我们还详细介绍了 Netty 中异步事件的分类:Inbound 类事件,Outbound 类事件,ExceptionCaught 事件。并详细介绍了每种分类下的所有事件的触发时机和在 pipeline 中的传播路径。

 

最后介绍了 pipeline 的结构以及创建和初始化过程,以及对 pipeline 相关操作的源码实现。

 

中间我们又穿插介绍了 ChannelHanderContext 的结构,介绍了 ChannelHandlerContext 具体封装了哪些关于 ChannelHandler 执行的上下文信息。

 

本文的内容到这里就结束了,感谢大家的观看,我们下篇文章见~~~

标签
已于2022-7-18 17:24:52修改
收藏
回复
举报
回复
    相关推荐