文章目录
前言
在分析过前两节(服务端启动、Reactor线程模型)之后,我们再来介绍一下,pipeline的传播机制。
在前两篇的分析中,不断的出现了pipeline,出镜率极高,其作用在Netty也是非常重要的,加上前两篇文章,这三篇文章算得上介绍了Netty中的“三板斧”(三大组件Channel、EventLoop、Pipeline),所以对于理解好Netty,理解这三大组件是必不可少的。
pipeline就像一条工厂流水线(责任链模式),其中流水头尾都有一个固定的Handler处理把关,流水中间部分Handler由用户自定义,想要产品最终变成什么样被生产出去,由用户自由组装Handler处理决定,在可扩展性方面来说是相当灵活的,因为如果要新的什么功能或是新的什么处理,在流水线中新增一个Handler即可,不需要的时候,就不要这个Handler,对于功能的增删是非常便利的。当然,pipeline还自带动态增删Handler的功能,这样一个灵活的设计思想,值得学习。
Pipeline初始化
在第一篇文章服务端启动中,我们可以知道,每创建一个Channel,都会在其内部创建一个pipeline
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
// 在Channel的构造函数中,初始化pipeline
pipeline = newChannelPipeline();
}
以此为入口,来分析一下pipeline的初始化
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}
这个方法,实际上传入了一个Channel实例,表示一个Channel跟一个pipeline实例一一对应。可以知道,这里Pipeline是DefaultChannelPipeline这个实现类,进入该类的构造函数
protected DefaultChannelPipeline(Channel channel) {
// 将channel保存起来,这里可以知道,可以通过pipeline拿到channel
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
// 在头尾创建了两个context守护头尾
tail = new TailContext(this);
head = new HeadContext(this);
// 双向链表的结构
head.next = tail;
tail.prev = head;
}
其实在第一篇文章中,已经介绍了头尾两个Conetxt,也介绍了一个Handler其实是对应一个Context的,可以把Context对象看成是Handler的封装类。可以看到,这里conetxt在pipeline中的数据结构是一个双向链表,由此可以知道,我们可以从head从下访问Handler直到tail,也可以从tail往上访问所有Handler直到head
Pipeline数据结构
这里先来介绍一下pipeline的数据结构,从上面的介绍可以知道,首先在pipeline初始化的时候构造了一个头尾Context的双向链表结构,那么我们再来看看,若添加Handler会发生什么
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
private void addFirst0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext nextCtx = head.next;
newCtx.prev = head;
newCtx.next = nextCtx;
head.next = newCtx;
nextCtx.prev = newCtx;
}
首先addLast顾名思义,其将元素添加到了tail前面,而addFirst将元素添加到了head后面
大致流程如图所示,到这里,读者应该能明白pipeline的数据结构
传播事件
接下来,我们来介绍事件的传播走向。
首先,传播事件有两种方式
-
调用pipeline进行传播
// pipeline.fireChannelRead() public final ChannelPipeline fireChannelRead(Object msg) { AbstractChannelHandlerContext.invokeChannelRead(head, msg); return this; }
-
调用pipeline中的某个Conetxt进行传播
// context.fireChannelRead() public ChannelHandlerContext fireChannelRead(final Object msg) { invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg); return this; }
那么,这两种传播方式又有什么区别呢?
头尾传播
首先介绍一下pipeline的传播方式。
- 读传播(fireRead)
可以看到,在fireChannelRead方法中,调用了AbstractChannelHandlerContext的静态方法invokeChannelRead去传播事件,并且值得一提的是,此时参数传入的是headContext。进入该方法看看
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
// 从context中拿到EventLoop,目的是检测是否是发动机线程在执行
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
// 执行context的invokeChannelRead方法
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
可以看到,pipeline的传播和context的传播区别并不大,pipeline的传播到最后也会调用到context的传播方法,只不过pipeline的读传播事件会从headContext先发起,从上往下传播
- 写传播
当调用pipeline的写方法时,首先会从tail进行传播
public final ChannelFuture write(Object msg) {
return tail.write(msg);
}
其会找到pipeline的OutBound类型的Handler,从下往上传播
那么我们这里进行总结
- 由head开始的往下传播的事件
- fireChannelActive
- fireChannelInactive
- fireExceptionCaught
- fireChannelRead
- fireChannelReadComplete
- …等等
- 由tail开始的往上传播的事件
- bind
- connect
- write
- flush
- …等等
Context传播
上面介绍了从pipeline开始传播的传播方式,可见其只不过是从头尾开始传播,最终还是调用了context进行事件的传播,而直接context传播与pipeline的区别只不过是起始的传播点不同而已。这里开始分析context的传播方法
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead