Netty框架入门篇 -- 核心组件再认识
前言
在第一篇文章《Netty框架入门篇 - 初识Netty及第一款Netty应用程序》详细介绍了Netty的一些基础知识,并粗略的分析了Netty的一些核心组件,为了后面更好地和进一步的了解Netty,本文将详细介绍Netty的核心组件以及它们是如何通过协作来支撑整个Netty体系结构的
Channel、EventLoop和ChannelFuture
Channel、EventLoop和ChannelFuture这些类,可以被认为是Netty网络抽象的代表:
- Channel等价于网络编程中的Socket,用于网络数据传输;
- EventLoop指事件循环,用于处理连接中的事件,控制流、多线程处理、并发
- ChannelFuture指异步通知,可以看作是将来要执行的操作的结果的占位符
Channel、EventLoop、Thread以及EventLoopGroup之间的关系图(摘自《Netty In Action》):
- 一个EventLoopGroup包含一个或者多个EventLoop;
- 一个EventLoop在它的生命周期内只和与一个Thread绑定;
- 所有由EventLoop处理的I/O 事件都将在它专有的Thread上被处理;
- 一个Channel在它的生命周期内只能注册于一个EventLoop;
- 一个EventLoop可能会被分配至一个或多个Channel。
Channel
基本的I/O操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语,在传统的网络编程中,其基本的构造是Socket,而Socket类对开发者来说并不友好,使用起来相对复杂。相对的Netty的Channel接口所提供的API,大大地降低了直接使用Socket类的复杂性。并且Netty的Channel相对于原生NIO的Channel具有如下优势:
- 在Channel接口层,采用Facade模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供;
- Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用;
- 具体实现采用聚合而非包含的方式,将相关的功能类聚合在Channel中,有Channel统一负责和调度,功能实现更加灵活。
此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根,主要包括:
- NioSocketChannel
- NioDatagramChannel
- NioSctpChannel
- EmbeddedChannel
Channel的生命周期状态
状态 | 描述 |
---|---|
ChannelUnregistered | Channel已经被创建,但还未注册到EventLoop |
ChannelRegistered | Channel已经被注册到EventLoop |
ChannelActive | Channel 处于活动状态(已经连接到它的远程节点),可以接收和发送数据 |
ChannelInactive | Channel没有连接到远程节点 |
Channel在其生命周期过程中,当状态发生改变时,都会生成相应的事件,而这些事件都会被传递给指定的ChannelHandler来进行处理
EventLoop
EventLoop可以称之为事件循环,它定义了Netty的核心抽象,用于处理网络连接的生命周期中所发生的事件。对于一个 EventLoop将由一个永远都不会改变的Thread驱动,同时任务可以直接提交给EventLoop,以立即执行或者调度执行。
根据配置和可用核心的不同,可能会创建多个EventLoop实例用以优化资源的使用,并且单个EventLoop可能会被指派用于服务多个Channel
Netty的EventLoop在继承了ScheduledExecutorService 的同时,只定义了一个方法parent()。在Netty 4中,所有的 I/O 操作和事件都由已经被分配给了EventLoop的那个Thread来处理
任务调度
偶尔,你将需要调度一个任务以便稍后(延迟)执行或者周期性地执行。例如,你可能想要注册一个在客户端已经连接了5分钟之后触发的任务。一个常见的用例是,发送心跳消息到远程节点,以检查连接是否仍然还活着。如果没有响应,你便知道可以关闭该Channel了
使用EventLoop调度任务
Channel channel = ctx.channel();
channel.eventLoop().schedule(new Runnable() {
public void run() {
//调度执行任务
}
}, 10, TimeUnit.SECONDS);
表示执行10秒后,任务将会由分配给Channel的EventLoop执行
Channel channel = ctx.channel();
//创建一个任务,进行调度执行
channel.eventLoop().scheduleAtFixedRate(new Runnable() {
public void run() {
//调度执行任务
}
}, 10,10, TimeUnit.SECONDS);
表示要调度的任务每隔10s就会执行一次
对于Netty提供的调度任务功能,对比于传统的JDK提供的任务调度API上有很大的性能提升,而性能提升的基础是需要依赖于Netty的底层线程模型
线程管理
Netty线程模型的卓越性能取决于对于当前执行的Thread的身份的确定,也就是说,确定它是否是分配给当前Channel以及它的EventLoop的那一个线程。如果(当前)调用线程正是支撑 EventLoop 的线程,那么所提交的代码块将会被直接执行。否则,EventLoop将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件
注意:永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任何其它任务。如果必须要进行阻塞调用或者执行长时间运行的任务,建议使用一个专门的EventExecutor
线程的分配
服务于Channel的I/O和事件的EventLoop包含在EventLoopGroup中,根据不同的传输实现,EventLoop的创建的分配方式也不同
异步传输
异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread),而且在当前的线程模型中,它们可能会被多个Channel所共享。这使得可以通过尽可能少量的Thread 来支撑大量的Channel,而不是每个Channel分配一个 Thread。
EventLoopGroup负责为每个新创建的Channel分配一个EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel
一旦一个Channel被分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的 Thread)。请牢记这一点,因为它可以使你从担忧你的ChannelHandler实现中的线程安全和同步问题中解脱出来
另外,需要注意,EventLoop的分配方式对ThreadLocal的使用的影响。因为一个EventLoop通常会被用于支撑多个 Channel,所以对于所有相关联的Channel来说,ThreadLocal都将是 一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上 下文中,它仍然可以被用于在多个Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件
阻塞传输
主要用于像OIO这样的其他传输,这里每一个Channel都将被分配给一个EventLoop(以及相关联的 Thread)
ChannelFuture
Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了ChannelFuture接口, 其 addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
可以将ChannelFuture看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行,此外,所有属于同一个Channel的操作都被保证其将以它们被调用的顺序被执行
ChannelHandler、ChannelPipeline和ChannelHandlerContext
ChannelHandler
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler的方法是由网络事件触发的。 事实上,ChannelHandler可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,例如各种编解码,或者处理转换过程中所抛出的异常
举例来说,ChannelInboundHandler是一个你将会经常实现的子接口。这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。 当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 直接冲刷数据然后输出到对端。应用程序的业务逻辑通常实现在一个或者多个 ChannelInboundHandler中
ChannelHandler子接口
Netty 定义了下面两个重要的 ChannelHandler 子接口:
- ChannelInboundHandler——处理入站数据以及各种状态变化;
- ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。
ChannelHandler的适配器类
Netty提供了许多不同类型的ChannelHandler,它们各自的功能主要取决于它们的超类。Netty以适配器类的形式提供了大量默认的ChannelHandler实现,其旨在简化应用程序处理逻辑的开发过程。
Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。 可以使用 ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter类作为自己 ChannelHandler的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandle 的基本实现。通过扩展抽象类 ChannelHandlerAdapter,它们获得了它们共同的超接口ChannelHandler 的方法
资源管理和SimpleChannelInboundHandler
回顾下《JDK原生网络编程-NIO基础入门》这篇文章,在NIO中是如何接收和发送网络数据的?首先创建了一个Buffer,应用程序中的业务部分和Channel之间通过Buffer进行数据的交换
Netty在处理网络数据时,同样也需要Buffer,在Read网络数据时由Netty创建Buffer, Write网络数据时Buffer往往是由业务方创建的。不管是读和写,Buffer用完后都必须进行释放,否则可能会造成内存泄露
在Write网络数据时,可以确保数据被写往网络了,Netty会自动进行Buffer的释放, 但是如果Write网络数据时,我们有ChannelOutboundHandler处理了write()操作并丢弃了数据,没有继续往下写,要由我们负责释放这个Buffer,就必须调用ReferenceCountUtil.release方法, 否则就可能会造成内存泄露
在Read网络数据时,如果我们可以确保每个ChannelInboundHandler都把数据往后传递了,也就是调用了相关的 fireChannelRead方法,Netty也会帮我们释放,同样的,如果我们有ChannelInboundHandler处理了数据,又不继续往后传递,又不调用负责释放的 ReferenceCountUtil.release 方法,就可能会造成内存泄露
但是由于消费入站数据是一项常规任务,所以Netty 提供了一个特殊的被称为 SimpleChannelInboundHandler的 ChannelInboundHandler实现。这个实现会在数据被channelRead0()方法消费之后自动释放数据
同时系统为我们提供的各种预定义Handler实现,都实现了数据的正确处理,所以我们自行在编写业务Handler时,也需要注意这一点:要么继续传递,要么自行释放
ChannelHandler 的生命周期
在ChannelHandler被添加到ChannelPipeline中或者被从ChannelPipeline中移除时会调用下面这些方法。这些方法中的每一个都接受一个 ChannelHandlerContext 参数
- handlerAdded: 当把ChannelHandler添加到ChannelPipeline中时被调用
- handlerRemoved: 当从ChannelPipeline中移除ChannelHandler时被调用
- exceptionCaught: 当处理过程中在ChannelPipeline中有错误产生时被调用
ChannelPipeline
ChannelPipeline提供了ChannelHandler链的容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel 被创建时,它将会被自动地分配一个新的ChannelPipeline,这项关联是永久性的;Channel既不能附加另外一个 ChannelPipeline,也不能分离其当前的。在Netty组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预
使得事件流经ChannelPipeline是ChannelHandler的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。实际上,被我们称为ChannelPipeline的是这些ChannelHandler的编排顺序
ChannelPipeline中ChannelHandler
入站和出站ChannelHandler可以被安装到同一个ChannelPipeline中。如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline的头部开始流动,最终,数据将会到达ChannelPipeline的尾端,届时,所有处理就都结束了
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作
如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline 中会发生什么。 虽然ChannelInboundHandle 和ChannelOutboundHandle都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler实现和 ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递
ChannelHandlerContext
通过使用作为参数传递到每个方法的 ChannelHandlerContext,事件可以被传递给当前 ChannelHandler 链中的下一个 ChannelHandler。虽然这个对象可以被用于获取底层的 Channel,但是它主要还是被用于写出站数据
ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。 ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互
ChannelHandlerContext 有很多的方法,其中一些方法也存在于 Channel 和 Channel-Pipeline 本身上,但是有一点重要的不同。如果调用Channel 或者ChannelPipeline 上 的这些方法,它们将沿着整个 ChannelPipeline 进行传播。而调用位于 ChannelHandlerContext上的相同方法,则将从当前所关联的 ChannelHandler 开始,并且只会传播给位于该 ChannelPipeline 中的下一个(入站下一个,出站上一个)能够处理该事件的 ChannelHandler
当使用ChannelHandlerContext 的API 的时候,有以下两点:
- ChannelHandlerContext 和ChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
- 相对于其他类的同名方法,ChannelHandlerContext的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。
引导
网络编程里,“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。因此,有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种 (ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据, 唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器
比较Bootstrap类 (摘自《Netty In Action》)
. | Bootstrap | ServerBootstrap |
---|---|---|
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数目 | 1 | 2 |
第一个区别:ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap则是由想要连接到远程节点的客户端应用程序所使用的
第二个区别:引导一个客户端只需要一个EventLoopGroup,但是一个 ServerBootstrap 则需要两个(也可以是同一个实例)
因为服务器需要两组不同的 Channel。第一组将只包含一个 ServerChannel,代表服务器 自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理 传入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel
与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。一旦连接被接受,第二个EventLoopGroup就会给它的Channel分配 一个EventLoop
总结
本文详细地介绍了Netty的一些概念和核心组件,包括ChannelHandler、EventLoop、ChannelPipeline等,并且重点地介绍了ChannelHandler类的层次结构。对于要深入学习Netty框架,这些组件是必须要深入理解的
本文地址:https://blog.csdn.net/wzljiayou/article/details/110202267