Fork me on GitHub
余鸢

ChannelHandlerContext接口

ChannelHandlerContext接口

ChannelHandlerContext代表了一个ChannelHandler和一个ChannelPipeline之间的关系,它在ChannelHandler被添加到ChannelPipeline时被创建。ChannelHandlerContext的主要功能是管理它对应的ChannelHandler和属于同一个ChannelPipeline的其他ChannelHandler之间的交互。

ChannelHandlerContext有很多方法,其中一些方法Channel和ChannelPipeline也有,但是有些区别。如果你在Channel或者ChannelPipeline实例上调用这些方法,它们的调用会穿过整个pipeline。而在ChannelHandlerContext上调用的同样的方法,仅仅从当前ChannelHandler开始,走到pipeline中下一个可以处理这个event的ChannelHandler。

在使用ChannelHandlerContext API时,请牢记下面几点:

  • 一个ChannelHandler绑定的ChannelHandlerContext 永远不会改变,所以把它的引用缓存起来是安全的。
  • ChannelHandlerContext的一些方法和其他类(Channel和ChannelPipeline)的方法名字相似,但是ChannelHandlerContext的方法采用了更短的event传递路程。我们应该尽可能利用这一点来实现最好的性能。

使用ChannelHandlerContext

代码1.0 Channel,ChannelPipeline,ChannelHandler和ChannelHandlerContext之间的关系

1
2
3
4
5
6
7
8
9
10
11
@ChannelHandler.Sharable
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
ReferenceCountUtil.release(msg);
promise.setSuccess();
}
}

在下面的这段代码里,你从ChannelHandlerContext中获取了Channel的引用。在Channel上调用write()会让写event穿过整个pipeline。

代码1.1 从ChannelHandlerContext从获取Channel

1
2
3
4
5
6
public static void writeViaChannel(ChannelHandlerContext context) {
ChannelHandlerContext ctx = context;
Channel channel = ctx.channel();
channel.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
}

下面这段代码是一个类似的例子,但是这次却写数据到一个ChannelPipeline。ChannelPipeline的引用是从ChannelHandlerContext中获取的。

代码1.2 从ChannelHandlerContext从获取ChannelPipeline

1
2
3
4
5
6
public static void writeViaChannelPipeline(ChannelHandlerContext context) {
ChannelHandlerContext ctx = context;
ChannelPipeline pipeline = ctx.pipeline();
pipeline.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
}

上述两个代码中的event传递路径是类似的。重点要注意的是,虽然不管是Channel还是ChannelPipeline上调用的write()都是穿过整个pipeline传递event的,但是在ChannelHandler里,从一个handler走到下一个,是通过ChannelHandlerContext的。

那为什么你可能会需要在ChannelPipeline某个特定的位置开始传送一个event呢?

  • 减少因为让event穿过那些对它不感兴趣的ChannelHandler而带来的开销
  • 避免event被那些可能对它感兴趣的handler处理

为了从某个特定的ChannelHandler开始处理,你必须获取前一个ChannelHandler绑定的ChannelHandlerContext的引用。这个ChannelHandlerContext会调用它所绑定的ChannelHandler的下一个handler。

下面的代码说明了这个用法。

代码1.3 调用ChannelHandlerContext上的write()

1
2
ChannelHandlerContext ctx = context;
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

如代码所示,穿过ChannelPipeline的消息从下一个ChannelHandler开始,忽略所有之前的ChannelHandler。

我们刚刚描述的这个用例很常见,当被用来在一个特定的ChannelHandler实例上调用一些操作时,这个做法特别有用。

ChannelHandler和ChannelHandlerContext的高级用法

你可以通过调用ChannelHandlerContext的pipeline方法来获取绑定的ChannelPipeline引用。这实现了对ChannelHandler在运行时的操控,可以实现一些更为复杂的设计。比如,你可以添加一个ChannelHandler到一个pipeline来支持一个动态的协议转换。

其他的高级应用还包括把一个ChannelHandlerContext的引用放入缓存,待后来使用。稍后在使用该引用时,可能不在ChannelHandler方法内,甚至可能在另一个线程里。这段代码说明了如何用这种模式来触发一个event。

代码1.4,缓存一个ChannelHandlerContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WriteHandler extends ChannelHandlerAdapter {
private ChannelHandlerContext ctx;
//保存ChannelHandlerContext的引用待后来使用
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
//用来之前保存的ChannelHandlerContext来发送休息
public void send(String msg) {
ctx.writeAndFlush(msg);
}
}

因为一个ChannelHandler可以属于多个ChannelPipeline,所以它可以绑定多个ChannelHandlerContext实例。想要实现这个用法的ChannelHandler必须加上注解@Sharable;否则,试图将它添加到多个ChannelPipeline时会触发一个异常。显然,想要在多并发channel(也就是连接)中保证线程安全,这样的一个ChannelHandler必须是线程安全的类。

代码1.5 可共享的ChannelHandler

1
2
3
4
5
6
7
8
9
10
@ChannelHandler.Sharable
public class SharableHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("channel read message " + msg);
//打印方法调用,转发消息到下一个ChannelHandlerContext
ctx.fireChannelRead(msg);
}
}

1.5中的ChannelHandler实现满足放入多个pipeline的条件;也就是说,它加上了@Sharable注解,而且没有包含任何状态。相反地,1.6中的代码会带来问题。

代码1.6 @Sharable的无效用法

1
2
3
4
5
6
7
8
9
10
11
@Sharable
public class UnSharableHandler extends ChannelInboundHandlerAdapter {
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
count++;
System.out.println("channelRead(...) called the " + count + " time");
//打印方法调用,转发消息到下一个ChannelHandlerContext
ctx.fireChannelRead(msg);
}
}

这段代码的问题是,它是有状态的,就是用来跟踪方法调用次数的实例变量count。把这个类的一个实例加到ChannelPipeline中,当它被并发的channel获取时,就很有可能出错。(当然,让channelRead()变成同步方法就可以修正这个简单的例子。)

总之,只有在你确信你的ChannelHandler是线程安全的情况下,才使用@Sharable

为什么要共享一个ChannelHandler?

将一个ChannelHandler装入多个ChannelPipeline的一个常见原因,是收集多个Channel的统计数据。