处理器 Handler 详解

在前面的章节,解读Java NIO的IO事件类型时讲到,可供选择器监控的通道IO事件类型包括以下4种:

·可读:SelectionKey.OP_READ
·可写:SelectionKey.OP_WRITE
·连接:SelectionKey.OP_CONNECT
·接收:SelectionKey.OP_ACCEPT
在Netty中,EventLoop反应器内部有一个Java NIO选择器成员执行以上事件的查询,然后进行对应的事件分发。事件分发(Dispatch)的目标就是Netty自己的Handler处理器。Netty的Handler处理器分为两大类:第一类是ChannelInboundHandler通道入站处理器;第二类是ChannelOutboundHandler通道出站处理器。二者都继承了ChannelHandler处理器接口。Netty中的Handler处理器的接口与继承关系,如图6-5所示。

一个事件的产生到发现到处理的全过程:

Netty中的入站处理,不仅仅是OP_READ输入事件的处理,还是从通道底层触发,由Netty通过层层传递,调用ChannelInboundHandler通道入站处理器进行的某个处理。
以底层的Java NIO中的OP_READ输入事件为例:
在通道中发生了OP_READ 事件后,会被EventLoop查询到,然后分发给ChannelInboundHandler通道入站处理 器,调用它的入站处理的方法read。在ChannelInboundHandler通道入站处理器内部 的read方法可以从通道中读取数据

这两个业务处理接口都有各自的默认实现:

ChannelInboundHandler的默认实现 为ChannelInboundHandlerAdapter,叫作通道入站处理适配器ChanneOutboundHandler的默认实现为ChanneloutBoundHandlerAdapter,叫作通道出 站处理适配器。这两个默认的通道处理适配器,分别实现了入站操作和出站操作的基本功能。 如果要实现自己的业务处理器,不需要从零开始去实现处理器的接口, 只需要继承通道处理适配器即可。

整个的IO处理操作环节包括:

从通道读数据包、数据包解码、业务处理、目标数据编码、把数据包写到通道,然后由通道发送到对端, 如图6-8所示。 前后两个环节,从通道读数据包和由通道发送到对端,由Netty的底层负责完 成,不需要用户程序负责。用户程序主要在Handler业务处理器中,Handler涉及的环节为:数据包解码、业务处理、目标数据编码、把数据包写到通道中。前面已经介绍过,从应用程序开发人员的角度来看,有入站和出站两种类型操作。
· 入站处理,触发的方向为:自底向上,Netty的内部(如通道)到 ChannelInboundHandler入站处理器
· 出站处理,触发的方向为:自顶向下,从ChannelOutboundHandler出站处理器 到Netty的内部(如通道)。 按照这种方向来分,前面数据包解码、业务处理两个环节——属于入站处理器 的工作;后面目标数据编码、把数据包写到通道中两个环节——属于出站处理器的 工作

【ChannelInboundHandler通道入站处理器】

当数据或者信息入站到Netty通道时,Netty将触发入站处理器ChannelInboundHandler所对应的入站API,进行入站操作处理。
1.channelRegistered
当通道注册完成后,Netty会调用fireChannelRegistered, 触发通道注册事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelRegistered方法,会被调用到。
2.channelActive
当通道激活完成后,Netty会调用fireChannelActive, 触发通道激活事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelActive方法,会被调用到。
3.channelRead
当通道缓冲区可读,Netty会调用fireChannelRead, 触发通道可读事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelRead方法,会被调用到。
4.channelReadComplete
当通道缓冲区读完,Netty会调用fireChannelReadComplete, 触发通道读完事 。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelReadComplete方法,会被调用到。
5.channelInactive
当连接被断开或者不可用,Netty会调用fireChannelInactive, 触发连接不可用事 。通道会启动对应的流水线处理,在通道注册过的入站处理器Handler的channelInactive方法,会被调用到。
6.exceptionCaught
当通道处理过程发生异常时,Netty会调用fireExceptionCaught,触发异常捕获事件。通道会启动异常捕获的流水线处理, 在通道注册过的处理器Handler的 exceptionCaught方法,会被调用到。注意,这个方法是在通道处理器中ChannelHandler定义的方法,入站处理器、出站处理器接口都继承到了该方法。

完整的生命周期:

