首先简单介绍一下这些组件
Channel:netty与系统交互的操作数据的通道,对应Java NIO中的Socket
ChannelHandler:Channel上就绪事件的具体处理器,按照事件源分InboundHandler和OutboundHandler
ChannelPipeline:ChannelHandler链的容器,每个Channel在创建时都会被分配一个ChannelPipeline
ChannelHandlerContext:代表ChannelHandler和ChannelPipeline之间的关联,其结构是一个双向链表(有指向上一个和下一个的ChannelHandlerContext),每当在ChannelPipeline中添加一个ChannelHandler时,就创建一个ChannelHandlerContext。事件在ChannelHandler中传播就是由对应的ChannelHandlerContext完成的。
它们之间的关系大致如下图:
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());
}
大概像下图一样:
开始测试
- 什么都不改,默认的方式执行一次,启动服务端,客户端发送消息“nihao”,控制台输出如下:
客户端也没有接受到任何返回消息:
很明显,没有执行3个ResponseHandler,因为我们根本就没有往Socket写入消息。
- 修改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”,观察输出:
RequestHandler2执行完后,执行了ResponseHandler1,客户端也收到了ResponseHandler1的消息,看来好像write事件只流转到了ResponseHandler1。
- 把刚才修改RequestHandler2的地方改成下面这样,继续观察:
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Back", CharsetUtil.UTF_8));
结果如下:
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。
总结
- ChannelHandlerContext是一个双向链接结构。
- 事件在ChannelHandler中传播就是由对应的ChannelHandlerContext完成的。
- Channel/ChannelPipeline中的方法是将事件传播整个pipeline,inbound是从前到后,而outbound是从后到前。
- ChannelHandlerContext中的方法是从下一个可以处理的handler开始传播事件,inbound是从前到后,而outbound是从后到前。
引出一个问题,为什么要这样设计,这么设计有什么用,这里直接引用Netty作者们的解释来回答:
- 为了减少将事件传经对它不感兴趣的ChannelHandler所带来的开销。
- 为了避免将事件传经那些可能会对它感兴趣的ChannelHandler。(希望不被处理)