一文聊透 Netty IO 事件的编排利器 pipeline | 详解所有 IO 事件的触发时机以及传播路径(上)

本系列Netty源码解析文章基于 4.1.56.Final版本

1. 前文回顾

在前边的系列文章中,笔者为大家详细剖析了 Reactor 模型在 netty 中的创建,启动,运行,接收连接,接收数据,发送数据的完整流程,在详细剖析整个 Reactor 模型如何在 netty 中实现的过程里,我们或多或少的见到了 pipeline 的身影。

Reactor启动后的结构.png

比如在 Reactor 启动的过程中首先需要创建 NioServerSocketChannel ,在创建的过程中会为 NioServerSocketChannel 创建分配一个 pipeline ,用于对 OP_ACCEPT 事件的编排。

当 NioServerSocketChannel 向 main reactor 注册成功后,会在 pipeline 中触发 ChannelRegistered 事件的传播。

当 NioServerSocketChannel 绑定端口成功后,会在 pipeline 中触发 ChannelActive 事件的传播。

主从Reactor组完整结构.png

又比如在 Reactor 接收连接的过程中,当客户端发起一个连接并完成三次握手之后,连接对应的 Socket 会存放在内核中的全连接队列中,随后 JDK Selector 会通知 main reactor 此时 NioServerSocketChannel 上有 OP_ACCEPT 事件活跃,最后 main reactor 开始执行 NioServerSocketChannel 的底层操作类 NioMessageUnsafe#read 方法在 NioServerSocketChannel 中的 pipeline 中传播 ChannelRead 事件。

传播ChannelRead事件.png

最终会在 NioServerSocketChannel 的 pipeline 中的 ServerBootstrapAcceptor 中响应 ChannelRead 事件并创建初始化 NioSocketChannel ,随后会为每一个新创建的 NioSocetChannel 创建分配一个独立的 pipeline ,用于各自 NioSocketChannel 上的 IO 事件的编排。并向 sub reactor 注册 NioSocketChannel ,随后在 NioSocketChannel 的 pipeline 中传播 ChannelRegistered 事件,最后传播 ChannelActive 事件。

传播ChannelRegister事件.png

还有在《Netty如何高效接收网络数据》一文中,我们也提过当 sub reactor 读取 NioSocketChannel 中来自客户端的请求数据时,会在 NioSocketChannel 的 pipeline 中传播 ChannelRead 事件,在一个完整的 read loop 读取完毕后会传播 ChannelReadComplete 事件。

在《一文搞懂Netty发送数据全流程》一文中,我们讲到了在用户经过业务处理后,通过 write 方法和 flush 方法分别在 NioSocketChannel 的 pipeline 中传播 write 事件和 flush 事件的过程。

笔者带大家又回顾了一下在前边系列文章中关于 pipeline 的使用场景,但是在这些系列文章中并未对 pipeline 相关的细节进行完整全面地描述,那么本文笔者将为大家详细的剖析下 pipeline 在 IO 事件的编排和传播场景下的完整实现原理。

内容概要.png

2. pipeline的创建

主从Reactor组完整结构.png

Netty 会为每一个 Channel 分配一个独立的 pipeline ,pipeline 伴随着 channel 的创建而创建。

前边介绍到 NioServerSocketChannel 是在 netty 服务端启动的过程中创建的。而 NioSocketChannel 的创建是在当 NioServerSocketChannel 上的 OP_ACCEPT 事件活跃时,由 main reactor 线程在 NioServerSocketChannel 中创建,并在 NioServerSocketChannel 的 pipeline 中对 OP_ACCEPT 事件进行编排时(图中的 ServerBootstrapAcceptor 中)初始化的。

无论是创建 NioServerSocketChannel 里的 pipeline 还是创建 NioSocketChannel 里的 pipeline , 最终都会委托给它们的父类 AbstractChannel 。

image.png
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {

    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        //channel全局唯一ID machineId+processId+sequence+timestamp+random
        id = newId();
        //unsafe用于底层socket的相关操作
        unsafe = newUnsafe();
        //为channel分配独立的pipeline用于IO事件编排
        pipeline = newChannelPipeline();
    }

    protected DefaultChannelPipeline newChannelPipeline() {
        return new DefaultChannelPipeline(this);
    }

}
public class DefaultChannelPipeline implements ChannelPipeline {

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

    //pipeline中的头结点
    final AbstractChannelHandlerContext head;
    //pipeline中的尾结点
    final AbstractChannelHandlerContext tail;

    //pipeline中持有对应channel的引用
    private final Channel channel;

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

    protected DefaultChannelPipeline(Channel channel) {
        //pipeline中持有对应channel的引用
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        
        ............省略.......

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

       ....................
}

在前边的系列文章中笔者多次提到过,pipeline 的结构是由 ChannelHandlerContext 类型的节点构成的双向链表。其中头结点为 HeadContext ,尾结点为 TailContext 。其初始结构如下:

pipeline的初始结构.png

2.1 HeadContext

    private static final String HEAD_NAME = generateName0(HeadContext.class);

