Netty 的 ChannelHandler 和 ChannelPipeline

本章将介绍:

  • ChannelHandler 和 ChannelPipeline APIs
  • 检测资源泄漏
  • 异常处理

前面的章节你学习了 ByteBuf, Netty’s 的数据容器(data container). 当我们在本章中探索Netty的数据流和处理组件时,我们将以您学到的知识为基础,您将开始看到框架的重要元素汇集在一起。

您已经知道ChannelHandler可以在ChannelPipeline中链接在一起以组织处理逻辑。 我们将研究涉及这些类的各种用例以及一个重要的关系ChannelHandlerContext。了解所有这些组件之间的交互对于使用Netty构建模块化,可重用的实现至关重要。

6.1 The ChannelHandler family

为了准备我们对ChannelHandler的详细研究,我们将花时间研究Netty组件模型这部分的一些基础。

6.1.1 The Channel lifecycle

Interface Channel定义了一个简单但功能强大的状态模型,它与ChannelInboundHandler API密切相关。 表6.1列出了四种信道状态。

Channel lifecycle states
通道的正常生命周期如图6.1所示。 当这些状态发生变化时,会生成相应的事件。 这些转发到ChannelPipeline中的ChannelHandler,然后可以对它们进行操作。

Channel state model

6.1.2 The ChannelHandler lifecycle

由接口ChannelHandler定义的生命周期操作,如表6.2所示,在将ChannelHandler添加到ChannelPipeline或从ChannelPipeline中删除ChannelHandler之后调用。 每个方法都接受ChannelHandlerContext参数。

ChannelHandler lifecycle methods
Netty 定义了 ChannelHandler 类的两个重要的子接口:

  • ChannelInboundHandler —— 处理所有类型的入站数据和状态更改
  • ChannelOutboundHandler —— 处理出站数据并允许拦截所有操作

在后面的章节,我们详细讨论这些接口。

6.1.3 Interface ChannelInboundHandler

表6.3列出了ChannelInboundHandler接口的生命周期方法。 在接收数据或关联通道的状态发生变化时调用它们。 正如我们前面提到的,这些方法与Channel生命周期密切相关。
ChannelInboundHandler methods
当一个 ChannelInboundHandler 实现方法覆盖了 channelRead() 时,它负责显式释放与池化的ByteBuf实例关联的内存.Netty为此提供了一个实用方法ReferenceCountUtil.release(),如下所示。

Releasing message resources

Netty使用WARN -level日志消息记录未发布的资源,从而可以非常简单地在代码中查找有问题的实例。 但以这种方式管理资源可能很麻烦。 更简单的替代方法是使用SimpleChannelInboundHandler。 下一个清单是清单6.1的变体,用于说明这一点。

Using SimpleChannelInboundHandler
由于SimpleChannelInboundHandler会自动释放资源,因此您不应存储对任何消息的引用以供以后使用,因为这些消息将变为无效。
第6.1.6节提供了参考处理的更详细讨论。

6.1.4 Interface ChannelOutboundHandler

ChannelOutboundHandler处理出站操作和数据。 它的方法由Channel,ChannelPipeline和ChannelHandlerContext调用。

ChannelOutboundHandler的强大功能是按需延迟操作或事件,这允许复杂的请求处理方法。 例如,如果暂停写入远程对等体,则可以延迟刷新操作并在以后恢复。

表6.4显示了ChannelOutboundHandler在本地定义的所有方法(省略了从ChannelHandler继承的那些方法)。
ChannelOutboundHandler methods part 1

ChannelOutboundHandler methods

ChannelPromise VS. ChannelFuture ChannelOutboundHandler中的大多数方法都会在操作完成时通知ChannelPromise参数。 ChannelPromise是ChannelFuture的子接口
定义可写方法,例如 setSuccess() 或 setFailure(),从而使ChannelFuture不可变。

接下来,我们将介绍简化编写ChannelHandler任务的类。

6.1.5 ChannelHandler adapters

您可以使用ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter类作为您自己的ChannelHandler的起点。 这些适配器分别提供ChannelInboundHandler和ChannelOutboundHandler的基本实现。 他们通过扩展抽象来获取他们共同的超接口ChannelHandler的方法。

