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

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

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 事件。

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

 传播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 中传播。

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

 传播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 事件

一文聊透Netty IO事件的编排利器pipeline |详解所有IO事件(三)-鸿蒙开发者社区 接收客户端连接.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 IO事件的编排利器pipeline |详解所有IO事件(三)-鸿蒙开发者社区

 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 的发送缓冲区中。

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

 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 事件。

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

 响应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() { }
}

2.触发自定义事件的传播

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);

    }
}

  3.自定义事件的响应和处理。

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 事件。

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