ChannelHandler中的回调方法的执行顺序为:
handlerAdded()→channelRegistered()→channelActive()→入站方法回调→channelInactive()→channelUnregistered()→handlerRemoved()。
其中,读数据的入站回调为:channelRead()→channelReadComplete();入站方法会多次调用,每一次有ByteBuf数据包入站都会调用到。除了两个入站回调方法外,其余的6个方法都和ChannelHandler的生命周期有关。
(1)handlerAdded():当业务处理器被加入到流水线后,此方法被回调。也就 是在完成ch.pipeline().addLast(handler)语句之后,会回调handlerAdded()。
(2)channelRegistered():当通道成功绑定一个NioEventLoop线程后,会通过 流水线回调所有业务处理器的channelRegistered()方法。
(3)channelActive():当通道激活成功后,会通过流水线回调所有业务处理器 的channelActive()方法。通道激活成功指的是,所有的业务处理器添加、注册的异步 任务完成,并且NioEventLoop线程绑定的异步任务完成。
(4)channelInactive():当通道的底层连接已经不是ESTABLISH状态,或者底 层连接已经关闭时,会首先回调所有业务处理器的channelInactive()方法。
(5)channelUnregistered():通道和NioEventLoop线程解除绑定,移除掉对这条 通道的事件处理之后,回调所有业务处理器的channelUnregistered()方法。
(6)handlerRemoved():最后,Netty会移除掉通道上所有的业务处理器,并且 回调所有的业务处理器的handlerRemoved()方法。 在上面的6个生命周期方法中,前面3个在通道创建的时候被先后回调,后面3 个在通道关闭的时候会先后被回调。 除了生命周期的回调,就是入站和出站处理的回调。对于Inhandler入站处理 器,有两个很重要的回调方法为:
(1)channelRead():有数据包入站,通道可读。流水线会启动入站处理流程, 从前向后,入站处理器的channelRead()方法会被依次回调到。
(2)channelReadComplete():流水线完成入站处理后,会从前向后,依次回调 每个入站处理器的channelReadComplete()方法,表示数据读取完毕。 至此,大家对ChannelInboundHandler的生命周期和入站业务处理,有一个非常 清楚的了解。

【ChannelOutboundHandler通道出站处理器】

当业务处理完成后,需要操作Java NIO底层通道时,通过一系列的ChannelOutboundHandler通道出站处理器,完成Netty通道到底层通道的操作。比方说建立底层连接、断开底层连接、写入底层Java NIO通道等。
1.bind
监听地址(IP+端口)绑定:完成底层Java IO通道的IP地址绑定。如果使用TCP传输协议, 这个方法用于服务器端
2.connect
连接服务端:完成底层Java IO通道的服务器端的连接操作。如果使用TCP传输协议, 这个方法用于客户端
3.write
写数据到底层:完成Netty通道向底层Java IO通道的数据写入操作。 此方法仅仅 是触发一下操作而已,并不是完成实际的数据写入操作
4.flush
腾空缓冲区中的数据,把 这些数据写到对端:将底层缓存区的数据腾空,立即 写出到对端
5.read
从底层读数据:完成Netty通道从Java IO通道的数据读取。
6.disConnect
断开服务器连接:断开底层Java IO通道的服务器端连接。如果使用TCP传输协议,此方法主要用于客户端。
7.close
主动关闭通道:关闭底层的通道,例如服务器端的新连接监听通道。

【ChannelInitializer通道初始化处理器】

通道和Handler业务处理器的关系是:一条Netty的通道拥有一条Handler业务处理器流水线,负责装配自己的Handler业务处理器。装配Handler的工作,发生在通道开始工作之前。现在的问题是:如果向流水线中装配业务处理器呢?这就得借助通道的初始化类——ChannelInitializer。
上面的ChannelInitializer也是通道初始化器,属于入站处理器的类型。在示例代码中,使用了ChannelInitializer的initChannel()方法。它是何方神圣呢?initChannel()方法是ChannelInitializer定义的一个抽象方法,这个抽象方法需要开发人员自己实现。在父通道调用initChannel()方法时,会将新接收的通道作为参数,传递给initChannel()方法。initChannel()方法内部大致的业务代码是:拿到新连接通道作为实际参数,往它的流水线中装配Handler业务处理器。

【ChannelHandlerContext上下文】