ChannelHandlerAdapter还提供了实用方法isSharable()。 如果实现注释为Sharable,则此方法返回true,表示可以将其添加到多个ChannelPipelines。
ChannelHandlerAdapter class hierarchy

ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter中提供的方法体调用相关ChannelHandlerContext上的等效方法,从而将事件转发到管道中的下一个ChannelHandler。要在您自己的处理程序中使用这些适配器类,只需扩展它们并覆盖您想要自定义的方法。

6.1.6 Resource management

无论何时通过调用 ChannelInboundHandler.channelRead() 或ChannelOutboundHandler.write() 来处理数据,都需要确保没有资源泄漏。 您可能还记得上一章,Netty使用引用计数来处理池化的ByteBuf。 因此,在使用完ByteBuf后调整引用计数非常重要。

为了帮助您诊断潜在问题,Netty提供了类ResourceLeakDetector,它将对应用程序的大约1%的缓冲区分配进行采样,以检查内存泄漏。 涉及的开销非常小。
如果检测到泄漏,将生成类似于以下内容的日志消息:
resource leak

Netty目前定义了四个泄漏检测级别:
Leak-detection levels
通过将以下Java系统属性设置为表中的一个值来定义泄漏检测级别:

java -Dio.netty.leakDetectionLevel=ADVANCED

如果使用JVM选项重新启动应用程序,您将看到应用程序最近访问泄漏缓冲区的位置。 以下是单元测试生成的典型泄漏报告:

ResourceLeakDetector
在实现 ChannelInboundHandler.channelRead() 和 ChannelOutboundHandler.write() 时,如何使用此诊断工具来防止泄漏? 让我们来看看你的channelRead() 操作使用入站消息的情况;也就是说,不通过调用ChannelHandlerContext.fireChannelRead() 将其传递给下一个ChannelInboundHandler。 此列表显示了如何发布消息。

Consuming and releasing an inbound message
Consuming inbound messages the easy way 由于消耗入站数据并释放它是一项常见任务,因此Netty提供了一个名为SimpleChannelInboundHandler的特殊ChannelInboundHandler实现。 一旦消息被channelRead0() 使用,该实现将自动释放消息。

在出站端,如果您处理 write() 操作并丢弃消息,则您负责释放它。 下一个清单显示了丢弃所有书面数据的实现。

Discarding and releasing outbound data

重要的是不仅要释放资源,还要通知ChannelPromise。否则,可能会出现ChannelFutureListener尚未收到有关已处理消息的通知的情况。

总而言之,如果消息被丢弃或丢弃并且未传递给ChannelPipeline中的下一个ChannelOutboundHandler,则用户有责任调用ReferenceCountUtil.release()。 如果消息到达实际传输层,则它将在写入或频道关闭时自动释放。

6.2 Interface ChannelPipeline

如果您将ChannelPipeline视为拦截流经Channel的入站和出站事件的ChannelHandler实例链,则很容易看出这些ChannelHandler的交互如何构成应用程序数据和事件处理逻辑的核心。

为每个创建的新频道分配一个新的ChannelPipeline。 这种关联是永久性的; Channel既不能附加另一个ChannelPipeline,也不能分离当前的ChannelPipeline。 这是Netty组件生命周期中的固定操作,并且不需要开发人员采取任何操作。

根据其来源,事件将由 ChannelInboundHandler 或 ChannelOutboundHandler 处理。 随后,它将通过调用 ChannelHandlerContext 实现转发到同一超类型的下一个处理程序。

ChannelHandlerContext
ChannelHandlerContext使ChannelHandler能够与其ChannelPipeline和其他处理程序进行交互。 处理程序可以通知ChannelPipeline中的下一个ChannelHandler,甚至可以动态修改它所属的ChannelPipeline。
ChannelHandlerContext具有丰富的API,用于处理事件和执行I / O操作。 6.3节将提供有关ChannelHandlerContext的更多信息。

