netty事件和异常的传播

关于ChannelRead事件的传播

在自定义handler的时候,通常要重写channelRead函数,如果想要将该事件向后传播(注意,传播顺序与handler添加顺序相同),需要调用fireChannelRead函数,ChannelRead事件便在这里中断

通常在重写的channelRead函数里,有两种传播ChannelRead事件的方式

public void channelRead(ChannelHandlerContext ctx, Object msg) {   
 //第一种   
 ctx.fireChannelRead(msg);    
//第二种   
 ctx.channel().pipeline().fireChannelRead(msg);
}

这两种方式的主要区别在于接下来传播的起始位置,非常重要

  • 使用第一种方式,事件会从该节点开始继续向后传播
  • 使用第二种方式,事件会从head节点开始传播

下面分析源码来做说明

第一种传播方式

跟进到AbstractChannelHandlerContext#fireChannelRead方法

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

findContextInbound方法就是在寻找下一个节点,看看这个方法的代码

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

继续看看skipContext方法

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

可以看到,这里判断的关键就在executionMask这个成员变量,而这个成员变量就在AbstractChannelHandlerContext里被赋值

AbstractChannelHandlerContext(DefaultChannelPipeline pipeline,
                              EventExecutor executor,
                              String name, 
                              Class<? extends ChannelHandler> handlerClass) {
    //省略其他代码
    this.executionMask = mask(handlerClass);
}

mask方法最终调用到mask0方法

private static int mask0(Class<? extends ChannelHandler> handlerType) {
    int mask = MASK_EXCEPTION_CAUGHT;
    try {
        if (ChannelInboundHandler.class.isAssignableFrom(handlerType)) {
            mask |= MASK_ALL_INBOUND;
 
            if (isSkippable(handlerType, "channelRegistered", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_REGISTERED;
            }
            if (isSkippable(handlerType, "channelRead", ChannelHandlerContext.class, Object.class)) {
                mask &= ~MASK_CHANNEL_READ;
            }
            //省略部分代码
        }
 
        if (ChannelOutboundHandler.class.isAssignableFrom(handlerType)) {
            mask |= MASK_ALL_OUTBOUND;
 
            if (isSkippable(handlerType, "bind", ChannelHandlerContext.class,
                            SocketAddress.class, ChannelPromise.class)) {
                mask &= ~MASK_BIND;
            }
            //省略部分代码
        }
 
        //省略部分代码
    } catch (Exception e) {
        //省略部分代码
    }
 
    return mask;
}

isSkippable函数只有在找不到函数或者函数被@Skip注解时才返回false

可以看到,实际上executionMask就是用来记录handler的类型信息和方法注解信息

skipContext方法实际上就是在寻找下一个没有用@Skip注解了ChannelRead方法的inbound节点

private static boolean isSkippable(
            final Class<?> handlerType, final String methodName, final Class<?>... paramTypes) throws Exception {
        return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
            @Override
            public Boolean run() throws Exception {
                Method m;
                try {
                    m = handlerType.getMethod(methodName, paramTypes);
                } catch (NoSuchMethodException e) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(
                            "Class {} missing method {}, assume we can not skip execution", handlerType, methodName, e);
                    }
                    return false;
                }
                return m != null && m.isAnnotationPresent(Skip.class);
            }
        });
    }

继续看fireChannelRead函数里调用到的invokeChannelRead函数

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        //不在当前eventloop,放到异步任务队列里
        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) {
            //省略
        }
    } else {
        fireChannelRead(msg);
    }
}

可以看到,这里就是在调用下一个节点的channelRead方法

第二种传播方式

跟进到DefaultChannelPipeline#fireChannelRead方法

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

继续跟进invokeChannelRead方法

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

可以看到,这里就是传入head节点,从head节点开始向后传播channelRead事件

资源释放相关问题

若自定义的handler继承自ChannelInboundHandlerAdapter,并且在ChannelRead函数里没有将事件向后传播,那么需要自行调用函数处理资源释放,如下

ReferenceCountUtil.release(msg);

Write事件传播

write事件的传播顺序与handler的添加顺序相反(即最后添加的outboundHandler最先处理write事件)

类似的,在用户代码里传播write事件也有两种方式

第一种方式,从当前节点,往前寻找outbound,继续传播
ctx.write(msg);

 第二种方式,从tail节点开始,往前寻找outbound传播
ctx.channel().write(msg);
如果没有中断,最终write事件会传播到head节点,然后head节点会调用unsafe的write方法

异常的传播

异常的产生

首先,异常是在ChannelReadChannelRegister等这些函数中抛出的,然后在形如invokeChannelXXX(例如invokeChannelRead)中捕获,例如

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

看看invokeExceptionCaught方法

private void invokeExceptionCaught(final Throwable cause) {
    if (invokeHandler()) {
        try {
            handler().exceptionCaught(this, cause);
        } catch (Throwable error) {
            //省略
        }
    } else {
        fireExceptionCaught(cause);
    }
}

可以看到,这里调用exceptionCaught方法处理异常

传播异常

异常的传播方向与handler的添加方向一致,并且不区分是inboundHandler还是outboundHandler(即异常可以从inboundHandler传播到outboundHandler,反之亦可)

默认情况下,如果不重写exceptionCaught方法,那么会把该异常继续向后传播,最终会传播到tail节点,tail节点会打印一条日志表明该异常未被处理

如果重写了exceptionCaught方法,并且想将该异常继续向后传播,那么需要调用fireExceptionCaught方法

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
    throws Exception {
    //其他处理代码
    ctx.fireExceptionCaught(cause);
}

关联:Netty4中Handler的执行顺序以及ctx.close() 与 ctx.channel().close()的区别

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容