    final class HeadContext extends AbstractChannelHandlerContext
            implements ChannelOutboundHandler, ChannelInboundHandler {
       //headContext中持有对channel unsafe操作类的引用 用于执行channel底层操作
        private final Unsafe unsafe;

        HeadContext(DefaultChannelPipeline pipeline) {
            super(pipeline, null, HEAD_NAME, HeadContext.class);
            //持有channel unsafe操作类的引用,后续用于执行channel底层操作
            unsafe = pipeline.channel().unsafe();
            //设置channelHandler的状态为ADD_COMPLETE
            setAddComplete();
        }

        @Override
        public ChannelHandler handler() {
            return this;
        }

        .......................
    }

我们知道双向链表结构的 pipeline 中的节点元素为 ChannelHandlerContext ,既然 HeadContext 作为 pipeline 的头结点,那么它一定是 ChannelHandlerContext 类型的,所以它需要继承实现 AbstractChannelHandlerContext ,相当于一个哨兵的作用,因为用户可以以任意顺序向 pipeline 中添加 ChannelHandler ,需要用 HeadContext 来固定指向第一个 ChannelHandlerContext 。

在《一文搞懂Netty发送数据全流程》 一文中的《1. ChannelHandlerContext》小节中,笔者曾为大家详细介绍过 ChannelHandlerContext 在 pipeline 中的作用,忘记的同学可以在回看下。

于此同时 HeadContext 又实现了 ChannelInboundHandler 和 ChannelOutboundHandler 接口,说明 HeadContext 即是一个 ChannelHandlerContext 又是一个 ChannelHandler ,它可以同时处理 Inbound 事件和 Outbound 事件。

我们也注意到 HeadContext 中持有了对应 channel 的底层操作类 unsafe ,这也说明 IO 事件在 pipeline 中的传播最终会落在 HeadContext 中进行最后的 IO 处理。它是 Inbound 事件的处理起点,也是 Outbound 事件的处理终点。这里也可以看出 HeadContext 除了起到哨兵的作用,它还承担了对 channel 底层相关的操作。

比如我们在《Reactor在Netty中的实现(启动篇)》中介绍的 NioServerSocketChannel 在向 main reactor 注册完成后会触发 ChannelRegistered 事件从 HeadContext 开始依次在 pipeline 中向后传播。

      @Override
        public void channelRegistered(ChannelHandlerContext ctx) {
            //此时firstRegistration已经变为false,在pipeline.invokeHandlerAddedIfNeeded中已被调用过
            invokeHandlerAddedIfNeeded();
            ctx.fireChannelRegistered();
        }

以及 NioServerSocketChannel 在与端口绑定成功后会触发 ChannelActive 事件从 HeadContext 开始依次在 pipeline 中向后传播,并在 HeadContext 中通过 unsafe.beginRead() 注册 OP_ACCEPT 事件到 main reactor 中。

     @Override
        public void read(ChannelHandlerContext ctx) {
            //触发注册OP_ACCEPT或者OP_READ事件
            unsafe.beginRead();
        }

同理在 NioSocketChannel 在向 sub reactor 注册成功后。会先后触发 ChannelRegistered 事件和 ChannelActive 事件从 HeadContext 开始在 pipeline 中向后传播。并在 HeadContext 中通过 unsafe.beginRead() 注册 OP_READ 事件到 sub reactor 中。

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            //pipeline中继续向后传播channelActive事件
            ctx.fireChannelActive();
            //如果是autoRead 则自动触发read事件传播
            //在read回调函数中 触发OP_ACCEPT或者OP_READ事件注册
            readIfIsAutoRead();
        }

在《一文搞懂Netty发送数据全流程》中介绍的 write 事件和 flush 事件最终会在 pipeline 中从后向前一直传播到 HeadContext ,并在 HeadContext 中相应事件回调函数中调用 unsafe 类操作底层 channel 发送数据。

        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
            //到headContext这里 msg的类型必须是ByteBuffer,也就是说必须经过编码器将业务层写入的实体编码为ByteBuffer
            unsafe.write(msg, promise);
        }

        @Override
        public void flush(ChannelHandlerContext ctx) {
            unsafe.flush();
        }

从本小节的内容介绍中,我们可以看出在 Netty 中对于 Channel 的相关底层操作调用均是在 HeadContext 中触发的。

2.2 TailContext

    private static final String TAIL_NAME = generateName0(TailContext.class);

    final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {

        TailContext(DefaultChannelPipeline pipeline) {
            super(pipeline, null, TAIL_NAME, TailContext.class);
            //设置channelHandler的状态为ADD_COMPLETE
            setAddComplete();
        }

        @Override
        public ChannelHandler handler() {
            return this;
        }
    
        ......................
}

同样 TailContext 作为双向链表结构的 pipeline 中的尾结点,也需要继承实现 AbstractChannelHandlerContext 。但它同时又实现了 ChannelInboundHandler 。

这说明 TailContext 除了是一个 ChannelHandlerContext 同时也是一个 ChannelInboundHandler 。

2.2.1 TailContext 作为一个 ChannelHandlerContext 的作用

TailContext 作为一个 ChannelHandlerContext 的作用是负责将 outbound 事件从 pipeline 的末尾一直向前传播直到 HeadContext 。当然前提是用户需要调用 channel 的相关 outbound 方法。