图6.3 显示了一个典型的ChannelPipeline布局,包含入站和出站的ChannelHandler,并说明了我们之前的声明,即ChannelPipeline主要是一系列ChannelHandler。ChannelPipeline还提供了方法通过ChannelPipeline本身传播事件。 如果触发了入站事件,则会从ChannelPipeline的开头传递到结尾。 在图6.3中,出站I / O事件将从ChannelPipeline的右端开始,然后继续向左。

ChannelPipeline and ChannelHandlers

ChannelPipeline relativity
您可以说,从通过ChannelPipeline的事件的角度来看,起始端取决于事件是入站还是出站。 但Netty始终将ChannelPipeline的入站条目(图6.3中的左侧)标识为开头,将出站条目(右侧)标识为结尾。
当您使用ChannelPipeline.add *()方法将入站和出站处理程序的混合添加到ChannelPipeline时,每个ChannelHandler的序数就是我们刚刚定义它们时从开始到结束的位置。 因此,如果您从左到右对图6.3中的处理程序进行编号,则入站事件看到的第一个ChannelHandler将为1; 出站看到的第一个处理程序活动将是5。

当管道传播事件时,它确定管道中下一个ChannelHandler的类型是否与移动方向匹配。 如果没有,ChannelPipeline将跳过该ChannelHandler并继续前进到下一个,直到找到与所需方向匹配的那个。 (当然,处理程序可能同时实现ChannelInboundHandler和ChannelOutboundHandler接口。)

6.2.1 Modifying a ChannelPipeline

ChannelHandler可以通过添加,删除或替换其他ChannelHandler来实时修改ChannelPipeline的布局。 (它也可以从ChannelPipeline中删除它。)这是ChannelHandler最重要的功能之一,所以我们将仔细研究它是如何完成的。 相关方法见表6.6。
ChannelHandler methods for modify a ChannelPipeline
下面的列表显示了如何处理这些方法
Modify the ChannelPipeline

稍后您将看到,轻松重组ChannelHandler的能力有助于实现极其灵活的逻辑。

ChannelHandler execution and blocking
通常,ChannelPipeline中的每个ChannelHandler都会处理由EventLoop(I / O线程)传递给它的事件。 不阻塞此线程至关重要,因为(阻塞)它会对I / O的整体处理产生负面影响。
有时可能需要与使用阻止API的遗留代码进行交互。 对于这种情况,ChannelPipeline 具有接受 EventExecutorGroup 的 add() 方法。 如果事件传递给自定 EventExecutorGroup,它将由此 EventExecutorGroup 中包含的 EventExecutor 之一处理,因此将从Channel本身的EventLoop中删除。 对于此用例,Netty提供了一个名为DefaultEventExecutorGroup的实现。

除了这些操作之外,还有其他操作用于按类型或名称访问ChannelHandler。 这些列于表6.7。

ChannelPipeline operations for accessing ChannelHandlers

6.2.2 Firing events

ChannelPipeline API 公开了用于调用入站和出站操作的其他方法。 表6.8列出了入站操作,该操作通知 ChannelInboundHandler 在 ChannelPipeline 中发生的事件。

ChannelPipeline inbound operations

在出站端,处理事件将导致对底层套接字执行某些操作。 表6.9列出了ChannelPipeline API的出站操作。
ChannelPipeline outbound operations
总的来说:

  • ChannelPipeline保存与Channel关联的ChannelHandler。
  • 可以根据需要添加和删除ChannelHandler来动态修改ChannelPipeline。
  • ChannelPipeline具有丰富的API,用于调用响应入站和出站事件的操作。

6.3 Interface ChannelHandlerContext

ChannelHandlerContext表示ChannelHandler和ChannelPipeline之间的关联,并且只要将ChannelHandler添加到ChannelPipeline,就会创建ChannelHandlerContext。ChannelHandlerContext的主要功能是管理其关联的ChannelHandler与同一ChannelPipeline中的其他人的交互。

ChannelHandlerContext有很多方法,其中一些也存在于Channel和ChannelPipeline本身,但有一个重要的区别。 如果在Channel或ChannelPipeline实例上调用这些方法,它们将在整个管道中传播。 调用ChannelHandlerContext的相同方法将从当前关联的ChannelHandler开始,并仅传播到能够处理事件的管道中的下一个ChannelHandler。

