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

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

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

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