Fork me on GitHub
余鸢

Netty学习-对ChannelHandler概念的理解与阐述

ChannelHandler和 ChannelPipeline

ChannelPipeline是ChannelHandler链的容器。

在许多方面的ChannelHandler是在你的应用程序的核心,尽管有时它可能并不明显。
ChannelHandler支持广泛的用途,使它难以界定。因此,最好是把它当作一个通用的容器, 处理进来的事件( 包括数据)并且通过ChannelPipeline。

下图展示了ChannelInboundHandler和ChannelOutboundHandler继承自父接口 ChannelHandler。

7

Netty中有两个方向的数据流,下图展示了入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之间有一个明显的区别:若数据是从用户应用程序到远程主机则是“出站(outbound)”,相反若数据时从远程主机到用户应用程序则是“入站(inbound)”。

为了使数据从一端到达另一端,一个或多个ChannelHandler将以某种方式操作数据。这些ChannelHandler会在程序的“引导”阶段被添加ChannelPipeline中,并且被添加的顺序将决定处理数据的顺序。

8

同样展示了进站和出站的处理器都可以被安装在相同的pipeline。如果消息或任何其他入站事件被读到,将从 pipeline头部开始,传递到第一个ChannelInboundHandler。该处理器可能会或可能不会实际修改数据,取决于其特定的功能,在这之后 该数据将被传递到链中的下一个ChannelInboundHandler。最后,将数据 到达pipeline 的尾部,此时所有处理结束。

数据的出站运动( 即,数据被“写入”) 在概念上是相同的。在这种情况下的数据从尾部流过ChannelOutboundHandlers的链,直到它到达头部。超过这点,出站数据将到达的网络传输,在这里显示为一个 socket。通常,这将触发一个写入操作。

更多Inbound、Outbound Handler

在当前的链( chain)中,事件可以通过ChanneHandlerContext传递给下一个handler。Netty为此提供了抽象基类ChannelInboundHandlerAdapter和hannelOutboundHandlerAdapter,用来处理你想要的事件。这些类提供的方法的实现,可以简单地通过调用ChannelHandlerContext上的相应方法将事件传递给下一个handler。在实际应用中,您可以按需覆盖相应的方法即可。

所以,如果出站和入站操作是不同的,当ChannelPipeline中有混合处理器时将发生什么?虽然入站和出站处理器都扩展了ChannelHandler,Netty的ChannelInboundHandler的实现 和ChannelOutboundHandler之间的是有区别的,从而保证数据传递只从一个处理器到下一个处理器保证正确的类型。

当ChannelHandler被添加到的ChannelPipeline 它得到一个ChannelHandlerContext,它代表一个ChannelHandler和ChannelPipeline之间的“绑定”。它通常是安全保存对此对象的引用,除了当协议中的使用的是不面向连接( 例如,UDP)。而该对象可以被用来获得 底层Channel,它主要是用来写出站数据。

还有,实际上,在 Netty 发送消息有两种方式。您可以直接写消息给Channel或写入ChannelHandlerContext对象。主要的区别是,前一种方法会导致消息从ChannelPipeline的尾部开始,而 后者导致消息从ChannelPipeline 下一个处理器开始。

细节化ChannelHandler

正如我们之前所说,有很多不同类型的 ChannelHandler。每个ChannelHandler做什么取决于其超类。Netty提供了一些默认的处理程序实现形式的“adapter( 适配器)”类。这些旨在简化开发处理逻辑。我们已经看到,在 pipeline中每个的 ChannelHandler负责转发事件到链中的下一个处理器。这些适配器类( 及其子类)会自动帮你实现,所以你只需要实现该特定的方法和事件。

为什么用适配器?

有几个适配器类,可以减少编写自定义 ChannelHandlers,因为他们提供对应接口的所有方法的默认实现。( 也有类似的适配器,用于创建编码器和解码器)这些都是创建自定义处理器时,会经常调用的适配器:ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter、ChannelDuplexHandlerAdapter

下面解释下三个 ChannelHandler 的子类型:编码器、解码器以及ChannelInboundHandlerAdapter 的子类SimpleChannelInboundHandler

编码器、解码器

当你发送或接收消息时,Netty 数据转换就发生了。入站消息将从字节转为一个Java对象;也就是说,“解码”。如果该消息是出站相反会发生:“编码”,从一个Java对象转为字节。其原因是简单的:网络数据是一系列字节,因此需要从那类型进行转换。

不同类型的抽象类用于提供编码器和解码器的,这取决于手头的任务。例如,应用程序可能并不需要马上将消息转为字节。相反,该消息将被转换 一些其他格式。一个编码器将仍然可以使用,但它也将衍生自不同的超类,在一般情况下,基类将有一个名字类似 ByteToMessageDecoder 或MessageToByteEncoder。在一种特殊类型的情况下,你可能会发现类似 ProtobufEncoder 和ProtobufDecoder,用于支持谷歌的 protocol buffer。

严格地说,其他处理器可以做编码器和解码器能做的事。但正如适配器类简化创建通道处理器,所有的编码器/解码器适配器类 都实现自 ChannelInboundHandler 或ChannelOutboundHandler。

对于入站数据,channelRead 方法/事件被覆盖。这种方法在每个消息从入站 Channel 读入时调用。该方法将调用特定解码器的“解码”方法,并将解码后的消息转发到管道中下个的ChannelInboundHandler。

出站消息是类似的。编码器将消息转为字节,转发到下个的 ChannelOutboundHandler。

SimpleChannelHandler

也许最常见的处理器是接收到解码后的消息并应用一些业务逻辑到这些数据。要创建这样一个 ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler 其中 T 是想要进行处理的类型。对这样的处理器,你将覆盖基类的一个或多个方法,获得被作为输入参数传递所有方法的 ChannelHandlerContext 的引用。

在这种类型的处理器方法中的最重要是 channelRead0(ChannelHandlerContext,T)。在这个调用中,T 是将要处理的消息。 你怎么做,完全取决于你,但无论如何你不能阻塞 I/O线程,因为这可能是不利于高性能。

阻塞操作

I/O线程一定不能完全阻塞,因此禁止任何直接阻塞操作在你的ChannelHandler,有一种方法来实现这一要求。你可以指定一个EventExecutorGroup当添加ChannelHandler到ChannelPipeline。此EventExecutorGroup将用于获得EventExecutor,将执行所有的ChannelHandler的方法。这EventExecutor将从I/O线程使用不同的线程,从而释放EventLoop。