图 6.10 总结了这些 ChannelHandlerContext API.

The ChannelHandlerContext API
The ChannelHandleContext API
当使用 ChannelHandlerContext API 的时候,请记住以下几点:

  • 与ChannelHandler关联的ChannelHandlerContext永远不会更改,因此可以安全地缓存对它的引用。
  • 正如我们在本节开头所解释的那样,ChannelHandlerContext方法涉及比其他类上可用的同名方法更短的事件流。 应尽可能利用这一点来提供最大性能。

6.3.1 Using ChannelHandlerContext

正如我们在本节开头所解释的那样,ChannelHandlerContext方法涉及比其他类上可用的同名方法更短的事件流。 应尽可能利用这一点来提供最大性能。

ChannelHandlerContext created when adding ChannelHandler to ChannelPipeline
在以下列表中,您从ChannelHandlerContext获取对Channel的引用。 在Channel上调用write() 会导致write事件一直流过管道。

Accessing the Channel from a ChannelHandlerContext

下一个清单显示了一个类似的示例,但将此时间写入ChannelPipeline。 同样,从ChannelHandlerContext检索引用.

Accessing the ChannelPipeline from a ChannelhandlerContext

如图6.5所示,清单6.6和6.7中的流程是相同的。 重要的是要注意尽管在Channel或ChannelPipeline 操作上调用的 write() 将事件一直传播到管道,但是在ChannelHandlerContext 上调用 ChannelHandler 级别从一个处理程序到下一个处理程序的移动。

Event passed to first ChannnelHandler in ChannelPipeline

为什么要从ChannelPipeline中的特定点开始传播事件?

  • 减少通过不感兴趣的ChannelHandler传递事件的开销
  • 防止对事件感兴趣的处理程序处理事件

要调用从特定ChannelHandler开始的处理,您必须在此之前引用与ChannelHandler关联的ChannelHandlerContext。 此ChannelHandlerContext将调用与其关联的ChannelHandler之后的ChannelHandler。
以下列表和图6.6说明了这种用法。
Calling ChannelHandlerContext write()

如图6.6所示,消息从下一个ChannelHandler开始流经ChannelPipeline,绕过前面的所有ChannelHipeler。
我们刚才描述的用例是一个常见的用例,它对于调用特定ChannelHandler实例上的操作特别有用。

Event flow for operations triggered via the ChannelHandlerContext

6.3.2 Advanced uses of ChannelHandler and ChannelHandlerContext

正如您在清单6.6中看到的那样,您可以通过调用ChannelHandlerContext的pipeline()方法获取对封闭ChannelPipeline的引用。 这样就可以对管道的ChannelHandler进行运行时操作,可以利用它来实现复杂的设计。 例如,您可以将ChannelHandler添加到管道以支持动态更改协议。

通过缓存对ChannelHandlerContext的引用以供以后使用,可以支持其他高级用法,这可能在任何ChannelHandler方法之外发生,甚至可能来自不同的线程。 此列表显示此模式用于触发事件。
Caching a ChannelHandlerContext

由于ChannelHandler可以属于多个ChannelPipeline,因此可以将其绑定到多个ChannelHandlerContext实例。 用于此用途的ChannelHandler必须使用@Sharable进行注释; 否则,尝试将其添加到多个ChannelPipeline将触发异常。 显然,为了安全使用
有多个并发通道(即连接),这样的ChannelHandler必须是线程安全的。

此列表显示了此模式的正确实现。

A sharable ChannelHandler
前面的ChannelHandler实现满足包含在多个管道中的所有要求; 即,它使用@Sharable进行注释,并且不保留任何状态。 相反,清单6.11中的代码会导致问题。

Invalid usage of @Sharable
这段代码的问题在于它有状态; 即实例变量count,它跟踪方法调用的数量。 将此类的实例添加到ChannelPipeline很可能在并发通道访问时产生错误。 (当然,这个简单的案例可以通过制作 channelRead() 来纠正同步。)
总之,只有在您确定ChannelHandler是线程安全的情况下才使用@Sharable。

