
一文聊透Netty IO事件的编排利器pipeline |详解所有IO事件(六)
5. ChanneHandlerContext 的创建
在介绍完 ChannelHandler 向 pipeline 添加的整个逻辑过程后,本小节我们来看下如何为 ChannelHandler 创建对应的 ChannelHandlerContext ,以及 ChannelHandlerContext 中具体包含了哪些上下文信息。
在创建 ChannelHandlerContext 之前,需要做两个重要的前置操作:
•通过 filterName 方法为 ChannelHandlerContext 过滤出在 pipeline 中唯一的名称。
•如果用户为 ChannelHandler 指定了特殊的 EventExecutorGroup ,这里就需要通过 childExecutor 方法从指定的 EventExecutorGroup 中选出一个 EventExecutor 与 ChannelHandler 绑定。
5.1 filterName
如果用户再向 pipeline 添加 ChannelHandler 的时候,为其指定了具体的名称,那么这里需要确保用户指定的名称在 pipeline 中是唯一的。
如果用户没有为 ChannelHandler 指定名称,那么就需要为 ChannelHandler 在 pipeline 中默认生成一个唯一的名称。
pipeline 中使用了一个 FastThreadLocal 类型的 nameCaches 来缓存各种类型 ChannelHandler 的基础名称。后面会根据这个基础名称不断的重试生成一个没有冲突的正式名称。缓存 nameCaches 中的 key 表示特定的 ChannelHandler 类型,value 表示该特定类型的 ChannelHandler 的基础名称 simpleClassName + #0。
自动为 ChannelHandler 生成默认名称的逻辑是:
•首先从缓存中 nameCaches 获取当前添加的 ChannelHandler 的基础名称 simpleClassName + #0。
•如果该基础名称 simpleClassName + #0 在 pipeline 中是唯一的,那么就将基础名称作为 ChannelHandler 的名称。
•如果缓存的基础名称在 pipeline 中不是唯一的,则不断的增加名称后缀 simpleClassName#1 ,simpleClassName#2 ...... simpleClassName#n 直到产生一个没有重复的名称。
虽然用户不大可能将同一类型的 channelHandler 重复添加到 pipeline 中,但是 netty 为了防止这种反复添加同一类型 ChannelHandler 的行为导致的名称冲突,从而利用 nameCaches 来缓存同一类型 ChannelHandler 的基础名称 simpleClassName + #0,然后通过不断的重试递增名称后缀,来生成一个在pipeline中唯一的名称。
5.2 childExecutor
通过前边的介绍我们了解到,当我们向 pipeline 添加 ChannelHandler 的时候,netty 允许我们为 ChannelHandler 指定特定的 executor 去执行 ChannelHandler 中的各种事件回调方法。
通常我们会为 ChannelHandler 指定一个EventExecutorGroup,在创建ChannelHandlerContext 的时候,会通过 childExecutor 方法从 EventExecutorGroup 中选取一个 EventExecutor 来与该 ChannelHandler 绑定。
EventExecutorGroup 是 netty 自定义的一个线程池模型,其中包含多个 EventExecutor ,而 EventExecutor 在 netty 中是一个线程的执行模型。相关的具体实现和用法笔者已经在《Reactor在Netty中的实现(创建篇)》一文中给出了详尽的介绍,忘记的同学可以在回顾下。
在介绍 executor 的绑定逻辑之前,这里笔者需要先为大家介绍一个相关的重要参数:SINGLE_EVENTEXECUTOR_PER_GROUP ,默认为 true 。
我们知道在 netty 中,每一个 channel 都会对应一个独立的 pipeline ,如果我们开启了 SINGLE_EVENTEXECUTOR_PER_GROUP 参数,表示在一个 channel 对应的 pipeline 中,如果我们为多个 ChannelHandler 指定了同一个 EventExecutorGroup ,那么这多个 channelHandler 只能绑定到 EventExecutorGroup 中的同一个 EventExecutor 上。
什么意思呢??比如我们有下面一段初始化pipeline的代码:
eventExecutorGroup 中包含 EventExecutor1,EventExecutor2 , EventExecutor3 三个执行线程。
假设此时第一个连接进来,在创建 channel1 后初始化 pipeline1 的时候,如果在开启 SINGLE_EVENTEXECUTOR_PER_GROUP 参数的情况下,那么在 channel1 对应的 pipeline1 中 channelHandler1,channelHandler2 , channelHandler3 绑定的 EventExecutor 均为 EventExecutorGroup 中的 EventExecutor1 。
第二个连接 channel2 对应的 pipeline2 中 channelHandler1 , channelHandler2 ,channelHandler3 绑定的 EventExecutor 均为 EventExecutorGroup 中的 EventExecutor2 。
第三个连接 channel3 对应的 pipeline3 中 channelHandler1 , channelHandler2 ,channelHandler3 绑定的 EventExecutor 均为 EventExecutorGroup 中的 EventExecutor3 。
以此类推........
SINGLE_EVENTEXECUTOR_PER_GROUP绑定.png
如果在关闭 SINGLE_EVENTEXECUTOR_PER_GROUP 参数的情况下, channel1 对应的 pipeline1 中 channelHandler1 会绑定到 EventExecutorGroup 中的 EventExecutor1 ,channelHandler2 会绑定到 EventExecutor2 ,channelHandler3 会绑定到 EventExecutor3 。
同理其他 channel 对应的 pipeline 中的 channelHandler 绑定逻辑同 channel1 。它们均会绑定到 EventExecutorGroup 中的不同 EventExecutor 中。
SINGLE_EVENTEXECUTOR_PER_GROUP关闭.png
当我们了解了 SINGLE_EVENTEXECUTOR_PER_GROUP 参数的作用之后,再来看下面这段绑定逻辑就很容易理解了。
如果我们并未特殊指定 ChannelHandler 的 executor ,那么默认会是对应 channel 绑定的 reactor 线程负责执行该 ChannelHandler 。
如果我们未开启 SINGLE_EVENTEXECUTOR_PER_GROUP ,netty 就会从我们指定的 EventExecutorGroup 中按照 round-robin 的方式为 ChannelHandler 绑定其中一个 eventExecutor 。
SINGLE_EVENTEXECUTOR_PER_GROUP关闭.png
如果我们开启了 SINGLE_EVENTEXECUTOR_PER_GROUP ,相同的 EventExecutorGroup 在同一个 pipeline 实例中的绑定关系是固定的。在 pipeline 中如果多个 channelHandler 指定了同一个 EventExecutorGroup ,那么这些 channelHandler 的 executor 均会绑定到一个固定的 eventExecutor 上。
SINGLE_EVENTEXECUTOR_PER_GROUP绑定.png
这种固定的绑定关系缓存于每个 pipeline 中的 Map<EventExecutorGroup, EventExecutor> childExecutors 字段中,key 是用户为 channelHandler 指定的 EventExecutorGroup ,value 为该 EventExecutorGroup 在 pipeline 实例中的绑定 eventExecutor 。
接下来就是从 childExecutors 中获取指定 EventExecutorGroup 在该 pipeline 实例中的绑定 eventExecutor,如果绑定关系还未建立,则通过 round-robin 的方式从 EventExecutorGroup 中选取一个 eventExecutor 进行绑定,并在 childExecutor 中缓存绑定关系。
如果绑定关系已经建立,则直接为 ChannelHandler 指定绑定好的 eventExecutor。
5.3 ChanneHandlerContext
在介绍完创建 ChannelHandlerContext 的两个前置操作后,我们回头来看下 ChannelHandlerContext 中包含了哪些具体的上下文信息。
ChannelHandlerContext.png
这里笔者重点介绍 orderd 属性和 executionMask 属性,其他的属性大家很容易理解。
当我们不指定 channelHandler 的 executor 时或者指定的 executor 类型为 OrderedEventExecutor 时,ordered = true。
那么这个 ordered 属性对于 ChannelHandler 响应 pipeline 中的事件有什么影响呢?
我们之前介绍过在 ChannelHandler 响应 pipeline 中的事件之前都会调用 invokeHandler() 方法来判断是否回调 ChannelHandler 的事件回调方法还是跳过。
•当 ordered == false 时,channelHandler 的状态为 ADD_PENDING 的时候,也可以响应 pipeline 中的事件。
•当 ordered == true 时,只有在 channelHandler 的状态为 ADD_COMPLETE 的时候才能响应 pipeline 中的事件
另一个重要的属性 executionMask 保存的是当前 ChannelHandler 的一些执行条件信息掩码,比如:
•当前 ChannelHandler 是什么类型的( ChannelInboundHandler or ChannelOutboundHandler ?)。
•当前 ChannelHandler 对哪些事件感兴趣(覆盖了哪些事件回调方法?)
这里需要一个 FastThreadLocal 类型的 MASKS 字段来缓存 ChannelHandler 对应的执行掩码。因为 ChannelHandler 类一旦被定义出来它的执行掩码就固定了,而 netty 需要接收大量的连接,创建大量的 channel ,并为这些 channel 初始化对应的 pipeline ,需要频繁的记录 channelHandler 的执行掩码到 context 类中,所以这里需要将掩码缓存起来。
计算 ChannelHandler 的执行掩码 mask0 方法虽然比较长,但是逻辑却十分简单。在本文的第三小节《3. pipeline中的事件分类》中,笔者为大家详细介绍了各种事件类型的掩码表示,这里我来看下如何利用这些基本事件掩码来计算出 ChannelHandler 的执行掩码的。
如果 ChannelHandler 是 ChannelInboundHandler 类型的,那么首先会将所有 Inbound 事件掩码设置进执行掩码 mask 中。
最后挨个遍历所有 Inbound 事件,从掩码集合 mask 中排除该 ChannelHandler 不感兴趣的事件。这样一轮下来,就得到了 ChannelHandler 的执行掩码。
从这个过程中我们可以看到,ChannelHandler 的执行掩码包含的是该 ChannelHandler 感兴趣的事件掩码集合。当事件在 pipeline 中传播的时候,在 ChannelHandlerContext 中可以利用这个执行掩码来判断,当前 ChannelHandler 是否符合响应该事件的资格。
同理我们也可以计算出 ChannelOutboundHandler 类型的 ChannelHandler 对应的执行掩码。
那么 netty 框架是如何判断出我们自定义的 ChannelHandler 对哪些事件感兴趣,对哪些事件不感兴趣的呢?
这里我们以 ChannelInboundHandler 类型举例说明,在本文第三小节中,笔者对所有 Inbound 类型的事件作了一个全面的介绍,但是在实际开发中,我们可能并不需要监听所有的 Inbound 事件,可能只是需要监听其中的一到两个事件。
对于我们不感兴趣的事件,我们只需要在其对应的回调方法上标注 @Skip 注解即可,netty 就会认为该 ChannelHandler 对标注 @Skip 注解的事件不感兴趣,当不感兴趣的事件在 pipeline 传播的时候,该 ChannelHandler 就不需要执行响应。
那我们在编写自定义 ChannelHandler 的时候是不是要在 ChannelInboundHandler 或者 ChannelOutboundHandler 接口提供的所有事件回调方法上,对我们不感兴趣的事件繁琐地一一标注 @Skip 注解呢?
其实是不需要的,netty 为我们提供了 ChannelInboundHandlerAdapter 类和 ChannelOutboundHandlerAdapter 类,netty 事先已经在这些 Adapter 类中的事件回调方法上全部标注了 @Skip 注解,我们在自定义实现 ChannelHandler 的时候只需要继承这些 Adapter 类并覆盖我们感兴趣的事件回调方法即可。
