高并发编程系列之Netty事件传播机制源码分析

1 Pipeline的事件传播机制

上一节中,我们已经知道AbstractChannelHandlerContext中有Inbound和Outbound两个boolean变量,分别用于标识Context所对应的Handler的类型。

(1)Inbound为true时,表示其对应的ChannelHandler是ChannelInboundHandler的子类。

(2)Outbound为true时,表示其对应的ChannelHandler是ChannelOutboundHandler的子类。

大家肯定还有很多疑惑,不知道这两个属性到底有什么作用?这还要从ChannelPipeline的事件传播类型说起。Netty中的传播事件可以分为两种:Inbound事件和Outbound事件。如下是Netty官网针对这两个事件的说明。

由上可以看出,Inbound事件和Outbound事件的流向是不一样的,Inbound事件的流向是从下至上的,而Outbound恰好相反,是从上到下。并且Inbound方法是通过调用相应的ChannelHandlerContext.fireIN_EVT()方法来传递的,而Outbound方法是通过调用ChannelHandlerContext.OUT_EVT()方法来传递的。例如:ChannelHandlerContext的fireChannelRegistered()调用会发送一个ChannelRegistered的Inbound给下一个ChannelHandlerContext,而ChannelHandlerContext的bind()方法调用时会发送一个bind的Outbound事件给下一个ChannelHandlerContext。

Inbound事件传播方法的代码如下。

Outbound事件传播方法的代码如下。

我们发现:Inbound类似于事件回调(响应请求的事件),而Outbound类似于主动触发(发起请求的事件)。注意,如果我们捕获了一个事件,并且想让这个事件继续传递下去,那么需要调用Context对应的传播方法fireXXX()方法。

如上面的代码所示,MyInboundHandler收到了一个channelActive事件,它在处理后,如果希望将事件继续传播下去,那么需要接着调用ctx.fireChannelActive()方法。

接下来我们可以用一个代码案例来了解一下Pipeline的传播机制。我们分别编写InboundHandlerA、InboundHandlerB、InboundHandlerC和OutboundHandlerA、OutboundHandlerB、OutboundHandlerC类。

InboundHandlerA的代码如下。

InboundHandlerB的代码如下。

InboundHandlerC的代码如下:

以上代码中InboundHandlerA、InboundHandlerB、InboundHandlerC都调用了ctx.fireChannelRead()方法向下传播。

OutboundHandlerA的代码如下。

OutboundHandlerB的代码如下。

OutboundHandlerC的代码如下。

以上代码中OutboundHandlerA、OutboundHandlerB、OutboundHandlerC都调用了ctx.write()方法输出。

下面我们编写测试代码,来了解其传播顺序。先是编写服务端代码。

PipelineServer类主要完成Pipeline的注册工作,代码如下。

PipelineClient类,与服务端建立连接并向服务端发送数据,代码如下。

ClientInHandler类,完成向服务端发送数据的动作,代码如下。

接下来,我们运行测试代码,分别启动PipelineServer和PipelineClient,得到的运行结果如下图所示。

我们再尝试调整PipelineServer中Handler的注册顺序,代码如下。

调整代码后,执行结果如下图所示。

从执行结果上看,我们已经知道了Handler的传播顺序:从Inbound开始顺序执行,然后从Outbound逆序执行。

1.1 Outbound事件传播方式

Outbound事件都是请求事件(Request Event),即请求某件事情的发生,然后通过Outbound事件进行通知。

Outbound事件的传播方向是从Tail到customContext再到Head。

下面我们以Connect事件为例,分析一下Outbound事件的传播机制。

首先,当用户调用了Bootstrap的connect()方法时,就会触发一个Connect请求事件,此调用会触发调用链,如下图所示。

继续跟踪,我们就发现AbstractChannel的connect()方法其实又调用了DefaultChannelPipeline的connect()方法,代码如下。

而pipeline.connect()方法的实现代码如下。

可以看到,当Outbound事件(这里是Connect事件)传递到Pipeline后,其实是以Tail为起点开始传播的。

而tail.connect()调用的是AbstractChannelHandlerContext的connect()方法。

顾名思义,findContextOutbound()方法的作用是以当前Context为起点,向Pipeline中Context双向链表的前端寻找第一个Outbound属性为true的Context(即关联ChannelOutboundHandler的Context),然后返回。findContextOutbound()方法的实现代码如下。

当我们找到了一个Outbound的Context后,就调用它的invokeConnect()方法,这个方法会调用Context关联的ChannelHandler的connect()方法,代码如下。

如果用户没有重写ChannelHandler的connect()方法,那么会调用ChannelOutboundHandlerAdapter的connect()方法,代码如下。

我们看到,ChannelOutboundHandlerAdapter的connect()方法仅仅调用了ctx.connect(),而这个调用又回到了Context.connect()方法调用Connect.findContextOutbound()方法,然后调用next.invokeConnect()方法,其次调用handler.connect()方法,最后又调用Context.connect()方法,如此循环下去,直到Connect事件传递到DefaultChannelPipeline的双向链表的头节点,即Head中。为什么会传递到Head中呢?回想一下,Head实现了ChannelOutboundHandler,因此它的Outbound属性是true。

因为Head本身既是一个ChannelHandlerContext,又实现了ChannelOutboundHandler接口,所以当connect()消息传递到Head后,会将消息传递到对应的ChannelHandler中处理,而Head的handler()方法返回的就是Head本身,代码如下。

因此最终Connect事件是在Head中被处理的。Head的Connect事件处理逻辑的代码如下。