public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable {

    ChannelFuture write(Object msg);

    ChannelFuture write(Object msg, ChannelPromise promise);

    ChannelOutboundInvoker flush();

    ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);

    ChannelFuture writeAndFlush(Object msg);

}
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {

   @Override
    public ChannelFuture write(Object msg) {
        return pipeline.write(msg);
    }

    @Override
    public Channel flush() {
        pipeline.flush();
        return this;
    }

    @Override
    public ChannelFuture writeAndFlush(Object msg) {
        return pipeline.writeAndFlush(msg);
    }
}
public class DefaultChannelPipeline implements ChannelPipeline {

   @Override
    public final ChannelFuture write(Object msg) {
        return tail.write(msg);
    }

    @Override
    public final ChannelPipeline flush() {
        tail.flush();
        return this;
    }

   @Override
    public final ChannelFuture writeAndFlush(Object msg) {
        return tail.writeAndFlush(msg);
    }

}

这里我们可以看到,当我们在自定义 ChannelHandler 中调用 ctx.channel().write(msg) 时,会在 AbstractChannel 中触发 pipeline.write(msg) ,最终在 DefaultChannelPipeline 中调用 tail.write(msg) 。使得 write 事件可以从 pipeline 的末尾开始向前传播,其他 outbound 事件的传播也是一样的道理。

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

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

}

而我们自定义的 ChannelHandler 会被封装在一个 ChannelHandlerContext 中从而加入到 pipeline 中,而这个用于装载自定义 ChannelHandler 的 ChannelHandlerContext 与 TailContext 一样本质也都是 ChannelHandlerContext ,只不过在 pipeline 中的位置不同罢了。

客户端channel pipeline结构.png
public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker {

    ChannelFuture write(Object msg);

    ChannelFuture write(Object msg, ChannelPromise promise);

    ChannelOutboundInvoker flush();

    ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);

    ChannelFuture writeAndFlush(Object msg);

}

我们看到 ChannelHandlerContext 接口本身也会继承 ChannelInboundInvoker
和 ChannelOutboundInvoker 接口,所以说 ContextHandlerContext 也可以触发 inbound 事件和 outbound 事件,只不过表达的语义是在 pipeline 中从当前 ChannelHandler 开始向前或者向后传播 outbound 事件或者 inbound 事件。

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

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

}

这里表示 write 事件从当前 EchoServerHandler 开始在 pipeline 中向前传播直到 HeadContext 。

客户端channel pipeline结构.png

2.2.2 TailContext 作为一个 ChannelInboundHandler 的作用

最后 TailContext 作为一个 ChannelInboundHandler 的作用就是为 inbound 事件在 pipeline 中的传播做一个兜底的处理。

这里提到的兜底处理是什么意思呢?

比如我们前边介绍到的,在 NioSocketChannel 向 sub reactor 注册成功后之后触发的 ChannelRegistered 事件和 ChannelActive 事件。或者在 reactor 线程读取 NioSocketChannel 中的请求数据时所触发的 channelRead 事件和 ChannelReadComplete 事件。

这些 inbound 事件都会首先从 HeadContext 开始在 pipeline 中一个一个的向后传递。

极端的情况是如果 pipeline 中所有 ChannelInboundHandler 中相应的 inbound 事件回调方法均不对事件作出处理,并继续向后传播。如下示例代码所示:

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

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

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.fireChannelReadComplete();
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelRegistered();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelActive();
    }
}

最终这些 inbound 事件在 pipeline 中得不到处理,最后会传播到 TailContext 中。

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {

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

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) {
            onUnhandledInboundChannelReadComplete();
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            onUnhandledInboundChannelActive();
        }

}

而在 TailContext 中需要对这些得不到任何处理的 inbound 事件做出最终处理。比如丢弃该 msg,并释放所占用的 directByteBuffer,以免发生内存泄露。

    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 {
            ReferenceCountUtil.release(msg);
        }
    }

3. pipeline中的事件分类

在前边的系列文章中,笔者多次介绍过,Netty 中的 IO 事件一共分为两大类: inbound 类事件和 outbound 类事件。其实如果严格来分的话应该分为三类。第三种事件类型为 exceptionCaught 异常事件类型。

而 exceptionCaught 事件在事件传播角度上来说和 inbound 类事件一样,都是从 pipeline 的 HeadContext 开始一直向后传递或者从当前 ChannelHandler 开始一直向后传递直到 TailContext 。所以一般也会将 exceptionCaught 事件统一归为 inbound 类事件。

而根据事件类型的分类,相应负责处理事件回调的 ChannelHandler 也会被分为两类:

  • ChannelInboundHandler :主要负责响应处理 inbound 类事件回调和 exceptionCaught 事件回调。

  • ChannelOutboundHandler :主要负责响应处理 outbound 类事件回调。

那么我们常说的 inbound 类事件和 outbound 类事件具体都包含哪些事件呢?

3.1 inbound类事件

final class ChannelHandlerMask {

    // inbound事件集合
    static final int MASK_ONLY_INBOUND =  MASK_CHANNEL_REGISTERED |
            MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ |
            MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED;

    private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_INBOUND;

    // inbound 类事件相关掩码
    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;

}

netty 会将其支持的所有异步事件用掩码来表示,定义在 ChannelHandlerMask 类中, netty 框架通过这些事件掩码可以很方便的知道用户自定义的 ChannelHandler 是属于什么类型的(ChannelInboundHandler or ChannelOutboundHandler )。

除此之外,inbound 类事件如此之多,用户也并不是对所有的 inbound 类事件感兴趣,用户可以在自定义的 ChannelInboundHandler 中覆盖自己感兴趣的 inbound 事件回调,从而达到针对特定 inbound 事件的监听。

这些用户感兴趣的 inbound 事件集合同样也会用掩码的形式保存在自定义 ChannelHandler 对应的 ChannelHandlerContext 中,这样当特定 inbound 事件在 pipeline 中开始传播的时候,netty 可以根据对应 ChannelHandlerContext 中保存的 inbound 事件集合掩码来判断,用户自定义的 ChannelHandler 是否对该 inbound 事件感兴趣,从而决定是否执行用户自定义 ChannelHandler 中的相应回调方法或者跳过对该 inbound 事件不感兴趣的 ChannelHandler 继续向后传播。

从以上描述中,我们也可以窥探出,Netty 引入 ChannelHandlerContext 来封装 ChannelHandler 的原因,在代码设计上还是遵循单一职责的原则, ChannelHandler 是用户接触最频繁的一个 netty 组件,netty 希望用户能够把全部注意力放在最核心的 IO 处理上,用户只需要关心自己对哪些异步事件感兴趣并考虑相应的处理逻辑即可,而并不需要关心异步事件在 pipeline 中如何传递,如何选择具有执行条件的 ChannelHandler 去执行或者跳过。这些切面性质的逻辑,netty 将它们作为上下文信息全部封装在 ChannelHandlerContext 中由netty框架本身负责处理。

以上这些内容,笔者还会在事件传播相关小节做详细的介绍,之所以这里引出,还是为了让大家感受下利用掩码进行集合操作的便利性,netty 中类似这样的设计还有很多,比如前边系列文章中多次提到过的,channel 再向 reactor 注册 IO 事件时,netty 也是将 channel 感兴趣的 IO 事件用掩码的形式存储于 SelectionKey 中的 int interestOps 中。

接下来笔者就为大家介绍下这些 inbound 事件,并梳理出这些 inbound 事件的触发时机。方便大家根据各自业务需求灵活地进行监听。

3.1.1 ExceptionCaught 事件

在本小节介绍的这些 inbound 类事件在 pipeline 中传播的过程中,如果在相应事件回调函数执行的过程中发生异常,那么就会触发对应 ChannelHandler 中的 exceptionCaught 事件回调。

    private void invokeExceptionCaught(final Throwable cause) {
        if (invokeHandler()) {
            try {
                handler().exceptionCaught(this, cause);
            } catch (Throwable error) {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                        "An exception {}" +
                        "was thrown by a user handler's exceptionCaught() " +
                        "method while handling the following exception:",
                        ThrowableUtil.stackTraceToString(error), cause);
                } else if (logger.isWarnEnabled()) {
                    logger.warn(
                        "An exception '{}' [enable DEBUG level for full stacktrace] " +
                        "was thrown by a user handler's exceptionCaught() " +
                        "method while handling the following exception:", error, cause);
                }
            }
        } else {
            fireExceptionCaught(cause);
        }
    }

当然用户可以选择在 exceptionCaught 事件回调中是否执行 ctx.fireExceptionCaught(cause) 从而决定是否将 exceptionCaught 事件继续向后传播。

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

当 netty 内核处理连接的接收,以及数据的读取过程中如果发生异常,会在整个 pipeline 中触发 exceptionCaught 事件的传播。

这里笔者为什么要单独强调在 inbound 事件传播的过程中发生异常,才会回调 exceptionCaught 呢 ?

因为 inbound 事件一般都是由 netty 内核触发传播的,而 outbound 事件一般都是由用户选择触发的,比如用户在处理完业务逻辑触发的 write 事件或者 flush 事件。

而在用户触发 outbound 事件后,一般都会得到一个 ChannelPromise 。用户可以向 ChannelPromise 添加各种 listener 。当 outbound 事件在传播的过程中发生异常时,netty 会通知用户持有的这个 ChannelPromise ,但不会触发 exceptionCaught 的回调

比如我们在《一文搞懂Netty发送数据全流程》一文中介绍到的在 write 事件传播的过程中就不会触发 exceptionCaught 事件回调。只是去通知用户的 ChannelPromise 。

    private void invokeWrite0(Object msg, ChannelPromise promise) {
        try {
            //调用当前ChannelHandler中的write方法
            ((ChannelOutboundHandler) handler()).write(this, msg, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    }

    private static void notifyOutboundHandlerException(Throwable cause, ChannelPromise promise) {
        PromiseNotificationUtil.tryFailure(promise, cause, promise instanceof VoidChannelPromise ? null : logger);
    }

而 outbound 事件中只有 flush 事件的传播是个例外,当 flush 事件在 pipeline 传播的过程中发生异常时,会触发对应异常 ChannelHandler 的 exceptionCaught 事件回调。因为 flush 方法的签名中不会给用户返回 ChannelPromise 。

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

3.1.2 ChannelRegistered 事件

当 main reactor 在启动的时候,NioServerSocketChannel 会被创建并初始化,随后就会向main reactor注册,当注册成功后就会在 NioServerSocketChannel 中的 pipeline 中传播 ChannelRegistered 事件。

当 main reactor 接收客户端发起的连接后,NioSocketChannel 会被创建并初始化,随后会向 sub reactor 注册,当注册成功后会在 NioSocketChannel 中的 pipeline 传播 ChannelRegistered 事件。

传播ChannelRegister事件.png
private void register0(ChannelPromise promise) {

        ................
        //执行真正的注册操作
        doRegister();

        ...........

        //触发channelRegister事件
        pipeline.fireChannelRegistered();

        .......
}

注意:此时对应的 channel 还没有注册 IO 事件到相应的 reactor 中。

3.1.3 ChannelActive 事件

当 NioServerSocketChannel 再向 main reactor 注册成功并触发 ChannelRegistered 事件传播之后,随后就会在 pipeline 中触发 bind 事件,而 bind 事件是一个 outbound 事件,会从 pipeline 中的尾结点 TailContext 一直向前传播最终在 HeadContext 中执行真正的绑定操作。

     @Override
        public void bind(
                ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
            //触发AbstractChannel->bind方法 执行JDK NIO SelectableChannel 执行底层绑定操作
            unsafe.bind(localAddress, promise);
        }
       @Override
        public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
             ..............

            doBind(localAddress);

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

            //绑定成功后 channel激活 触发channelActive事件传播
            if (!wasActive && isActive()) {
                invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        //HeadContext->channelActive回调方法 执行注册OP_ACCEPT事件
                        pipeline.fireChannelActive();
                    }
                });
            }
  
            ...............
        }

