Netty使用及常用组件(二)

ChannelPipeline和ChannelHandlerContext

ChannelPipeline 接口

当Channel 被创建时,它将会被自动地分配一个新的ChannelPipeline,每个Channel 都有自己的ChannelPipeline。这项关联是永久性的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。

ChannelPipeline 提供了ChannelHandler 链的容器,并定义了用于在该链上传播入站(也就是从网络到业务处理)和 出站(也就是从业务处理到网络),各种事件流的API,我们代码中的ChannelHandler 都是放在ChannelPipeline 中的。

使得事件流经ChannelPipeline 是ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些ChannelHandler 对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler,而且ChannelHandler 对象也完全可以拦截事件不让事件继续传递。它们的执行顺序是由它们被添加的顺序所决定的。

ChannelHandler 的生命周期

在ChannelHandler被添加到ChannelPipeline 中或者被从ChannelPipeline 中移除时会调用下面这些方法。这些方法中的每一个都接受一个ChannelHandlerContext 参数。

handlerAdded 当把ChannelHandler 添加到ChannelPipeline 中时被调用

handlerRemoved 当从ChannelPipeline 中移除ChannelHandler 时被调用

exceptionCaught 当处理过程中在ChannelPipeline 中有错误产生时被调用

ChannelPipeline中的ChannelHandler

入站和出站ChannelHandler 被安装到同一个ChannelPipeline中,ChannelPipeline以双向链表的形式进行维护管理。比如下图,我们在网络上传递的数据,要求加密,但是加密后密文比较大,需要压缩后再传输,而且按照业务要求,需要检查报文中携带的用户信息是否合法,于是我们实现了5个Handler:解压(入)Handler、压缩(出)handler、解密(入) Handler、加密(出) Handler、授权(入) Handler。

如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,但是只被处理入站事件的Handler处理,也就是解压(入)Handler、解密(入) Handler、授权(入) Handler,最终,数据将会到达ChannelPipeline 的尾端,届时,所有处理就都结束了。

数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从链的尾端开始流动,但是只被处理出站事件的Handler处理,也就是加密(出) Handler、压缩(出)handler,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,也就是我们的Socket。

Netty 能区分入站事件的Handler和出站事件的Handler,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递。

所以在我们编写Netty应用程序时要注意,分属出站和入站不同的Handler ,在业务没特殊要求的情况下是无所谓顺序的,正如我们下面的图所示,比如‘压缩(出)handler‘可以放在‘解压(入)handler‘和‘解密(入) Handler‘中间,也可以放在‘解密(入) Handler‘和‘授权(入) Handler‘之间。

而同属一个方向的Handler则是有顺序的,因为上一个Handler处理的结果往往是下一个Handler的要求的输入。比如入站处理,对于收到的数据,只有先解压才能得到密文,才能解密,只有解密后才能拿到明文中的用户信息进行授权检查,所以解压->解密->授权这个三个入站Handler的顺序就不能乱。

ChannelPipeline上的方法

既然ChannelPipeline以双向链表的形式进行维护管理Handler,自然也提供了对应的方法在ChannelPipeline中增加或者删除、替换Handler。

addFirst、addBefore、addAfter、addLast

将一个ChannelHandler 添加到ChannelPipeline 中

remove 将一个ChannelHandler 从ChannelPipeline 中移除

replace 将ChannelPipeline 中的一个ChannelHandler 替换为另一个ChannelHandler

get 通过类型或者名称返回ChannelHandler

context 返回和ChannelHandler 绑定的ChannelHandlerContext

names 返回ChannelPipeline 中所有ChannelHandler 的名称

ChannelPipeline 的API 公开了用于调用入站和出站操作的附加方法。

ChannelHandlerContext

ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联,每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandlerContext,为什么需要这个ChannelHandlerContext ?前面我们已经说过,ChannelPipeline以双向链表的形式进行维护管理Handler,毫无疑问,Handler在放入ChannelPipeline的时候必须要有两个指针pre和next来说明它的前一个元素和后一个元素,但是Handler本身来维护这两个指针合适吗?想想我们在使用JDK的LinkedList的时候,我们放入LinkedList的数据是不会带这两个指针的,LinkedList内部会用类Node对我们的数据进行包装,而类Node则带有两个指针pre和next。

所以,ChannelHandlerContext 的主要作用就和LinkedList内部的类Node类似。

不过ChannelHandlerContext 不仅仅只是个包装类,它还提供了很多的方法,比如让事件从当前ChannelHandler传递给链中的下一个ChannelHandler,还可以被用于获取底层的Channel,还可以用于写出站数据。

 

Channel、ChannelPipeline和ChannelHandlerContext上的事件传播

