Netty笔记:ChannelHandlerContext与Channel/ChannelPipeline方法的区别

429 阅读4分钟

首先简单介绍一下这些组件

Channel:netty与系统交互的操作数据的通道,对应Java NIO中的Socket
ChannelHandler:Channel上就绪事件的具体处理器,按照事件源分InboundHandler和OutboundHandler
ChannelPipeline:ChannelHandler链的容器,每个Channel在创建时都会被分配一个ChannelPipeline
ChannelHandlerContext:代表ChannelHandler和ChannelPipeline之间的关联,其结构是一个双向链表(有指向上一个和下一个的ChannelHandlerContext),每当在ChannelPipeline中添加一个ChannelHandler时,就创建一个ChannelHandlerContext。事件在ChannelHandler中传播就是由对应的ChannelHandlerContext完成的。

它们之间的关系大致如下图:

image.png

ChannelHandlerContext和Channel/ChannelPipeline有很多相同的方法,但是这些方法有很重要的区别。调用ChannelHandlerContext上的这些方法,事件将从下一个合适的handler传播,而调用Channel/ChannelPipeline上的这些方法,事件则从整个pipeline传播所有可处理的handler。

什么意思呢,通过实际例子来解释。

首先,分别定义3个简单的inboundhandler和3个简单的outboundhandler作为请求和响应,处理逻辑一样,如下:
RequestHandler1

public class RequestHandler1 extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("RequestHandler1");
        super.channelRead(ctx, msg);
    }
}

ResponseHandler1

public class ResponseHandler1 extends ChannelOutboundHandlerAdapter {

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("ResponseHandler1");
        ((ByteBuf) msg).writeCharSequence("+ResponseHandler1 ", CharsetUtil.UTF_8);
        super.write(ctx, msg, promise);
    }
}

然后将这6个handler依次添加到pipeline中:

@Override
protected void initChannel(SocketChannel ch) throws Exception {
    ch.pipeline().addLast(
            new RequestHandler1(),
            new ResponseHandler1(),
            new RequestHandler2(),
            new ResponseHandler2(),
            new RequestHandler3(),
            new ResponseHandler3());
}

大概像下图一样:

image.png

开始测试

  1. 什么都不改,默认的方式执行一次,启动服务端,客户端发送消息“nihao”,控制台输出如下:

image.png

客户端也没有接受到任何返回消息:

image.png

很明显,没有执行3个ResponseHandler,因为我们根本就没有往Socket写入消息。

  1. 修改RequestHandler2的channelRead方法,通过ChannelHandlerContext的方法写入一个消息到通道中,代码如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    System.out.println("RequestHandler2");
    ctx.writeAndFlush(Unpooled.copiedBuffer("Back", CharsetUtil.UTF_8));
    super.channelRead(ctx, msg);
}

启动服务端,客户端发送消息“nihao”,观察输出:

image.png

image.png

RequestHandler2执行完后,执行了ResponseHandler1,客户端也收到了ResponseHandler1的消息,看来好像write事件只流转到了ResponseHandler1。

  1. 把刚才修改RequestHandler2的地方改成下面这样,继续观察:
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Back", CharsetUtil.UTF_8));

结果如下:

image.png

image.png

3个ResponseHandler都执行了,而且顺序是3->2->1(outbound事件),这里back只打印一次是因为在第一个handler处被flush了。

结果分析

在上面第2次测试时,我们是用ctx的writeAndFlush回写消息的,但是好像只有ResponseHandler1接收到了这个消息,通过跟踪writeAndFlush的源码,发现在write的里面有这么一段逻辑:

final AbstractChannelHandlerContext next = findContextOutbound(flush ? (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
...
next.invokeWriteAndFlush(m, promise);

意思就是查找下一个outbound的ctx,然后由这个ctx通过invokeWriteAndFlush将事件传递下去,这也验证了前面介绍ChannelHandlerContext时提到的“事件在ChannelHandler中传播就是由对应的ChannelHandlerContext完成的”。

继续进入到findContextOutbound中,看看是如何查找下一个ctx的:

private AbstractChannelHandlerContext findContextOutbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.prev;
    } while ((ctx.executionMask & mask) == 0);
    return ctx;
}

递归查找ctx.prev,也就是从后往前找,如果是inbound,则就是从前往后找:next。这个prev和next其实也是一个AbstractChannelHandlerContext,实现了ChannelHandlerContext接口,这里可以看出ChannelHandlerContext是一个双向链表结构,prev和next是在初始化channel往pipeline中添加handler时设置的:

private void addLast0(AbstractChannelHandlerContext newCtx) {
    AbstractChannelHandlerContext prev = tail.prev;
    newCtx.prev = prev;
    newCtx.next = tail;
    prev.next = newCtx;
    tail.prev = newCtx;
}

addLast方法是将handler插入到倒数第二个节点上。

测试1和测试3都是调用channel上的方法回写消息的,channel又是调用pipeline的方法,而pipeline是直接调用它的tail的write方法,从尾部节点开始执行,到这里就又回到上面分析的逻辑了。

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

分析到这里就可以解释上面测试的结果了,在RequestHandler2里发起一个outbound事件,就通过RequestHandler2.ctx.prev找到ResponseHandler1,往前传递,所以只执行了ResponseHandler1,如果是在RequestHandler3上调用write则结果就是ResponseHandler2->ResponseHandler1。

总结

  1. ChannelHandlerContext是一个双向链接结构。
  2. 事件在ChannelHandler中传播就是由对应的ChannelHandlerContext完成的。
  3. Channel/ChannelPipeline中的方法是将事件传播整个pipeline,inbound是从前到后,而outbound是从后到前。
  4. ChannelHandlerContext中的方法是从下一个可以处理的handler开始传播事件,inbound是从前到后,而outbound是从后到前。

引出一个问题,为什么要这样设计,这么设计有什么用,这里直接引用Netty作者们的解释来回答:

  1. 为了减少将事件传经对它不感兴趣的ChannelHandler所带来的开销。
  2. 为了避免将事件传经那些可能会对它感兴趣的ChannelHandler。(希望不被处理)