当 netty 服务端 NioServerSocketChannel 绑定端口成功之后,才算是真正的 Active ,随后触发 ChannelActive 事件在 pipeline 中的传播。

之前我们也提到过判断 NioServerSocketChannel 是否 Active 的标准就是 : 底层 JDK Nio ServerSocketChannel 是否 open 并且 ServerSocket 是否已经完成绑定。

    @Override
    public boolean isActive() {
        return isOpen() && javaChannel().socket().isBound();
    }

而客户端 NioSocketChannel 中触发 ChannelActive 事件就会比较简单,当 NioSocketChannel 再向 sub reactor 注册成功并触发 ChannelRegistered 之后,紧接着就会触发 ChannelActive 事件在 pipeline 中传播。

传播ChannelActive事件.png
private void register0(ChannelPromise promise) {

        ................
        //执行真正的注册操作
        doRegister();

        ...........

        //触发channelRegister事件
        pipeline.fireChannelRegistered();

        .......

        if (isActive()) {

                    if (firstRegistration) {
                        //触发channelActive事件
                        pipeline.fireChannelActive();
                    } else if (config().isAutoRead()) {
                        beginRead();
                    }
          }
}

而客户端 NioSocketChannel 是否 Active 的标识是:底层 JDK NIO
SocketChannel 是否 open 并且底层 socket 是否连接。毫无疑问,这里的 socket 一定是 connected 。所以直接触发 ChannelActive 事件。

    @Override
    public boolean isActive() {
        SocketChannel ch = javaChannel();
        return ch.isOpen() && ch.isConnected();
    }

注意:此时 channel 才会到相应的 reactor 中去注册感兴趣的 IO 事件。当用户自定义的 ChannelHandler 接收到 ChannelActive 事件时,表明 IO 事件已经注册到 reactor 中了。

3.1.4 ChannelRead 和 ChannelReadComplete 事件

接收客户端连接.png

当客户端有新连接请求的时候,服务端的 NioServerSocketChannel 上的 OP_ACCEPT 事件会活跃,随后 main reactor 会在一个 read loop 中不断的调用 serverSocketChannel.accept() 接收新的连接直到全部接收完毕或者达到 read loop 最大次数 16 次。

在 NioServerSocketChannel 中,每 accept 一个新的连接,就会在 pipeline 中触发 ChannelRead 事件。一个完整的 read loop 结束之后,会触发 ChannelReadComplete 事件。

    private final class NioMessageUnsafe extends AbstractNioUnsafe {

        @Override
        public void read() {
            ......................


                try {
                    do {
                        //底层调用NioServerSocketChannel->doReadMessages 创建客户端SocketChannel
                        int localRead = doReadMessages(readBuf);
                        .................
                    } while (allocHandle.continueReading());

                } catch (Throwable t) {
                    exception = t;
                }

                int size = readBuf.size();
                for (int i = 0; i < size; i ++) {            
                    pipeline.fireChannelRead(readBuf.get(i));
                }

                pipeline.fireChannelReadComplete();

                     .................
        }
    }

当客户端 NioSocketChannel 上有请求数据到来时,NioSocketChannel 上的 OP_READ 事件活跃,随后 sub reactor 也会在一个 read loop 中对 NioSocketChannel 中的请求数据进行读取直到读取完毕或者达到 read loop 的最大次数 16 次。

在 read loop 的读取过程中,每读取一次就会在 pipeline 中触发 ChannelRead 事件。当一个完整的 read loop 结束之后,会在 pipeline 中触发 ChannelReadComplete 事件。

Netty接收网络数据流程.png

这里需要注意的是当 ChannelReadComplete 事件触发时,此时并不代表 NioSocketChannel 中的请求数据已经读取完毕,可能的情况是发送的请求数据太多,在一个 read loop 中读取不完达到了最大限制次数 16 次,还没全部读取完毕就退出了 read loop 。一旦退出 read loop 就会触发 ChannelReadComplete 事件。详细内容可以查看笔者的这篇文章《Netty如何高效接收网络数据》。

3.1.5 ChannelWritabilityChanged 事件