ChannelHandlerContext 有很多的方法,其中一些方法也存在于Channel 和Channel-Pipeline 本身上,但是有一点重要的不同。如果调用Channel 或者ChannelPipeline 上的这些方法,它们将沿着整个ChannelPipeline 进行传播。而调用位于ChannelHandlerContext上的相同方法,则将从当前所关联的ChannelHandler 开始,并且只会传播给位于该ChannelPipeline 中的下一个(入站下一个,出站上一个)能够处理该事件的ChannelHandler。

 我们用一个实际例子来说明,比如服务器收到对端发过来的报文,解压后需要进行解密,结果解密失败,要给对端一个应答。

如果发现解密失败原因是服务器和对端的加密算法不一致,应答报文只能以明文的压缩格式发送,就可以在解密handler中直接使用ctx.write给对端应答,这样应答报文就只经过压缩Handler就发往了对端;

其他情况下,应答报文要以加密和压缩格式发送,就可以在解密handler中使用channel.write()或者channelpipeline.write()给对端应答,这样应答报文就会流经整个出站处理过程。

 

ChannelHandlerContext 的API

alloc 返回和这个实例相关联的Channel 所配置的ByteBufAllocator

bind 绑定到给定的SocketAddress,并返回ChannelFuture

channel 返回绑定到这个实例的Channel

close 关闭Channel,并返回ChannelFuture

connect 连接给定的SocketAddress,并返回ChannelFuture

deregister 从之前分配的EventExecutor 注销,并返回ChannelFuture

disconnect 从远程节点断开,并返回ChannelFuture

executor 返回调度事件的EventExecutor

fireChannelActive 触发对下一个ChannelInboundHandler 上的channelActive()方法(已连接)的调用

fireChannelInactive 触发对下一个ChannelInboundHandler 上的channelInactive()方法(已关闭)的调用

fireChannelRead 触发对下一个ChannelInboundHandler 上的channelRead()方法(已接收的消息)的调用

fireChannelReadComplete 触发对下一个ChannelInboundHandler 上的channelReadComplete()方法的调用

fireChannelRegistered 触发对下一个ChannelInboundHandler 上的fireChannelRegistered()方法的调用

fireChannelUnregistered 触发对下一个ChannelInboundHandler 上的fireChannelUnregistered()方法的调用

fireChannelWritabilityChanged 触发对下一个ChannelInboundHandler 上的fireChannelWritabilityChanged()方法的调用

fireExceptionCaught 触发对下一个ChannelInboundHandler 上的fireExceptionCaught(Throwable)方法的调用

fireUserEventTriggered 触发对下一个ChannelInboundHandler 上的fireUserEventTriggered(Object evt)方法的调用

handler 返回绑定到这个实例的ChannelHandler

isRemoved 如果所关联的ChannelHandler 已经被从ChannelPipeline中移除则返回true

name 返回这个实例的唯一名称

pipeline 返回这个实例所关联的ChannelPipeline

read 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发一个channelRead事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler 的channelReadComplete(ctx)方法

write 通过这个实例写入消息并经过ChannelPipeline

writeAndFlush 通过这个实例写入并冲刷消息并经过ChannelPipeline

当使用ChannelHandlerContext 的API 的时候,有以下两点:

  • ChannelHandlerContext 和ChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
  • 相对于其他类的同名方法,ChannelHandlerContext的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。

ChannelHandler

ChannelHandler 接口

从应用程序开发人员的角度来看,Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,例如各种编解码,或者处理转换过程中所抛出的异常。

举例来说,ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler 直接冲刷数据然后输出到对端。应用程序的业务逻辑通常实现在一个或者多个ChannelInboundHandler 中。

这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被应用程序的业务逻辑所处理。

Netty 定义了下面两个重要的ChannelHandler 子接口:

ChannelInboundHandler——处理入站数据以及各种状态变化;

ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。

ChannelInboundHandler 接口

下面列出了接口 ChannelInboundHandler 的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel 状态发生改变时被调用。正如我们前面所提到的,这些方法和Channel 的生命周期密切相关。

channelRegistered 当Channel 已经注册到它的EventLoop 并且能够处理I/O 时被调用

channelUnregistered 当Channel 从它的EventLoop 注销并且无法处理任何I/O 时被调用

channelActive 当Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪

channelInactive 当Channel 离开活动状态并且不再连接它的远程节点时被调用

channelReadComplete 当Channel上的一个读操作完成时被调用

channelRead 当从Channel 读取数据时被调用

ChannelWritabilityChanged

当Channel 的可写状态发生改变时被调用。可以通过调用Channel 的isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过Channel.config().setWriteHighWaterMark()和Channel.config().setWriteLowWaterMark()方法来设置

userEventTriggered 当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用。

注意:channelReadComplete和channelRead这两个方法非常让人搞不清两者的区别是什么,我们先放下这个疑问,后面会有解释。

