Netty实战三之Netty的组件和设计

有关Netty,我们可以从两个视角来讨论Netty:类库的视角以及框架的视角,对于使用Netty编写高效的、可重用的和可维护的代码来说,两者缺一不可。

Netty解决了两个响应的关注领域,可以大致标志为技术的和体系结构的。

它基于Java NIO的异步和事件驱动的实现,保证了高负载下应用程序性能的最大化和可伸缩性。其次,Netty也包含了一组设计模式,将应用程序逻辑从网络层解耦,简化开发过程,同时也最大限度地提高了可测试性、模块化以及代码的可重用性。

Netty网络抽象的代表:

——Channel:Socket

——EventLoop:控制流、多线程处理、并发

——ChannelFuture:异步通知

1、Channel接口

Netty的Channel接口所提供的的API,大大降低了直接使用Socket类的复杂性,此外,Channel也拥有许多预定义的、专门化实现的广泛类层次结构的根。

——EmbeddedChannel

——LocalServerChannel

——NioDatagramChannel

——NioSctpChannel

——NioSocketChannel

2、EventLoop接口

EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。图3-1在高层次上说明了Channel、EventLoop、Thread以及EventLoopGroup之间的关系。

Netty实战三之Netty的组件和设计

——EventLoopGroup包含一个或者多个EventLoop

——一个EventLoop在它的生命周期内只和一个Thread绑定

——所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理

——一个Channel在它的生命周期内只注册于一个EventLoop

——一个EventLoop可能会被分配给一个或多个Channel

注意,在这种设计中,一个给定Channel的I/O操作都是由相同的Thread执行的,实际上消除了对于同步的需要。

3、ChannelFuture接口

Netty中所有的I/O操作都是异步的,因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法,为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。

关于ChannelFuture的更多讨论——可以将ChannelFuture看作是将来要执行的结果的占位符,它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行,此外,所有属于同一个Channel的操作都被保证其将以它们被调用的顺序被执行。

4、ChannelHandler接口

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

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

5、ChannelPipeline接口

ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,它会被自动地分配到它专属的ChannelPipeline。

——一个ChannelInitializer的实现被注册到了ServerBootstrap中; ——当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler; ——ChannelInitializer将它自己从ChannelPipeline中移除;

ChannelHandler是专为支持广泛的用途而设计的,可以将它看作是处理往来ChannelPipeline事件(包括数据)的任何代码的通用容器。图2-3说明了这一点,其展示了从ChannelHandler派生的ChannelInboundHandler和ChannelOutboundHandler接口。

Netty实战三之Netty的组件和设计

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

图3-3说明了一个Netty应用程序中入站和出站数据流之间的区别。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站,反之则称为入站。

Netty实战三之Netty的组件和设计

由上图可看出入站和出站ChannelHandler可以被安装到同一个ChannelPipeline中,事件的读取将从ChannelPipeline的头部开始流动,并被传递给第一个ChannelInboundHandler,这个ChannelHandler不一定会实际修改数据,具体取决于它的具体功能,在之后,数据将会传递给链中的下一个ChannelInboundHandler,最终,数据将会到达ChannelPipeline的尾端,即所有处理结束。

出站事件也相同的流程,在到达链的头部之后,出站数据将会到达网络传输层,这里显示为Socket,通常情况下,这将触发一个写操作。

关于入站和出站ChannelHandler的更多讨论

通过使用作为参数传递到每个方法的ChannelHandlerContext,事件可以被传递给当前ChannelHandler链中的下一个ChannelHandler。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。通过调用ChannelHandlerContext上的对应方法,每个都提供了简单地将事件传递给下一个ChannelHandler的方法的实现,随后,你可以通过重写你所感兴趣的那些方法来扩展这些类。

虽然ChannelInboundHandler和ChannelOutboundHandler都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递。

当ChannelHandler被添加到ChannelPipeline时,它将被分配一个ChannelHandlerContext,其代表了ChannelHandler和ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。

在Netty中,有两种发送消息的方式。你可以直接写到Channel中,也可以写到ChannelHandler相关联的ChannelHandlerContext对象中,前一种方式将会导致消息从ChannelPipeline的尾端开始流动,而后者将导致消息从ChannelPipeline中的下一个ChannelHandler开始流动。

6、深入了解ChannelHandler

有许多不同类型的ChannelHandler,它们各自的功能主要取决于它们的超类。Netty以适配器类的形式提供了大量默认的ChannelHandler实现,其旨在简化应用程序处理逻辑的开发过程,ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个ChannelHandler,这些适配器将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。

为什么需要适配器类

有些适配器类可以将编写自定义的ChannelHandler所需要的努力降到最低限度,因为他们提供了定义在对应接口中的所有方法的默认实现。

——ChannelHandlerAdapter

——ChannelInboundHandlerAdapter

——ChannelOutboundHandlerAdapter

——ChannelDuplexHandler

7、编码器和解码器

当你通过Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站就解码为Java对象,出站就编码为字节。(网络数据总是一系列的字节)

所有由Netty提供的编码器、解码器适配器类都实现了ChannelOutboundHandler或者ChannelInboundHandler接口。

你将会发现对于入站数据,channelRead方法、事件已经被重写了,对于每个从入站Channel读取消息,这个方法都将会被调用,随后,它将调用由预置解码器所提供的的decode()方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。

出站消息的模式是相反方向的:编码器将消息转换为字节,并将它们转发给下一个ChannelOutboundHandler。

8、抽象类SimpleChannelInboundHandler

最常见的情况是,你的应用程序会利用一个ChannelHandler来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler,其中T是你要处理的消息的Java类型,在这个ChannelHandler中,你将需要重写基类的一个或者多个方法,并且获取一个到ChannelHandlerContext的引用,这个引用将作为输入参数传递给ChannelHandler的所有方法。

在这种类型的ChannelHandler中,最重要的方法是channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于你。

9、引导

Netty的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。

“服务器”和“客户端”实际上表示了不同的网络行为:是监听传入的连接还是建立到一个或者多个进程的连接。

面向连接的协议:请记住,严格来说,“连接”这个属于仅适用于面向连接的协议,如TCP、其保证了两个连接端点之间消息的有序传递。

因此,有两种类型的引导:一种用于客户端(简单称为Bootstrap),而另一种(ServerBootstrap)用于服务器。无论你的应用程序使用哪些协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
Netty实战三之Netty的组件和设计

这两种类型的引导类之间的第一个区别已经讨论过了:ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap则由想要连接到远程节点的客户端应用程序所使用的。

第二个区别可能更加明显,引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个(也可以是同一个实例),为什么呢?

因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。
Netty实战三之Netty的组件和设计

与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。一旦连接被接收,第二个EventLoopGroup就会给它的Channel分配一个EventLoop。

上一篇:Netty(二十一) - Netty核心模块


下一篇:Netty学习笔记