当我们处理完业务逻辑得到业务处理结果后,会调用 ctx.write(msg) 触发 write 事件在 pipeline 中的传播。

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

最终 netty 会将发送数据 msg 写入 NioSocketChannel 中的待发送缓冲队列 ChannelOutboundBuffer 中。并等待用户调用 flush 操作从 ChannelOutboundBuffer 中将待发送数据 msg ,写入到底层 Socket 的发送缓冲区中。

ChannelOutboundBuffer中缓存待发送数据.png

当对端的接收处理速度非常慢或者网络状况极度拥塞时,使得 TCP 滑动窗口不断的缩小,这就导致发送端的发送速度也变得越来越小,而此时用户还在不断的调用 ctx.write(msg) ,这就会导致 ChannelOutboundBuffer 会急剧增大,从而可能导致 OOM 。netty 引入了高低水位线来控制 ChannelOutboundBuffer 的内存占用。

public final class WriteBufferWaterMark {

    private static final int DEFAULT_LOW_WATER_MARK = 32 * 1024;
    private static final int DEFAULT_HIGH_WATER_MARK = 64 * 1024;
}

当 ChanneOutboundBuffer 中的内存占用量超过高水位线时,netty 就会将对应的 channel 置为不可写状态,并在 pipeline 中触发 ChannelWritabilityChanged 事件。

    private void setUnwritable(boolean invokeLater) {
        for (;;) {
            final int oldValue = unwritable;
            final int newValue = oldValue | 1;
            if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
                if (oldValue == 0) {
                    //触发fireChannelWritabilityChanged事件 表示当前channel变为不可写
                    fireChannelWritabilityChanged(invokeLater);
                }
                break;
            }
        }
    }

当 ChannelOutboundBuffer 中的内存占用量低于低水位线时,netty 又会将对应的 NioSocketChannel 设置为可写状态,并再次触发 ChannelWritabilityChanged 事件。

响应channelWritabilityChanged事件.png
    private void setWritable(boolean invokeLater) {
        for (;;) {
            final int oldValue = unwritable;
            final int newValue = oldValue & ~1;
            if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
                if (oldValue != 0 && newValue == 0) {
                    fireChannelWritabilityChanged(invokeLater);
                }
                break;
            }
        }
    }

用户可在自定义 ChannelHandler 中通过 ctx.channel().isWritable() 判断当前 channel 是否可写。

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {

        if (ctx.channel().isWritable()) {
            ...........当前channel可写.........
        } else {
            ...........当前channel不可写.........
        }
    }

3.1.6 UserEventTriggered 事件

netty 提供了一种事件扩展机制可以允许用户自定义异步事件,这样可以使得用户能够灵活的定义各种复杂场景的处理机制。

下面我们来看下如何在 Netty 中自定义异步事件。

  1. 定义异步事件。
public final class OurOwnDefinedEvent {
 
    public static final OurOwnDefinedEvent INSTANCE = new OurOwnDefinedEvent();

    private OurOwnDefinedEvent() { }
}
  1. 触发自定义事件的传播
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
            ......省略.......
            //事件在pipeline中从当前ChannelHandlerContext开始向后传播
            ctx.fireUserEventTriggered(OurOwnDefinedEvent.INSTANCE);
            //事件从pipeline的头结点headContext开始向后传播
            ctx.channel().pipeline().fireUserEventTriggered(OurOwnDefinedEvent.INSTANCE);

    }
}
     
  1. 自定义事件的响应和处理。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        if (OurOwnDefinedEvent.INSTANCE == evt) {
              .....自定义事件处理......
        }
    }

}

后续随着我们源码解读的深入,我们还会看到 Netty 自己本身也定义了许多 UserEvent 事件,我们后面还会在介绍,大家这里只是稍微了解一下相关的用法即可。

3.1.7 ChannelInactive和ChannelUnregistered事件

当 Channel 被关闭之后会在 pipeline 中先触发 ChannelInactive 事件的传播然后在触发 ChannelUnregistered 事件的传播。

我们可以在 Inbound 类型的 ChannelHandler 中响应 ChannelInactive 和 ChannelUnregistered 事件。

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        
        ......响应inActive事件...
        
        //继续向后传播inActive事件
        super.channelInactive(ctx);
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        
          ......响应Unregistered事件...

        //继续向后传播Unregistered事件
        super.channelUnregistered(ctx);
    }

这里和连接建立之后的事件触发顺序正好相反,连接建立之后是先触发 ChannelRegistered 事件然后在触发 ChannelActive 事件。

3.2 Outbound 类事件

final class ChannelHandlerMask {

    // outbound 事件的集合
    static final int MASK_ONLY_OUTBOUND =  MASK_BIND | MASK_CONNECT | MASK_DISCONNECT |
            MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH;

    private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND;
    
    // outbound 事件掩码
    static final int MASK_BIND = 1 << 9;
    static final int MASK_CONNECT = 1 << 10;
    static final int MASK_DISCONNECT = 1 << 11;
    static final int MASK_CLOSE = 1 << 12;
    static final int MASK_DEREGISTER = 1 << 13;
    static final int MASK_READ = 1 << 14;
    static final int MASK_WRITE = 1 << 15;
    static final int MASK_FLUSH = 1 << 16;
}

和 Inbound 类事件一样,Outbound 类事件也有对应的掩码表示。下面我们来看下 Outbound类事件的触发时机:

