• 处理器 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实例即可
  • 相关阅读:
    springboot整合搭建webservice项目
    MobTech ShareSDK Android端快速集成
    数据处理软件介绍(PIX4D和MAOIHPHO)
    Elasticsearch:如何在不更新证书的情况下为集群之间建立互信
    【Ubuntu系统内核更新与卸载】
    公众号免费注册教程
    使用ThinkMusic网站源码配合cpolar,发布本地音乐网站
    【Linux】Linux权限
    使用pytorch搭建MobileNetV2并基于迁移学习训练
    upload-labs靶场通关指南(第1-3关)
  • 原文地址:https://blog.csdn.net/qq_34448345/article/details/127440225