Why share a ChannelHander ?
在多个ChannelPipeline中安装单个ChannelHandler的常见原因是跨多个Channel收集统计信息。

以上是对ChannelHandlerContext及其与其他框架组件的关系的讨论。 接下来我们将看看异常处理。

6.4 Exception handling

异常处理是任何实际应用程序的重要组成部分,可以通过各种方式进行处理。 因此,Netty提供了几种处理入站或出站处理期间抛出的异常的选项。 本节将帮助您了解如何设计最适合您需求的方法。

6.4.1 handling inbound exceptions

如果在处理入站事件期间抛出异常,它将从ChannelInboundHandler中触发它的位置开始流经ChannelPipeline。 要处理此类入站异常,您需要在ChannelInboundHandler实现中覆盖以下方法。

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception

下面的清单显示了一个关闭Channel并打印异常堆栈跟踪的简单示例。
Basic inbound exception handling
因为异常将继续在入站方向上流动(就像所有入站事件一样),实现前面逻辑的ChannelInboundHandler通常放在ChannelPipeline的最后。 这可以确保始终处理所有入站异常,无论它们位于ChannelPipeline中的哪个位置。

您应该如何对异常作出反应,这可能与您的应用程序非常相关。 您可能想要关闭频道(和连接),或者您可能尝试恢复。 如果您没有对入站异常实现任何处理(或者不使用异常),Netty将记录未处理异常的事实。

总而言之:

  • ChannelHandler.exceptionCaught() 的默认实现将当前异常转发到管道中的下一个处理程序。
  • 如果异常到达管道(pipeline)的末尾,则将其记录为未处理。
  • 要定义自定义处理,请覆盖exceptionCaught()。 然后,您决定是否将异常传播到该点以外。

6.4.2 Handling outbound exceptions

处理出站(outbound)操作中的正常完成和异常的选项基于以下通知机制:

  • 每一个 outbound opeartion 返回一个 ChannelFuture. 当操作完成时,向ChannelFuture注册的ChannelFutureListeners会收到成功或错误的通知。
  • 几乎 ChannelOutboundHandler 的所有方法都传递了 ChannelPromise 的实例。 作为ChannelFuture 的子类,还可以为 ChannelPromise 分配用于异步通知的侦听器。 但ChannelPromise 也有可写方法,可立即通知:
ChannelPromise setSuccess();
ChannelPromise setFailure(Throwable cause);

添加 ChannelFutureListener 是在 ChannelFuture 实例上调用addListener(ChannelFutureListener) 的问题,有两种方法可以做到这一点。 最常用的一种方法是在出站操作返回的ChannelFuture上调用addListener()(例如write())。

下面的清单使用这种方法添加一个ChannelFutureListener,它将打印堆栈跟踪,然后关闭Channel。

Adding a ChannelFutureListener to a ChannelFuture

第二个选项是将ChannelFutureListener添加到ChannelPromise,ChannelPromise作为ChannelOutboundHandler方法的参数传递。 接下来显示的代码与上一个列表具有相同的效果。

Adding a ChannelFutureListener to a ChannelPromise

ChannelPromise writable methods
通过在ChannelPromise上调用 setSuccess() 和 setFailure(),您可以在ChannelHandler方法返回给调用者时立即获知操作的状态。

为什么选择一种方法而不是另一种?对于异常的详细处理,您可能会发现在调用出站操作时添加ChannelFutureListener更合适,如清单6.13所示。对于处理异常的不太专业的方法,您可能会发现清单6.14中显示的自定义ChannelOutboundHandler实现更简单。

如果您的ChannelOutboundHandler本身抛出异常会发生什么? 在这种情况下,Netty本身将通知已注册相应ChannelPromise的任何听众。

6.5 Summary

本章我们仔细查看了 Netty 的数据处理组建,ChannelHandler. 我们讨论了 ChannelHandlers 是如何链接在一起的,它们以 ChannelInboundHandlers 和 ChannelOutboundHandlers 的形式与 ChannelPipeline 交互。

下一章将重点介绍Netty的编解码器抽象,这使得编写协议编码器和解码器比直接使用底层的ChannelHandler实现更容易。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值