目录

Netty的组件和设计

本章主要内容

  • Netty 的工作和体系结构方面的内容
  • Channel、EventLoop 和 ChannelFuture
  • ChannelHandler 和 ChannelPipeline
  • 引导

在第 1 章中,我们给出了 Java 高性能网络编程的历史以及工作基础的小结。这为 Netty 的核 心概念和构件块的概述提供了背景。

在第 2章中,我们把我们的讨论范围扩大到了应用程序的开发。通过构建一个简单的客户端 和服务器,你学习了引导,并且获得了最重要的 ChannelHandler API 的实战经验。与此同时, 你也验证了自己的开发工具都能正常运行。

由于本书剩下的部分都建立在这份材料的基础之上,所以我们将从两个不同的但却又密切相 关的视角来探讨 Netty:类库的视角以及框架的视角。对于使用 Netty 编写高效的、可重用的和 可维护的代码来说,两者缺一不可。

框架的英文为Framework意思是框架、机制、准则。最早是源于建筑行业,是一个框子——指其约束性,也是一个架子——指其支撑性。是一个基本概念上的结构,用于去解决或者处理复杂的问题。

现在,框架这个广泛的定义使用的十分流行,尤其在软件行业。在软件工程中,框架被定义为整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种定义认为,框架是可被应用开发者定制的应用骨架。

Netty框架的抽象构件:Channel、EventLoop、ChannelFuture

通俗的说,框架是实现某种功能的半成品,提供了一些常用的工具类和一些基础通用化的组件,可以供开发人员在此基础上,更高效的满足各自的业务需求。

btw 提一下中间件

  • 中间件是一类软件,它为上层的应用软件提供运行与开发的环境,帮助用户开发和集成应用软件。它不仅仅要实现互连,还要实现应用之间的互操作;最突出的特点是其网络通信功能。最流行的交易中间件为Tuxedo。有两个关键特征,为上层的应用层服务;必须连接到操作系统的层面,并确保持运行工作状态。
  • 中间件是一种应用于分布式系统的基础软件,位于应用与操作系统、数据库之间,主要用于解决分布式环境下数据传输、数据访问、应用调度、系统构建和系统集成、流程管理等问题,是分布式环境下支撑应用开发、运行和集成的平台。
  • 中间件产品开发的核心思想是抽取分布式系统对于数据传输、信息系统构建与集成等问题的共性要求,封装共性问题的解决方法,对外提供简单统一的接口,从而减少开发人员面对上述共性问题时的难度和重复性工作量,提高系统的开发效率。

从高层次的角度来看,Netty 解决了两个相应的关注领域,我们可将其大致标记为工作的和 体系结构的。首先,它的基于 Java NIO 的异步的和事件驱动的实现,保证了高负载下应用程序 性能的最大化和可伸缩性。其次,Netty 也包含了一组设计模式,将应用程序逻辑从网络层解耦, 简化了开发过程,同时也最大限度地提高了可测试性、模块化以及代码的可重用性。

在我们更加详细地研究 Netty 的各个组件时,我们将密切关注它们是如何通过协作来支撑这 些体系结构上的最佳实践的。通过遵循同样的原则,我们便可获得 Netty 所提供的所有益处。牢 记这个目标,在本章中,我们将回顾到目前为止我们介绍过的主要概念和组件。

所谓架构,不仅要掌握体系结构,还要掌握各个组件的工作实现细节

就如同一个建筑工程师,不仅要掌握设计一栋建筑的结构,还要对各种建筑材料有一定的了解。

Channel、EventLoop和ChannelFuture

接下来的各节将会为我们对于 Channel 、 EventLoop 和 ChannelFuture 增添更多的细节,这些类合在一起,可以被认为是 Netty 网络抽象的代表

  • Channel — Socket ;
  • EventLoop —控制流、多线程处理、并发;
  • ChannelFuture 异步通知

Channel接口

基本的 I/O 操作( bind() 、 connect() 、 read() 和 write() )依赖于底层网络传输所提 供的原语。在基于 Java 的网络编程中,其基本的构造是 class Socket 。Netty 的 Channel 接 口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。此外, Channel 也是拥有许多 预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:

  • EmbeddedChannel;
  • LocalServerChannel;
  • NioDatagramChannel;
  • NioSctpChannel;
  • NioSocketChannel

EventLoop接口

EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。我们将 在第 7 章中结合 Netty 的线程处理模型的上下文对 EventLoop 进行详细的讨论。目前,图 3-1 在高层次上说明了 Channel 、 EventLoop 、 Thread 以及 EventLoopGroup 之间的关系。

https://gitee.com/lienhui68/picStore/raw/master/null/20200904013924.png

这些关系是:

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop ;
  • 一个EventLoop在它的生命周期内只和一个Thread绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
  • 一个 EventLoop 可能会被分配给一个或多个 Channel 。

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

ChannelFuture接口

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

关于 ChannelFuture 的更多讨论

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

我们将在第 7 章中深入地讨论EventLoop和EventLoopGroup。

ChannelHandler和ChannelPipeline

现在,我们将更加细致地看一看那些管理数据流以及执行应用程序处理逻辑的组件。

ChannelHandler接口

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

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

ChannelPipeline接口

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

ChannelHandler 安装到 ChannelPipeline 中的过程如下所示:

  • 一个 ChannelInitializer 的实现被注册到了 ServerBootstrap 或者用于客户端的 Bootstrap中

  • 当 ChannelInitializer.initChannel() 方法被调用时, ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的 ChannelHandler ;

    ChannelInitializer 本身也是一个ChannelHandler,负责在channel被创建时执行初始化工作——安装一组自定义的ChannelHandler并将自己移除。

  • ChannelInitializer 将它自己从 ChannelPipeline 中移除。