3.2.1 read 事件

大家这里需要注意区分 read 事件和 ChannelRead 事件的不同

ChannelRead 事件前边我们已经介绍了,当 NioServerSocketChannel 接收到新连接时,会触发 ChannelRead 事件在其 pipeline 上传播。

当 NioSocketChannel 上有请求数据时,在 read loop 中读取请求数据时会触发 ChannelRead 事件在其 pipeline 上传播。

而 read 事件则和 ChannelRead 事件完全不同,read 事件特指使 Channel 具备感知 IO 事件的能力。NioServerSocketChannel 对应的 OP_ACCEPT 事件的感知能力,NioSocketChannel 对应的是 OP_READ 事件的感知能力。

read 事件的触发是在当 channel 需要向其对应的 reactor 注册读类型事件时(比如 OP_ACCEPT 事件 和 OP_READ 事件)才会触发。read 事件的响应就是将 channel 感兴趣的 IO 事件注册到对应的 reactor 上。

比如 NioServerSocketChannel 感兴趣的是 OP_ACCEPT 事件, NioSocketChannel 感兴趣的是 OP_READ 事件。

在前边介绍 ChannelActive 事件时我们提到,当 channel 处于 active 状态后会在 pipeline 中传播 ChannelActive 事件。而在 HeadContext 中的 ChannelActive 事件回调中会触发 Read 事件的传播。

final class HeadContext extends AbstractChannelHandlerContext
            implements ChannelOutboundHandler, ChannelInboundHandler {

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            ctx.fireChannelActive();  
            readIfIsAutoRead();
        }

        private void readIfIsAutoRead() {
            if (channel.config().isAutoRead()) {
                //如果是autoRead 则触发read事件传播
                channel.read();
            }
        }

        @Override
        public void read(ChannelHandlerContext ctx) {
            //触发注册OP_ACCEPT或者OP_READ事件
            unsafe.beginRead();
        }
 }

而在 HeadContext 中的 read 事件回调中会调用 Channel 的底层操作类 unsafe 的 beginRead 方法,在该方法中会向 reactor 注册 channel 感兴趣的 IO 事件。对于 NioServerSocketChannel 来说这里注册的就是 OP_ACCEPT 事件,对于 NioSocketChannel 来说这里注册的则是 OP_READ 事件。

    @Override
    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();

        if ((interestOps & readInterestOp) == 0) {
            //注册监听OP_ACCEPT或者OP_READ事件
            selectionKey.interestOps(interestOps | readInterestOp);
        }
    }

细心的同学可能注意到了 channel 对应的配置类中包含了一个 autoRead 属性,那么这个 autoRead 到底是干什么的呢?

其实这是 netty 为大家提供的一种背压机制,用来防止 OOM ,想象一下当对端发送数据非常多并且发送速度非常快,而服务端处理速度非常慢,一时间消费不过来。而对端又在不停的大量发送数据,服务端的 reactor 线程不得不在 read loop 中不停的读取,并且为读取到的数据分配 ByteBuffer 。而服务端业务线程又处理不过来,这就导致了大量来不及处理的数据占用了大量的内存空间,从而导致 OOM 。

面对这种情况,我们可以通过 channelHandlerContext.channel().config().setAutoRead(false) 将 autoRead 属性设置为 false 。随后 netty 就会将 channel 中感兴趣的读类型事件从 reactor 中注销,从此 reactor 不会再对相应事件进行监听。这样 channel 就不会在读取数据了。

这里 NioServerSocketChannel 对应的是 OP_ACCEPT 事件, NioSocketChannel 对应的是 OP_READ 事件。

        protected final void removeReadOp() {
            SelectionKey key = selectionKey();
            if (!key.isValid()) {
                return;
            }
            int interestOps = key.interestOps();
            if ((interestOps & readInterestOp) != 0) {        
                key.interestOps(interestOps & ~readInterestOp);
            }
        }

而当服务端的处理速度恢复正常,我们又可以通过 channelHandlerContext.channel().config().setAutoRead(true) 将 autoRead 属性设置为 true 。这样 netty 会在 pipeline 中触发 read 事件,最终在 HeadContext 中的 read 事件回调方法中通过调用 unsafe#beginRead 方法将 channel 感兴趣的读类型事件重新注册到对应的 reactor 中。

    @Override
    public ChannelConfig setAutoRead(boolean autoRead) {
        boolean oldAutoRead = AUTOREAD_UPDATER.getAndSet(this, autoRead ? 1 : 0) == 1;
        if (autoRead && !oldAutoRead) {
            //autoRead从false变为true
            channel.read();
        } else if (!autoRead && oldAutoRead) {
            //autoRead从true变为false
            autoReadCleared();
        }
        return this;
    }

read 事件可以理解为使 channel 拥有读的能力,当有了读的能力后, channelRead 就可以读取具体的数据了。

3.2.2 write 和 flush 事件

write 事件和 flush 事件我们在《一文搞懂Netty发送数据全流程》一文中已经非常详尽的介绍过了,这里笔者在带大家简单回顾一下。

write 事件和 flush 事件均由用户在处理完业务请求得到业务结果后在业务线程中主动触发。

用户既可以通过 ChannelHandlerContext 触发也可以通过 Channel 来触发。

