2021SC@SDUSC
1. 核心组件
EventLoop 和 EventLoopGroup(已分析)
ChannelPipeline(已分析)
ChannelHandler
ChannelHandlerContext
2. ChannelHandler责任链模式的过滤链 (本节分析)
3. 通信协议和私有协议栈开发
上一次博客分析完 ChannelPipeline,这个类实现了责任链模式,这次就趁热打铁,分析老师布置的下一个任务:Netty是如何实现责任链模式的过滤链的?
目录
一、责任链模式的过滤链 概念
1. 首先,我们先了解一下,什么是责任链?
我们先来看一个通俗的例子:
一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但是每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了员工请假的难度。因为领导有很多,员工到底找哪位领导他还得自己判断,所以这会显得特别麻烦。
很显然,在该例子中,请假就是一个请求,而且多个对象都可以处理该请求,有部门负责人、副总经理、总经理等,他们都可以进行批假,但是每个对象的处理条件或权限不同,比如部门负责人有可能只能批1~2天的假,一旦超过这一请假天数,员工就得去找部门负责人的顶头上司,也就是副总经理了,要是还超过了副总经理批假的一个范围的话,那么员工就得再去找总经理批假了,这就增加了员工请假的难度。
为了避免请求发送者与多个请求处理者耦合在一起,就出现了责任链模式:
将所有请求的处理者通过前一个对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
好处:
客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
适合的场景:
1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
3、可动态指定一组对象处理请求。
责任链模式主要包含以下角色:
- 抽象处理者角色(Handler):定义一个处理请求的接口或抽象类,包含抽象处理方法和一个后继连接(即记住下一个对象的引用)。
- 具体处理者角色(Concrete Handler):实现抽象处理者的处理方法,判断能否处理本次请求,若可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类角色(Client):创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。也就是说,客户类不需要去找对应的对象进行处理,而只需将处理链创建好即可。就拿上述张三请假的示意图来说,他只需要找他自己的部门负责人即可,至于请假流程要经过哪几步,他并不需要去关注。
2. Netty是如何实现的?
Netty 的 ChannelPipeline 和 ChannelHandler 机制类似于 Servlet 和 Filter 过滤器,这类拦截器实际上是责任链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。
Servlet Filter 能够以声明的方式插入到 HTTP 请求响应的处理过程中,用于拦截请求和响应,以便能够查看、提取或以某种方式操作正在客户端和服务器之间交换的数据。
拦截器封装了业务定制逻辑,能够实现对 Web 应用程序的预处理和事后处理。
过滤器提供了一种面向对象的模块化机制,用来将公共任务封装到可插入的组件中。
这些组件通过 Web 部署配置文件进行声明,可以方便地添加和删除过滤器,无须改动任何应用程序代码或 JSP 页面,由 Servlet 进行动态调用。通过在请求 / 响应链中使用过滤器,可以对应用程序的 Servlet 或 JSP 页面提供的核心处理进行补充,而不破坏 Servlet 或 JSP 页面的功能。
Netty 的 Channel 过滤器实现原理与 Servlet Filter 机制一致,它将 Channel 的数据管道抽象为 ChannelPipeline,消息在 ChannelPipeline 中流动和传递。
ChannelPipeline 持有 I/O 事件拦截器 ChannelHandler 的链表,由 ChannelHandler 对 I/O 事件进行拦截和处理,可以方便地通过新增和删除 ChannelHandler 来实现不同的业务逻辑定制,不需要对已有的 ChannelHandler 进行修改,能够实现对修改封闭和对扩展的支持。
ChannelPipeline 是 ChannelHandler 的容器,它负责 ChannelHandler 的管理和事件拦截与调度。
二、ChannelPipeline的事件传播
2.1 创建Handler链
在理解事件在多个ChannelHandler间的传播顺序前,有两个关键点需要明确:
1. pipeline初始化时,会创建两个哨兵Handler,即HeadContext、TailContext,我们添加的Handler处于这两个哨兵Handler之间,HeadContext可以是入站事件传播的起点,一定是出站事件传播的终点。TailContext可以是出站事件传播的起点。
2. 事件的传播起点、方向、目标:
(1)入站事件传播的起点为当前Handler或者HeadContext,方向为next,也就是往下一个,目标是 InboundHandler。
(2)出站事件传播的起点为当前Handler或者TailContext,方向为prev,也就是往回一个,目标是OutboundHandler。
这里还需要贴出上一篇博客中的一个图片:
服务端的启动入口我们组的一位同学已经分析过,是Bootstrap.connect,connect方法首先会通过反射创建一个Channel,这个过程如下图:
在Channel的构造方法中,会为该Channel初始化一个DefaultChannelPipeline。HeadContext、TailContext,是pipeline中初始的两个Handler,也就是上面说的哨兵Handler,其中TailContext是InboundHandler,HeadContext既是InboundHandler,也是OutboundHandler。
后续我们在调用pipeline.addLast()方法添加的Handler都会处于HeadContext与TailContext之间,比如在我们添加了ByteToMessageDecoder(子类)、MessagetobyteEncoder(子类)、BizHandler三个Handler之后,Handler链就是这样(注意,Handler之间是双向连接的,从左往右是next方向,从右往左是prev方向):
上面讲的这个Handler链的创建流程,我们在上一节的博客中都进行了详细的源码分析。
2.2 ChannelPipeline传播事件
Handler链创建成功之后,现在来分析一下ChannelPipeline是如何传播事件的。
上次博客中分析过,ChannelHandler分为出站和入站,不同的事件会走不同的方向。出站类型的handler不会处理入站数据,反过来也是。如果我们想写一个处理入站数据的handler,只需要继承ChannelInboundHandlerAdapter就可以了,而处理出站数据则继承ChannelOutboundHandlerAdapter就可以了。
2.2.1 入站事件传播
所以我们从这两个类中开始分析,首先分析入站数据在ChannelPipeline中的传播。
inbound事件传播方法有很多:
ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()
我们关注ChannelHandlerContext.fireChannelRead(Object)这个方法。
我们先找到read事件的源头,Netty中,channel的所有IO事件由EventLoop处理,所以我们将视线转移到NioEventLoop中,前面分析过,NioEventLoop的run方法里干了两件事情:一是轮询并处理selector事件,二是处理taskQueue中任务。我们的重点放在一。从run方法开始,直到出现read事件传播,大致流程如下:
最后调用pipeline.fireChannelRead进行read事件传播。
在netty中,事件传播有两类方法
- Pipeline. *
比如fireChannelRead传播读事件、write传播写事件 ( channel.fire* 最终也会走到 pipeline.fire*)
- ChannelHandlerContext. *
比如fireChannelRead传播读事件、write传播写事件
这两类方法的唯一区别就是传播的起点不一样,前者的起点是HeadContext(入站事件起点) 或者TailContext(出站事件起点),后者的起点是当前Handler。
先来看 pipeline.fireChannelRead方法逻辑:
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
这里的head就是HeadContext,pipeline中第一个Handler,通过Pipeline.* 方法传播入站事件时,就以HeadContext为起点,它会继续把读事件往下传播。
再来看ChannelHandlerContext.fireChannelRead方法逻辑:
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
这里invokeChannelRead的第一个参数与在pipeline中调用的不同,使用的是findContextInbound方法的返回值作为参数。返回值是下一个符合要求的ChannelHandler。
以上是介绍两个事件传播方法的不同。下面来看一下事件传播的过程。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
当我们捕获了一个事件,并且想让这个事件继续传递下去,那么需要调用Context相应的传播方法。
我们可以从源码中看到,channelRead方法仅仅是调用了ChannelHandlerContext的fireChannelRead方法。
这个方法在AbstractChannelHandlerContext中被重写,下面是它的实现细节:
public ChannelHandlerContext fireChannelRead(final Object msg) {
//寻找下一个符合要求的ChannelHandler
//调用invokeChannelRead来调用下一个ChannelHandler的invokeChannelRead方法
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
首先来看findContextInbound(MASK_CHANNEL_READ)方法:
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;
}
我们分析过,AbstractChannelHandlerContext对象会有两个标志:inbound和outbound,不同的handler实现会设置相应的标志位。findContextInbound方法的含义就是在ChannelPipeline中寻找下一个属于入站属性的handler。返回值作为参数传入invokeChannelRead方法。
下面进入invokeChannelRead方法查看,返回的handler的用处:
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);//将事件传递给了下一个ChannelHandler
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
我们看到,在这个方法中,调用了next.invokeChannelRead(m),其中next就是我们传入的handler。
我们进入invokeChannelRead方法查看,使用handler 进行的操作:
private void invokeChannelRead(Object msg) {
if (invokeHandler()) {
try {
//调用ChannelInboundHandler的channelRead方法
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
invokeExceptionCaught(t);
}
} else {
fireChannelRead(msg);
}
}
可以发现,在fireChannelRead方法中,调用了findContextInbound方法来寻找下一个符合要求的ChannelHandler,然后调用invokeChannelRead来调用下一个ChannelHandler的invokeChannelRead方法,也就是将事件传递给了下一个ChannelHandler。
所以,到这里我们对读事件的入站传播进行一个小结:
1.NioEventLoop轮询出就绪的read事件后,调用Pipeline.fireChannelRead方法传播事件。
2.Pipeline.fireChannelRead方法会以HeadContext为起点,向next方向找InboundHandler。
3.在解码出Message后,这个Handler会调用ChannelHandlerContext.fireChannelRead方法传播事件。
4.该方法会以当前Handler为起点,向next方向找InboundHandler。
5.直到一个handler不会继续传播读事件,读事件结束。
关于出站事件的传播以及责任链模式的过滤链的其他内容,会在下一篇博客分析。