处理上面netty自带io事件处理handler之外,我们做业务开发写的handler属于另外一个基类ChannelHandlerContext通道处理器上下文类
在Handler业务处理器被添加到流水线中时,会创建一个通道处理器上下文ChannelHandlerContext,它代表了ChannelHandler通道处理器和ChannelPipeline通道
流水线之间的关联。
ChannelHandlerContext中包含了有许多方法,主要可以分为两类:
第一类是获 取上下文所关联的Netty组件实例,如所关联的通道、所关联的流水线、上下文内部 Handler业务处理器实例等;第二类是入站和出站处理方法
入站传播或不传播:
在Channel、ChannelPipeline、ChannelHandlerContext三个类中,会有同样的出站和入站处理方法,同一个操作出现在不同的类中,功能有何不同呢?
如果通过 Channel或ChannelPipeline的实例来调用这些方法,它们就会在整条流水线中传播。
如果是通过ChannelHandlerContext通道处理器上下文进行调用,就只会从当 前的节点开始执行Handler业务处理器,并传播到同类型处理器的下一站(节点)。
入站手动截断:
(1)不调用supper.channelXxx(ChannelHandlerContext…)
(2)也不调用ctx.fireChannelXxx()[ctx.fireChannelRead(msg)方法是另外一种入站向下传递的方法]
出站手动截断:
如何截断出站处理流程呢?结论是:出站处理流程只要开始执行,就不能被截断。强行截断的话,Netty会抛出异常。如果业务条件不满足,可以不启动出站处理。大家可以运行示例工程中的testPipelineOutBoundCutting测试方法,会看到截断后抛出的异常,这里就不再赘述。
Channel、Handler、ChannelHandlerContext三者的关系为:
Channel通道拥有一条ChannelPipeline通道流水线,每一个流水线节点为一个ChannelHandlerContext通道处理器上下文对象,每一个上下文中包裹了一个ChannelHandler通道处理器。在ChannelHandler通道处理器的入站/出站处理方法中,Netty都会传递一个Context上下文实例作为实际参数。通过Context实例的实参,在业务处理中,可以获取ChannelPipeline通道流水线的实例或者Channel通道的实例。

【handler热插拔】

Netty中的处理器流水线是一个双向链表。在程序执行过程中,可以动态进行业务处理器的热拔插:动态地增加、删除流水线上的业务处理器Handler。主要的Handler热拔插方法声明在ChannelPipeline接口中。

【handler共享】

这里的NettyEchoServerHandler在前面加了一个特殊的Netty注解: @ChannelHandler.Sharable。这个注解的作用是标注一个Handler实例可以被多个通道安全地共享。
什么叫作Handler共享呢?
就是多个通道的流水线可以加入同一个 Handler业务处理器实例[即handler实例所有channel的流水线共享]。而这种操作,Netty默认是不允许的
但是,很多应用场景需要Handler业务处理器实例能共享。例如,一个服务器处理十万以上的通道,如果一个通道都新建很多重复的Handler实例,就需要上十万以上重复的Handler实例,这就会浪费很多宝贵的空间,降低了服务器的性能。 所以, 如果在Handler实例中,没有与特定通道强相关的数据或者状态,建议设计成共享的 模式:在前面加了一个Netty注解:@ChannelHandler.Sharable
还有一个隐藏比较深的重点:
同一个通道上的所有业务处理器,只能被同一个 线程处理。所以,不是@Sharable共享类型的业务处理器,在线程的层面是安全的,不需要进行线程的同步控制。而不同的通道,可能绑定到多个不同的EventLoop反应器线程。 因此,加上了@ChannelHandler.Sharable注解后的共享业务处理器的实例, 可能被多个线程并发执行[比如两个channel同时入站数据且同时流入到某个handler中此时]
这样,就会导致一个结果:@Sharable共享实例不是线程层面安全的。显而易见, @Sharable共享的业务处理器,如果需要操作的数据不仅仅 是局部变量,则需要进行线程的同步控制,以保证操作是线程层面安全的
如何判断一个Handler是否为@Sharable共享呢?ChannelHandlerAdapter提供了实用方法——isSharable()。如果其对应的实现加上了@Sharable注解,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline通道流水线中。NettyEchoServerHandler回显服务器处理器没有保存与任何通道连接相关的数据,也没有内部的其他数据需要保存。 所以,它不光是可以用来共享,而且不需要 做任何的同步控制。在这里,为它加上了@Sharable注解表示可以共享,更进一步, 这里还设计了一个通用的INSTANCE静态实例,所有的通道直接使用这个 INSTANCE实例即可
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

0x13

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值