Netty学习笔记(六)Pipeline的传播机制

前面简单提到了下Pipeline的传播机制,这里再详细分析下
Pipeline的传播机制中有两个非常重要的属性inbound和outbound(AbstractChannelHandlerContext的属性),
inbound为true表示其对应的ChannelHandler实现了ChannelInboundHandler接口
outbound为true表示其对应的ChannelHandler实现了ChannelOutboundHandler接口

Pipeline是一个双向链表,其head实现了ChannelOutboundHandler接口,tail实现了ChannelInboundHandler接口

Netty的传播事件可以分为两种:inbound和outbound事件(我简单理解为输入和输出)

摘自Netty官网
上面是Netty官网关于两个事件的说明: inbound 事件和outbound 事件的流向是不一样的, inbound 事件从socket.read()开始,其流向是自下而上的,而outbound刚好相反,自上而下,以socket.write()为结束。其中inbound 的传递方式是通过调用ChannelHandlerContext.fireIN_EVT()方法,而outbound 的传递方式是通过调用ChannelHandlerContext.OUT_EVT()方法

我们来看下Netty源码里定义的ChannelInboundHandler 和ChannelOutboundHandler
在这里插入图片描述
在这里插入图片描述
从方法命名发现,ChannelInboundHandler定义的都是类似于回调(响应事件通知)的方法,而ChannelOutboundHandler定义的都是操作(主动触发请求)的方法

Outbound事件传播方式

Outbound 事件都是请求事件(request event),即请求某件事情的发生,然后通过 Outbound 事件进行通知。按照官网的说明其传播方法应该是tail–>handler–>head
以Bootstrap的connect事件为例,分析下其传播流程

其调用链如下:
Bootstrap.connect()
–>Bootstrap.doResolveAndConnect()
–>Bootstrap.doResolveAndConnect0()
–>Bootstrap.doConnect()
–>AbstractChannel.connect(remoteAddress, promise)
–>DefaultChannelPipeline.connect(remoteAddress, promise)

 public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }

可以看到,这里的connect事件确实是以tail为起点开始传播的
实际会调用AbstractChannelHandlerContext的如下代码:

public ChannelFuture connect(
            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
        final AbstractChannelHandlerContext next = findContextOutbound();
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeConnect(remoteAddress, localAddress, promise);
        } else {
            safeExecute(executor, new Runnable() {
                @Override
                public void run() {
                    next.invokeConnect(remoteAddress, localAddress, promise);
                }
            }, promise, null);
        }
        return promise;
    }

主要做两件事:
找到下一个outbound的context,然后调用其invokeConnect方法
先来看下findContextOutbound()方法

private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.prev;
        } while (!ctx.outbound);
        return ctx;
    }

逻辑很简单,就是从当前的context节点(这里就是tail节点)开始向前遍历,直到找到Outbound为true的context并返回
找到一个outbound为true的context之后就会调用其invokeConnect方法,然后会获取其关联的ChannelOutboundHandler并调用connect方法

private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        if (invokeHandler()) {
            try {
                ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
            } catch (Throwable t) {
                notifyOutboundHandlerException(t, promise);
            }
        } else {
            connect(remoteAddress, localAddress, promise);
        }
    }

默认调用的是ChannelOutboundHandlerAdapter的connect方法(如果我们重写了该方法就会调用我们自己的实现,此时如果想要让其继续向下传递,需要手动调用ctx.connect()),然后又调用了context的connect方法,即又回到了AbstractChannelHandlerContext的connect()方法,开始向前去找下一个满足outbound为true的context

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

那么这样的循环什么时候会结束呢,从一开始就说明了Pipeline的head节点是HeadContext,并且其满足outbound为true这一条件,所以最后一定会走到HeadContext的handler()方法,然后调用对应的connect。
前面说过HeadContext既是ChannelHandlerContext又是ChannelHandler,所以handler()方法返回的就是HeadContext对象,其connect方法如下:

  @Override
        HeadContext(DefaultChannelPipeline pipeline) {
            super(pipeline, null, HEAD_NAME, false, true);
            unsafe = pipeline.channel().unsafe();
            setAddComplete();
        }
        public void connect(
                ChannelHandlerContext ctx,
                SocketAddress remoteAddress, SocketAddress localAddress,
                ChannelPromise promise) throws Exception {
            unsafe.connect(remoteAddress, localAddress, promise);
        }

最终调用了unsafe的connect方法,之类的unsafe其实是Pipeline里保存的channel里的unsafe,我们在Channel的初始化的时候看到过
继续跟下去发现会调用AbstractNioChannel的内部类AbstractNioUnsafe的如下方法

  @Override
        public final void connect(
                final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
            try {     
                boolean wasActive = isActive();
                if (doConnect(remoteAddress, localAddress)) {
                    fulfillConnectPromise(promise, wasActive);
                } else {
                    connectPromise = promise;
                    requestedRemoteAddress = remoteAddress;
                    ...
                    promise.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                        }
                    });
                }
            } catch (Throwable t) {
                promise.tryFailure(annotateConnectException(t, remoteAddress));
                closeIfClosed();
            }
        }

