1 概述

ChannelHandlerContext 代表 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline,都会创建 ChannelHandlerContext。

1.1 主要功能

管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。

ChannelHandlerContext一些方法也存在于 Channel 和 ChannelPipeline 本身,若:

  • 调用 Channel 或 ChannelPipeline 的这些方法,它们将沿整个 ChannelPipeline 传播
  • 调用 ChannelHandlerContext 的相同方法,则将从当前所关联的 ChannelHandler 开始,并且只会传播给位于该ChannelPipeline 中的下一个能够处理该事件的 ChannelHandler

1.2 ChannelHandlerContext API

表 6-10 ChannelHandlerContext 的 API:

Netty高手必知必会的ChannelHandlerContext技巧_方法调用

Netty高手必知必会的ChannelHandlerContext技巧_方法调用_02

注意到最后一个 read 方法的注释①:通过配合 ChannelConfig.setAutoRead(boolean autoRead)方法,可以实现反应式系统的特性之一背压(back-pressure)。

FAQ

  • ChannelHandlerContext 和 ChannelHandler 之间的关联(绑定)永远不会改变,所以对它的引用安全
  • 相较其他类的同名方法,ChannelHandler Context的方法将产生更短的事件流,尽可能利用该特性获得最大性能

2 使用 ChannelHandlerContext

2.1 家族关系

图 6-4 展示家族关系:

Netty高手必知必会的ChannelHandlerContext技巧_ide_03

2.2 实例

通过 ChannelHandlerContext 获取到 Channel 的引用。调用Channel#write()将导致写入事件从tail到head地流经 ChannelPipeline:

代码清单 6-6:从 ChannelHandlerContext 访问 Channel
   
 ChannelHandlerContext ctx = ..;
 // 获取到与 ChannelHandlerContext相关联的 Channel 的引用
 Channel channel = ctx.channel();
 // 通过 Channel 写入缓冲区
 channel.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

类似例子,但这次是写入 ChannelPipeline。ChannelPipline引用是通过 ChannelHandlerContext 获取:

代码清单 6-7 通过 ChannelHandlerContext 访问 ChannelPipeline
   
 ChannelHandlerContext ctx = ..;
 // 获取到与 ChannelHandlerContext 相关联的 ChannelPipeline 的引用
 ChannelPipeline pipeline = ctx.pipeline();
 // 通过 ChannelPipeline写入缓冲区
 pipeline.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

如图 6-5所见,代码清单 6-6 和代码清单 6-7 中的事件流一样。

Netty高手必知必会的ChannelHandlerContext技巧_线程安全_04

被调用的 Channel 或 ChannelPipeline 上的write()方法虽然将一直传播事件通过整个 ChannelPipeline,但在 ChannelHandler 级别上,事件从一个 ChannelHandler 到下一个 ChannelHandler 的移动是由 ChannelHandlerContext 上的调用完成的。

2.3 从 ChannelPipeline 中的某特定点开始传播事件

  • 减少将事件传经对它不感兴趣的 ChannelHandler 所带来的开销
  • 避免将事件传经那些可能会对它感兴趣的 ChannelHandler

要想调用从某个特定的 ChannelHandler 开始的处理过程,须获取到在(ChannelPipeline)该 ChannelHandler 之前的 ChannelHandler 所关联的 ChannelHandlerContext。这个 ChannelHandlerContext 将调用和它所关联的 ChannelHandler 之后的ChannelHandler。代码清单 6-8 和图 6-6 说明这种用法。

// 获取到 ChannelHandlerContext 的引用
 ChannelHandlerContext ctx = ..;
 // write()方法将把缓冲区数据发送到下一个 ChannelHandler
 ctx.write(Unpooled.copiedBuffer("JavaEdge", CharsetUtil.UTF_8));

如图 6-6,消息将从下一个 ChannelHandler 开始流经 ChannelPipeline,绕过了所有前面的 ChannelHandler:

Netty高手必知必会的ChannelHandlerContext技巧_方法调用_05

我们刚才所描述的用例是常见的,对于调用特定的 ChannelHandler 实例上的操作尤其有用。

3 ChannelHandler 和 ChannelHandlerContext 高级用法

3.1 获得private的 ChannelPipeline 的引用

代码清单 6-6,可调用 ChannelHandlerContext#pipeline()获得private修饰的 ChannelPipeline 的引用。使得运行时得以操作 ChannelPipeline 的 ChannelHandler,利用这点实现一些复杂设计。如通过将 ChannelHandler 添加到 ChannelPipeline 实现动态的协议切换。

3.2 缓存到 ChannelHandlerContext 的引用

供稍后使用。可能发生在任何的 ChannelHandler 方法之外,甚至来自不同线程。代码清单 6-9 展示了用这种模式来触发事件。

public class WriteHandler extends ChannelHandlerAdapter {
   
    private ChannelHandlerContext ctx;
   
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
       // 存储到 ChannelHandlerContext的引用以供稍后使用
       this.ctx = ctx; 
    }
   
    // 使用之前存储的到 ChannelHandlerContext 的引用来发送消息
    public void send(String msg) { 
       ctx.writeAndFlush(msg);
    } 
 }

3.3 多个 ChannelPipeline 共享同一 ChannelHandler

一个 ChannelHandler 可以从属于多个 ChannelPipeline,所以它也可以绑定到多个 ChannelHandlerContext 实例。这种用法指在多个 ChannelPipeline 共享同一 ChannelHandler,而对应的ChannelHandler 须用@Sharable 注解;否则,试图将它添加到多个 ChannelPipeline 时将触发异常。为安全被用于多个并发的 Channel(即连接),这样的 ChannelHandler 必须是线程安全的。

正例

代码清单 6-10 展示这种模式的一个正确实现:

代码清单 6-10 可共享的 ChannelHandler
   
 // 使用注解@Sharable标注
 @Sharable 
 public class SharableHandler extends ChannelInboundHandlerAdapter {
   
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
      
      System.out.println("Channel read message: " + msg);
      // 记录方法调用,并转发给下一个 ChannelHandler
      ctx.fireChannelRead(msg); 
    } 
 }

前面的 ChannelHandler 实现符合所有的将其加入到多个 ChannelPipeline 的需求,即使用注解@Sharable,且也不持有任何状态。

反例

相反,代码清单 6-11 中的实现有问题。

代码清单 6-11 @Sharable 的错误用法
 
 // 使用注解@Sharable
 @Sharable 
 public class UnsharableHandler extends ChannelInboundHandlerAdapter { 
    private int count;
   
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
      // 将 count 字段的值加 1
      count++;
      // 记录方法调用
      System.out.println("channelRead(...) called the " 
      + count + " time");
      // 并转发给下一个ChannelHandler
      ctx.fireChannelRead(msg);
    } 
 }
拥有状态

主要问题在于,对其所持有的状态的修改并非线程安全,如可通过 AtomicInteger 规避。

即用于跟踪方法调用次数的实例变量count。将这个类的一个实例添加到ChannelPipeline将极有可能在它被多个并发的Channel访问时导致问题。(这简单问题可通过使channelRead()变为同步方法来修正)。总之,只应该在确定了你的 ChannelHandler 是线程安全的时才使用 @Sharable

为何要共享同一个ChannelHandler ?

常见原因:用于收集跨多个 Channel 的统计信息。

4 总结

本文就是 ChannelHandlerContext 和它与其他的框架组件之间的关系的讨论。