ChannelOutboundHandler 接口

出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、Channel-

Pipeline 以及ChannelHandlerContext 调用。

所有由ChannelOutboundHandler 本身所定义的方法:

bind(ChannelHandlerContext,SocketAddress,ChannelPromise)

当请求将Channel 绑定到本地地址时被调用

connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise)

当请求将Channel 连接到远程节点时被调用

disconnect(ChannelHandlerContext,ChannelPromise)

当请求将Channel 从远程节点断开时被调用

close(ChannelHandlerContext,ChannelPromise) 当请求关闭Channel 时被调用

deregister(ChannelHandlerContext,ChannelPromise)

当请求将Channel 从它的EventLoop 注销时被调用

read(ChannelHandlerContext) 当请求从Channel 读取更多的数据时被调用

flush(ChannelHandlerContext) 当请求通过Channel 将入队数据冲刷到远程节点时被调用

write(ChannelHandlerContext,Object,ChannelPromise) 当请求通过Channel 将数据写到远程节点时被调用

ChannelHandler的适配器

有一些适配器类可以将编写自定义的ChannelHandler 所需要的工作降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter(处理入站) 和ChannelOutboundHandlerAdapter(处理出站)。

我们可以使用ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter类作为自己的ChannelHandler 的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandler 的基本实现。通过扩展抽象类ChannelHandlerAdapter,它们获得了它们共同的超接口ChannelHandler 的方法。

不过ChannelOutboundHandler有个非常让人迷惑的read方法,ChannelOutboundHandler不是处理出站事件的吗?怎么会有read方法呢?其实这个read方法不是表示读数据,而是表示业务发出了读(read)数据的要求,这个要求也会封装为一个事件进行传播,这个事件因为是业务发出到网络的,自然就是个出站事件,而且这个事件触发的就是ChannelOutboundHandler中read方法。

如果我们的Handler既要处理入站又要处理出站怎么办呢?这个时候就可以使用类ChannelDuplexHandler,当然也可以同时实现ChannelOutboundHandler, ChannelInboundHandler这两个接口,自然就要麻烦很多了。

Handler的共享和并发安全性

ChannelHandlerAdapter 还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline。

因为每个socketChannel有自己的pipeline而且每个socketChannel又是和线程绑定的,所以这些Handler的实例之间完全独立的,只要Handler的实例之间不是共享了全局变量,Handler的实例是线程安全的。

但是如果业务需要我们在多个socketChannel之间共享一个Handler的实例怎么办呢?比如统计服务器接受到和发出的业务报文总数,我们就需要用一个Handler的实例来横跨所有的socketChannel来统计所有socketChannel业务报文数。

为了实现这一点,我们可以实现一个MessageCountHandler,并且在MessageCountHandler上使用Netty的@Sharable注解,然后在安装MessageCountHandler实例到pipeline时,共用一个即可。当然,因为MessageCountHandler实例是共享的,所以在实现MessageCountHandler的统计功能时,请务必注意线程安全,我们在具体实现时就使用了Java并发编程里的Atomic类来保证这一点。

资源管理和SimpleChannelInboundHandler

回想一下我们在NIO中是如何接收和发送网络数据的?都是首先创建了一个Buffer,应用程序中的业务部分和Channel之间通过Buffer进行数据的交换:

Netty在处理网络数据时,同样也需要Buffer,在Read网络数据时由Netty创建Buffer,Write网络数据时Buffer往往是由业务方创建的。不管是读和写,Buffer用完后都必须进行释放,否则可能会造成内存泄露。

在Write网络数据时,可以确保数据被写往网络了,Netty会自动进行Buffer的释放,但是如果Write网络数据时,我们有outBoundHandler处理了write()操作并丢弃了数据,没有继续往下写,要由我们负责释放这个Buffer,就必须调用ReferenceCountUtil.release方法,否则就可能会造成内存泄露。

在Read网络数据时,如果我们可以确保每个InboundHandler都把数据往后传递了,也就是调用了相关的fireChannelRead方法,Netty也会帮我们释放,同样的,如果我们有InboundHandler处理了数据,又不继续往后传递,又不调用负责释放的ReferenceCountUtil.release方法,就可能会造成内存泄露。

但是由于消费入站数据是一项常规任务,所以Netty 提供了一个特殊的被

称为SimpleChannelInboundHandler 的ChannelInboundHandler 实现。这个实现会在数据被channelRead0()方法消费之后自动释放数据。

同时系统为我们提供的各种预定义Handler实现,都实现了数据的正确处理,所以我们自行在编写业务Handler时,也需要注意这一点:要么继续传递,要么自行释放。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

30岁老阿姨

支持一下哦!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值