不同之处在于如果通过 ChannelHandlerContext 触发,那么 write 事件或者 flush 事件就会在 pipeline 中从当前 ChannelHandler 开始一直向前传播直到 HeadContext 。

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

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

如果通过 Channel 触发,那么 write 事件和 flush 事件就会从 pipeline 的尾部节点 TailContext 开始一直向前传播直到 HeadContext 。

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

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.channel().flush();
    }

当然还有一个 writeAndFlush 方法,也会分为 ChannelHandlerContext 触发和 Channel 的触发。触发 writeAndFlush 后,write 事件首先会在 pipeline 中传播,最后 flush 事件在 pipeline 中传播。

netty 对 write 事件的处理最终会将发送数据写入 Channel 对应的写缓冲队列 ChannelOutboundBuffer 中。此时数据并没有发送出去而是在写缓冲队列中缓存,这也是 netty 实现异步写的核心设计。

最终通过 flush 操作从 Channel 中的写缓冲队列 ChannelOutboundBuffer 中获取到待发送数据,并写入到 Socket 的发送缓冲区中。

3.2.3 close 事件

当用户在 ChannelHandler 中调用如下方法对 Channel 进行关闭时,会触发 Close 事件在 pipeline 中从后向前传播。

//close事件从当前ChannelHandlerContext开始在pipeline中向前传播
ctx.close();
//close事件从pipeline的尾结点tailContext开始向前传播
ctx.channel().close();

我们可以在Outbound类型的ChannelHandler中响应close事件。

public class ExampleChannelHandler extends ChannelOutboundHandlerAdapter {

    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        
        .....客户端channel关闭之前的处理回调.....
        
        //继续向前传播close事件
        super.close(ctx, promise);
    }
}

最终 close 事件会在 pipeline 中一直向前传播直到头结点 HeadConnect 中,并在 HeadContext 中完成连接关闭的操作,当连接完成关闭之后,会在 pipeline中先后触发 ChannelInactive 事件和 ChannelUnregistered 事件。

3.2.4 deRegister 事件

用户可调用如下代码将当前 Channel 从 Reactor 中注销掉。

//deregister事件从当前ChannelHandlerContext开始在pipeline中向前传播
ctx.deregister();
//deregister事件从pipeline的尾结点tailContext开始向前传播
ctx.channel().deregister();

我们可以在 Outbound 类型的 ChannelHandler 中响应 deregister 事件。

public class ExampleChannelHandler extends ChannelOutboundHandlerAdapter {

    @Override
    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {


        .....客户端channel取消注册之前的处理回调.....

        //继续向前传播connect事件
        super.deregister(ctx, promise);
    }
}

最终 deRegister 事件会传播至 pipeline 中的头结点 HeadContext 中,并在 HeadContext 中完成底层 channel 取消注册的操作。当 Channel 从 Reactor 上注销之后,从此 Reactor 将不会在监听 Channel 上的 IO 事件,并触发 ChannelUnregistered 事件在 pipeline 中传播。

3.2.5 connect 事件

在 Netty 的客户端中我们可以利用 NioSocketChannel 的 connect 方法触发 connect 事件在 pipeline 中传播。

//connect事件从当前ChannelHandlerContext开始在pipeline中向前传播
ctx.connect(remoteAddress);
//connect事件从pipeline的尾结点tailContext开始向前传播
ctx.channel().connect(remoteAddress);

我们可以在 Outbound 类型的 ChannelHandler 中响应 connect 事件。

public class ExampleChannelHandler extends ChannelOutboundHandlerAdapter {

    @Override
    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
                        ChannelPromise promise) throws Exception {
                 
        
        .....客户端channel连接成功之前的处理回调.....
        
        //继续向前传播connect事件
        super.connect(ctx, remoteAddress, localAddress, promise);
    }
}

最终 connect 事件会在 pipeline 中的头结点 headContext 中触发底层的连接建立请求。当客户端成功连接到服务端之后,会在客户端 NioSocketChannel 的 pipeline 中传播 channelActive 事件。

3.2.6 disConnect 事件

在 Netty 的客户端中我们也可以调用 NioSocketChannel 的 disconnect 方法在 pipeline 中触发 disconnect 事件,这会导致 NioSocketChannel 的关闭。

//disconnect事件从当前ChannelHandlerContext开始在pipeline中向前传播
ctx.disconnect();
//disconnect事件从pipeline的尾结点tailContext开始向前传播
ctx.channel().disconnect();

我们可以在 Outbound 类型的 ChannelHandler 中响应 disconnect 事件。

public class ExampleChannelHandler extends ChannelOutboundHandlerAdapter {


    @Override
    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        
        .....客户端channel即将关闭前的处理回调.....
        
        //继续向前传播disconnect事件
        super.disconnect(ctx, promise);
    }
}

最终 disconnect 事件会传播到 HeadContext 中,并在 HeadContext 中完成底层的断开连接操作,当客户端断开连接成功关闭之后,会在 pipeline 中先后触发 ChannelInactive 事件和 ChannelUnregistered 事件。


本文是 pipeline 的上半部分内容,主要讲述了 pipeline 的结构以及 各种异步 IO 事件。在下篇文章中,笔者会对 pipeline 的源码实现做进一步的剖析~~

你可能感兴趣的:(一文聊透 Netty IO 事件的编排利器 pipeline | 详解所有 IO 事件的触发时机以及传播路径(上))