为了审查发送或者接收数据时将会发生什么,让我们来更加深入地研究 ChannelPipeline 和 ChannelHandler 之间的共生关系吧。

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

https://gitee.com/lienhui68/picStore/raw/master/null/20200904021734.png

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

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

https://gitee.com/lienhui68/picStore/raw/master/null/20200904022045.png

图 3-3 也显示了入站和出站 ChannelHandler 可以被安装到同一个 ChannelPipeline 中。如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部 开始流动,并被传递给第一个 ChannelInboundHandler 。这个 ChannelHandler 不一定 会实际地修改数据, 具体取决于它的具体功能, 在这之后, 数据将会被传递给链中的下一个 ChannelInboundHandler 。最终,数据将会到达 ChannelPipeline 的尾端,届时,所有 处理就都结束了。

数据的出站运动(即正在被写的数据)在概念上也是一样的。 在这种情况下, 数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站 数据将会到达网络传输层,这里显示为 Socket 。通常情况下,这将触发一个写操作。

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

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

说白了,就是适配器模式和责任链设计模式

鉴于出站操作和入站操作是不同的,你可能会想知道如果将两个类别的 ChannelHandler 都混合添加到同一个 ChannelPipeline 中会发生什么。虽然 ChannelInboundHandle 和 ChannelOutboundHandle 都扩展自 ChannelHandler ,但是 Netty 能区分 ChannelInboundHandler 实现和 ChannelOutboundHandler 实现, 并确保数据只会在具有相同定向类型的两个 ChannelHandler 之间传递。

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

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

更加深入地了解ChannelHandler接口

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

为什么需要适配器类

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

下面这些是编写自定义 ChannelHandler 时经常会用到的适配器类:

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandler

接下来我们将研究 3 个 ChannelHandler 的子类型:编码器、解码器和 SimpleChannelInboundHandler<T> —— ChannelInboundHandlerAdapter 的一个子类。

编码器和解码器

当你通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解 码;也就是说,从字节转换为另一种格式,通常是一个 Java 对象。如果是出站消息,则会发生 相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数 据总是一系列的字节。

对应于特定的需要,Netty 为编码器和解码器提供了不同类型的抽象类。例如,你的应用程 序可能使用了一种中间格式,而不需要立即将消息转换成字节。你将仍然需要一个编码器,但是 它将派生自一个不同的超类。为了确定合适的编码器类型,你可以应用一个简单的命名约定。

通常来说, 这些基类的名称将类似于 ByteToMessageDecoder 或 MessageToByte- Encoder 。对于特殊的类型,你可能会发现类似于 ProtobufEncoder 和 ProtobufDecoder 这样的名称——预置的用来支持 Google 的 Protocol Buffers。

严格地说, 其他的处理器也可以完成编码器和解码器的功能。 但是, 正如有用来简化 ChannelHandler 的创建的适配器类一样,所有由 Netty 提供的编码器/解码器适配器类都实现 了 ChannelOutboundHandler 或者 ChannelInboundHandler 接口。

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

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

抽象类SimpleChannelInboundHandler

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

在这种类型的 ChannelHandler 中, 最重要的方法是 channelRead0(Channel- HandlerContext,T) 。除了要求不要阻塞当前的 I/O 线程之外,其具体实现完全取决于你。我 们稍后将对这一主题进行更多的说明。

ChannelInboundHandler 和 SimpleChannelInboundHandler的区别

最主要的区别就是SimpleChannelInboundHandler在接收到数据后会自动release掉数据占用的Bytebuffer资源(自动调用Bytebuffer.release())。而为何服务器端不能用呢,因为我们想让服务器把客户端请求的数据发送回去,而服务器端有可能在channelRead方法返回前还没有写完数据,因此不能让它自动release。

引导

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

通常来说,我们把前面的用例称作引导一个服务器,后面的用例称作引导一个客户端。虽然 这个术语简单方便,但是它略微掩盖了一个重要的事实,即“服务器”和“客户端”实际上表示 了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接

面向连接的协议

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

因此, 有两种类型的引导:一种用于客户端(简单地称为 Bootstrap ), 而另一种 ( ServerBootstrap )用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据, 唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。表 3-1 比较了这两种 类型的引导类。

https://gitee.com/lienhui68/picStore/raw/master/null/20200904031357.png

实际上,ServerBootstrap 类也可以只使用一个 EventLoopGroup,此时其将在两个场景下共用 同一个 EventLoopGroup。

前面写的Echo服务端b.group(group)就是这样,其内部实现是b.group(group,group),使用的同一个group。

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

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

因为服务器需要两组不同的 Channel 。第一组将只包含一个 ServerChannel ,代表服务 器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传 入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel 。图 3-4 说明了这个模 型,并且展示了为何需要两个不同的 EventLoopGroup 。

https://gitee.com/lienhui68/picStore/raw/master/null/20200904031806.png

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

小结

在本章中,我们从工作和体系结构这两个角度探讨了理解 Netty 的重要性。我们也更加详 ChannelPipeline 细地重新审视了之前引入的一些概念和组件,特别是 ChannelHandler 、 ChannelPipeline和引导。

特别地,我们讨论了 ChannelHandler 类的层次结构,并介绍了编码器和解码器,描述了 它们在数据和网络字节格式之间来回转换的互补功能。

下面的许多章节都将致力于深入研究这些组件,而这里所呈现的概览应该有助于你对整体 的把控。

下一章将探索 Netty 所提供的不同类型的传输,以及如何选择一个最适合于你的应用程序的传输。