到这里,整个Connect请求事件就结束了。下图描述了整个Connect请求事件的处理过程。

我们仅仅以Connect请求事件为例,分析了Outbound事件的传播过程,但是其实所有的Outbound的事件传播都遵循着一样的传播规律,小伙伴们可以试着分析一下其他Outbound事件,体会一下它们的传播过程。

1.2 Inbound事件传播方式

Inbound事件和Outbound事件的处理过程是类似的,只是传播方向不同。

Inbound事件是一个通知事件,即某件事已经发生了,然后通过Inbound事件进行通知。Inbound通常发生在Channel的状态改变或I/O事件就绪时。

Inbound的特点是其传播方向从Head到customContext再到Tail。

上面我们分析了connect()方法其实是一个Outbound事件,那么接着分析Connect()事件后会发生什么Inbound事件,并最终找到Outbound和Inbound事件之间的联系。当Connect()事件传播到Unsafe后,其实是在AbstractNioUnsafe的connect()方法中进行处理的,代码如下。

在AbstractNioUnsafe的connect()方法中,先调用doConnect()方法进行实际的Socket连接,当连接后会调用fulfillConnectPromise()方法,代码如下。

我们看到,在fulfillConnectPromise()方法中,会通过调用pipeline().fireChannelActive()方法将通道激活的消息(即Socket连接成功)发送出去。而这里,当调用pipeline.fireXXX后,就是Inbound事件的起点。因此当调用pipeline().fireChannelActive()方法时,就产生了一个ChannelActive Inbound事件,接下来看一下Inbound事件是怎么传播的,代码如下。

果然,在fireChannelActive()方法中调用了head.invokeChannelActive()方法,因此可以证明Inbound事件在Pipeline中传输的起点是Head。head.invokeChannelActive()方法的代码如下。

上面的代码应该很熟悉了。回想一下在Outbound事件(例如Connect事件)的传输过程中,我们也有类似的如下操作。

(1)首先调用findContextInbound(),从Pipeline的双向链表中找到第一个Inbound属性为true的Context,然后将其返回。

(2)调用Context的invokeChannelActive()方法,invokeChannelActive()方法的代码如下。

这个方法和Outbound的对应方法(如invokeConnect()方法)如出一辙。与Outbound一样,如果用户没有重写channelActive()方法,就会调用ChannelInboundHandlerAdapter的channelActive()方法,代码如下。

同样地,在ChannelInboundHandlerAdapter的channelActive()方法中,仅仅调用了ctx.fireChannelActive()方法,因此就调用Context.fireChannelActive()方法,其次调用Connect.findContextInbound()方法,然后调用nextContext.invokeChannelActive()方法,再然后调用nextHandler.channelActive()方法,最后调用nextContext.fireChannelActive()方法,如此循环。同理,Tail本身既实现了ChannelInboundHandler接口,又实现了ChannelHandlerContext接口,因此当channelActive()消息传递到Tail后,会将消息传递到对应的ChannelHandler中处理,而Tail的handler()方法返回的就是Tail本身,代码如下。

因此ChannelActive Inbound事件最终是在Tail中处理的,我们看一下它的处理方法。

TailContext的channelActive()方法是空的。大家自行查看TailContext的Inbound处理方法时会发现,它们的实现都是空的。可见,如果是Inbound,当用户没有实现自定义的处理器时,那么默认是不处理的。下图描述了Inbound事件的传输过程。

1.3 小结

Outbound事件为传播过程总结如下。

(1)Outbound事件是请求事件(由Connect发起一个请求,并最终由Unsafe处理这个请求)。

(2)Outbound事件的发起者是Channel。

(3)Outbound事件的处理者是Unsafe。

(4)Outbound事件在Pipeline中的传输方向是从Tail到Head。

(5)在ChannelHandler中处理事件时,如果这个Handler不是最后一个Handler,则需要调用ctx的方法(如ctx.connect()方法)将此事件继续传播下去。如果不这样做,那么此事件的传播会提前终止。

(6)Outbound事件的传播方向是,从Context.OUT_EVT()方法到Connect.findContextOutbound()方法,再到nextContext.invokeOUT_EVT()方法,再到nextHandler.OUT_EVT()方法,最后到nextContext.OUT_EVT()方法。

Inbound事件传播过程总结如下。

(1)Inbound事件为通知事件,当某件事情已经就绪后,会通知上层。

(2)Inbound事件的发起者是Unsafe。

(3)Inbound事件的处理者是Channel,如果用户没有实现自定义的处理方法,那么Inbound事件默认的处理者是TailContext,并且其处理方法是空实现。

(4)Inbound事件在Pipeline中的传输方向是从Head到Tail。

(5)在ChannelHandler中处理事件时,如果这个Handler不是最后一个Handler,则需要调用ctx.fireIN_EVT()方法(如ctx.fireChannelActive()方法)将此事件继续传播下去。如果不这样做,那么此事件的传播会提前终止。

(6)Intbound事件的传播方向是,从Context.fireIN_EVT()方法到Connect.findContextInbound()方法,再到nextContext.invokeIN_EVT()方法,再到nextHandler.IN_EVT()方法,最后到nextContext.fireIN_EVT()方法。

由此可知,Outbound事件和Inbound事件在设计上十分相似,并且Context与Handler之间的调用关系也容易混淆,因此我们在阅读这里的代码时,需要特别注意。

展开阅读全文

页面更新:2024-03-26

标签:事件   服务端   源码   顺序   机制   方向   过程   消息   代码   通知   方法   用户

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top