这里的doConnect()方法主要做了两件事;
1.调用jdk底层的bind方法
2.调用jdk底层的connect方法
然后我们看下这里的fulfillConnectPromise方法

private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {

            boolean active = isActive();    
            boolean promiseSet = promise.trySuccess();
            if (!wasActive && active) {
                pipeline().fireChannelActive();
            }
            if (!promiseSet) {
                close(voidPromise());
            }
        }

如果在调用doConnect方法之前channel不是active激活状态,调用后变为激活状态,那么就会调用pipeline的fireChannelActive方法,将这一事件–激活成功通知下去 (注意下这里的fireXX方法应该是inbound的类型事件)
接下来我们看看这里的inbound事件会怎么传播

Inbound事件传播方式

DefaultChannelPipeline.fireChannelActive–>AbstractChannelHandlerContext.invokeChannelActive

public final ChannelPipeline fireChannelActive() {
        AbstractChannelHandlerContext.invokeChannelActive(head);
        return this;
    }
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
        //最开始传入的next对象是head节点,说明Inbound事件确实是从Head开始传递的
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            //调用HeadContext的invokeChannelActive()方法(实际是执行了AbstractChannelHandlerContext里的方法)
            next.invokeChannelActive();
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelActive();
                }
            });
        }
    }
 private void invokeChannelActive() {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelActive(this);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelActive();
        }
    }

先执行对应的handler的channelActive方法,这里就是HeadContext的channelActive方法,然后调用context的fireChannelActive继续向下传播

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

这里的fireChannelActive做的事情和前面Outbound事件的类似,向后遍历找到满足inbound为true的context,再调用invokeChannelActive(next),又回到了开始,是不是和outbound的传播很类似

不过需要注意的一点,在传播的过程中会调用对应的ChannelInboundHandler的channelActive(this)方法,如果想要让事件继续往下传播,那么在我们对应的channelActive都需要调用ctx.fireChannelActive向下传播(就像HeadContext做的那样);如果我们没有重写channelActive方法,默认会执行ChannelInboundHandlerAdapter的channelActive方法,它会帮我们调用fireChannelActive()

public ChannelHandlerContext fireChannelActive() {
        final AbstractChannelHandlerContext next = findContextInbound();
        invokeChannelActive(next);
        return this;
    }

这样不断传播下去,最后会找到TailContext节点,前面说过tail是Pipeline的尾结点并且其inbound属性为true,那么就会执行TailContext的channelActive方法,如下:

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

这里就是一个空实现,其实TailContext对于ChannelInboundHandler接口的实现大部分都是空方法,除了下面三个函数

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            ReferenceCountUtil.release(evt);
        }
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            onUnhandledInboundException(cause);
        }
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            onUnhandledInboundMessage(msg);
        }

说明对于Inbound事件,如果用户没有添加自定义的处理器,那么默认都是不处理的

注意到这里的HeadContext在执行fireChannelActive()向下传递之外,还执行了一个方法readIfIsAutoRead(),

@Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelActive();
            readIfIsAutoRead();
        }
        
         private void readIfIsAutoRead() {
            //channelconfig的默认isAutoRead是1也就是开启自动读取
            if (channel.config().isAutoRead()) {
                //这里的read()会调用 tail.read();从tail向read开始传递
                channel.read();
            }
        }

我们看到在如下回调方法里也调用了readIfIsAutoRead()

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

这里的channelReadComplete顾名思义就是完成了channel的数据读取的回调(是在NioEventLoop的processSelectedKey(SelectionKey k, AbstractNioChannel ch)里调用了unsafe.read()方法时触发回调的)。
说明在默认情况下,Channel会开启自动读取模式的,只要Channel是active的,读完一波数据之后就继续向selector注册读事件,这样就可以连续不断得读取数据。
关于processSelectedKey方法的调用流程可以参考我的EventLoopGroup的学习笔记
Netty学习笔记(三)EventLoopGroup开篇
Netty学习笔记(四)EventLoopGroup续篇

前面说到TailContext对于ChannelInboundHandler接口的实现大部分都是空方法,我们来看下其中比较重要的两个方法体不为空的实现exceptionCaught(),channelRead()

@Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            onUnhandledInboundMessage(msg);
        }
          /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    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);
        }
    }

如果Inbound事件没有被用户自定义的ContextHandler处理,那么就会一直向下传播,head->tail,最后tail节点会接收到在Pipeline传播过程中没有被处理的消息,tail节点就会给我们发出一个警告,告诉我们,它已经将我们未处理的数据给丢掉了

对于未处理的流转到tail的异常也是这样处理的,这里的注释提示说异常流转到Tail节点是因为Pipeline的最后一个handler没有处理异常。换句话来说就是如果我们想处理异常,就需要在Pipeline的最后一个非tail节点进行处理,即该handler需要加在自定义节点的最末尾
那么这样是如何保证我们的异常最终都会进入到这个handler的呢?后面分析一下

 /**
     * Called once a {@link Throwable} hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelHandler#exceptionCaught(ChannelHandlerContext, Throwable)}.
     */
    protected void onUnhandledInboundException(Throwable cause) {
        try {
            logger.warn(
                    "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
                            "It usually means the last handler in the pipeline did not handle the exception.",
                    cause);
        } finally {
            ReferenceCountUtil.release(cause);
        }
    }
Pipeline中的异常传播和处理

如果要在业务代码中加入异常处理器,统一处理pipeline过程中的所有的异常,那么该异常处理器需要加载自定义节点的最末尾,如下图所示
在这里插入图片描述
分别以Outbound和Inbound事件来看看异常是怎么在最后一个Handler里被捕捉到并处理的

outBound异常的处理

以 ctx.channel().read()为例

  @Override
    public Channel read() {
        pipeline.read();
        return this;
    }
    @Override
    public final ChannelPipeline read() {
        tail.read();
        return this;
    }
     @Override
    public ChannelHandlerContext read() {
        final AbstractChannelHandlerContext next = findContextOutbound();
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeRead();
        } else {
          ...
        }

        return this;
    }

其调用链如下:channel.read()–>pipeline.read()–>tail.read()–>tail.invokeRead()
进入Pipeline之后会从tail传播到head,最后调用HeadContext的read()方法

@Override
        public final void beginRead() {
           
            try {
                doBeginRead();
            } catch (final Exception e) {
                invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.fireExceptionCaught(e);
                    }
                });
            }
        }

可以看到对于捕捉到的异常,最后都会调用fireExceptionCaught进行处理,我们看下它的实现

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

下面的流程就是Pipeline的传播了
调用链如下:
DefaultChannelPipeline.fireExceptionCaught()
–>AbstractChannelHandlerContext.invokeExceptionCaught(head,cause)
–>AbstractChannelHandlerContext.invokeExceptionCaught(cause) (此时节点为HeadContext)
–>HeadContext.exceptionCaught(ctx,cause)
–>AbstractChannelHandlerContext.fireExceptionCaught(cause) (向下传播)
–>AbstractChannelHandlerContext.invokeExceptionCaught(next,cause) (这里的节点就是当前节点的下一个节点)

会一直传递直到tail节点,如果我们在最后一个自定义Handler里处理了异常,那么就不会传播到TailContext,否则TailContext就会执行到如下方法,提示我们该异常未被处理,将被抛弃

 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            onUnhandledInboundException(cause);
        }
inBound异常的处理

以前面提到的DefaultChannelPipeline.fireChannelActive()方法为例
以head为起点,会调用Inbound属性为true节点的invokeChannelActive()方法,在调用context的handler的channelActive方法时会进行 try { … } catch ( Throwable t ) { }

private void invokeChannelActive() {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelActive(this);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelActive();
        }
    }
     private void notifyHandlerException(Throwable cause) {
        if (inExceptionCaught(cause)) {
            ...
            return;
        }
        invokeExceptionCaught(cause);
    }

下面的流程就很简单了,执行InboundContext handler的exceptionCaught()方法,ChannelInboundHandlerAdapter帮我们实现了该接口方法,如果我们没有重写对应的方法,会继续向下传播,这里的invokeExceptionCaught和上面的Outbound中的异常在传播过程中执行到的方法是同一个,最终也会向Tail传播

 public ChannelHandlerContext fireExceptionCaught(final Throwable cause) {
        invokeExceptionCaught(next, cause);
        return this;
    }

所以,我们就可以定义这样一个ExceptionCaughtHandler 来处理Inbound和Outbound的异常

public class ExceptionCaughtHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {       
          //TODO 异常处理
          System.out.println("打印异常通知");
    }
}

我觉得这里ExceptionCaughtHandler可以是InboundHandler或者是OutboundHandler,因为在异常传播的时候并没有像其他Inbound和OutBound事件那样对context的 inbound和outbound属性有限制

总结
Outbound事件
  1. Outbound 事件是请求事件
  2. Outbound 事件的发起者是Channel。
  3. Outbound 事件最终是交给channel的unsafe,调用jdk底层的NIO API
  4. Outbound 事件在Pipeline 中的传输方向是tail -> 自定义handler–>head。
  5. Outbound事件在传播过程中,需要调用 ctx.OUT_EVT() 方法传播,否则传播会停止
Inbound事件
  1. Inbound 事件是通知回调事件,当某个操作完成后,通知上层
  2. Inbound 事件发起者是 unsafe。
  3. Inbound 事件的处理者是 Channel,如果用户没有实现自定义的处理方法,那么 Inbound 事件默认 的处理者是TailContext,并且其处理方法是空实现。
  4. Inbound 事件在Pipeline 中的传输方向是head-> 自定义handler–>tail。
  5. Inbound 事件在传播过程中,需要调用 ctx.fireIN_EVT() 方法传播,否则传播会停止
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值