目录

引导

本章主要内容

  • 引导客户端和服务器
  • 从 Channel 内引导客户端
  • 添加 ChannelHandler
  • 使用 ChannelOption 和属性

在深入地学习了 ChannelPipeline 、 ChannelHandler 和 EventLoop 之后,你接下来 的问题可能是:“如何将这些部分组织起来,成为一个可实际运行的应用程序呢?”

答案是?“引导”(Bootstrapping)。到目前为止, 我们对这个术语的使用还比较含糊,现 在已经到了精确定义它的时候了。简单来说,引导一个应用程序是指对它进行配置,并使它运 行起来的过程—尽管该过程的具体细节可能并不如它的定义那样简单, 尤其是对于一个网络 应用程序来说。

和它对应用程序体系架构的做法 一致,Netty处理引导的方式使你的应用程序 和网络层相 隔离,无论它是客户端还是服务器。正如同你将要看到的,所有的框架组件都将会在后台结合在 一起并且启用。引导是我们一直以来都在组装的完整拼图 中缺失的那一块。当你把它放到正确 的位置上时,你的Netty应用程序就完整了。

应用程序体系架构,指的是分层抽象

“拼图”指的是 Netty 的核心概念以及组件,也包括了如何完整正确地组织并且运行一个 Netty 应用程序。

Bootstrap类

引导类的层次结构包括一个抽象的父类和两个具体的引导子类,如图 8-1 所示。

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

相对于将具体的引导类分别看作用于服务器和客户端的引导来说,记住它们的本意是用来支撑不同的应用程序的功能的将有所裨益。也就是说,服务器致力于使用一个父 Channel 来接受来自客户端的连接,并创建子 Channel 以用于它们之间的通信;而客户端将最可能只需要一个 单独的、没有父 Channel 的 Channel 来用于所有的网络交互。(正如同我们将要看到的,这也适用于无连接的传输协议,如 UDP,因为它们并不是每个连接都需要一个单独的 Channel 。)

TCP是可靠的面向连接的协议,UDP协议是一种不可靠的无连接的协议

TCP的可靠是体现在其通信过程必须经过三个阶段:连接建立、数据通信、连接撤销。并且使用了确认机制。因此其通信结果是可掌握的,必然成功的。 而UDP则几乎把传输的成功率完全依赖在IP协议上了,即尽最大努力去完成传输任务。它没有建立连接和撤销连接阶段,因此传输的结果有可能是失败,因此称为不可靠的协议。

我们在前面的几章中学习的几个 Netty 组件都参与了引导的过程,而且其中一些在客户端和 服务器都有用到。两种应用程序类型之间通用的引导步骤由 AbstractBootstrap 处理,而特 定于客户端或者服务器的引导步骤则分别由 Bootstrap 或 ServerBootstrap 处理。

在本章中接下来的部分,我们将详细地探讨这两个类,首先从不那么复杂的 Bootstrap 类 开始。

为什么引导类是 Cloneable 的

你有时可能会需要创建多个具有类似配置或者完全相同配置的 Channel 。为了支持这种模式而又不 需 要 为 每 个 Channel 都 创 建 并 配 置 一 个 新 的 引 导 类 实 例 , AbstractBootstrap 被 标 记 为 了 Cloneable 。在一个已经配置完成的引导类实例上调用 clone() 方法将返回另一个可以立即使用的引 导类实例。

注意,这种方式只会创建引导类实例的 EventLoopGroup 的一个浅拷贝,所以,后者 将在所有克隆的 Channel 实例之间共享。这是可以接受的,因为通常这些克隆的 Channel 的生命周期都很短暂,一个典型的场景是——创建一个 Channel 以进行一次HTTP请求。

AbstractBootstrap 类的完整声明是:

1
2
public abstract class AbstractBootstrap 
  	<B extends AbstractBootstrap<B,C>,C extends Channel>

在这个签名中,子类型 B 是其父类型的一个类型参数,因此可以返回到运行时实例的引用以 支持方法的链式调用(也就是所谓的流式语法)。

其子类的声明如下:

1
public class Bootstrap extends AbstractBootstrap<Bootstrap,Channel>

1
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap,ServerChannel>

引导客户端和无连接协议

Bootstrap 类被用于客户端或者使用了无连接协议的应用程序中。表 8-1 提供了该类的一 个概览,其中许多方法都继承自 AbstractBootstrap 类。

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

① 这里指默认的无参构造函数,因为内部使用了反射来实现 Channel 的创建。

下一节将一步一步地讲解客户端的引导过程。我们也将讨论在选择可用的组件实现时保持兼 容性的问题。

引导客户端

Bootstrap 类负责为客户端和使用无连接协议的应用程序创建 Channel ,如图 8-2 所示。

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

代码清单 8-1 中的代码引导了一个使用 NIO TCP 传输的客户端。

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

这个示例使用了前面提到的流式语法;这些方法(除了 connect() 方法以外)将通过每次 方法调用所返回的对 Bootstrap 实例的引用链接在一起。

Channel和EventLoopGroup的兼容性

代码清单 8-2 所示的目录清单来自 io.netty.channel 包。你可以从包名以及与其相对应 的类名的前缀看到, 对于 NIO 以及 OIO 传输两者来说, 都有相关的 EventLoopGroup 和 Channel 实现。

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

必须保持这种兼容性, 不能混用具有不同前缀的组件, 如 NioEventLoopGroup 和 OioSocketChannel 。代码清单 8-3 展示了试图这样做的一个例子。

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

这段代码将会导致 IllegalStateException,因为它混用了不兼容的传输。

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

关于 IllegalStateException 的更多讨论

在引导的过程中,在调用bind() 或者 connect() 方法之前,必须调用以下方法来设置所需的组件:

  • group();
  • channel() 或者 channelFactory() ;
  • handler()

如果不这样做,则将会导致 IllegalStateException 。对 handler() 方法的调用尤其重要,因 为它需要配置好 ChannelPipeline 。

引导服务器

我们将从 ServerBootstrap API 的概要视图开始我们对服务器引导过程的概述。然后, 我们将会探讨引导服务器过程中所涉及的几个步骤, 以及几个相关的主题, 包含从一个 ServerChannel 的子 Channel 中引导一个客户端这样的特殊情况。

ServerBootstrap类

表 8-2 列出了 ServerBootstrap 类的方法。

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

下一节将介绍服务器引导的详细过程。

引导服务器

你可能已经注意到了,表 8-2 中列出了一些在表 8-1 中不存在的方法: childHandler() 、 childAttr() 和 childOption() 。这些调用支持特别用于服务器应用程序的操作。具体来说, ServerChannel 的实现负责创建子 Channel ,这些子 Channel 代表了已被接受的连接。因此,负责引导 ServerChannel 的 ServerBootstrap 提供了这些方法,以简化将设置应用到 已被接受的子 Channel 的 ChannelConfig 的任务

图 8-3 展示了 ServerBootstrap 在 bind() 方法被调用时创建了一个 ServerChannel , 并且该 ServerChannel 管理了多个子 Channel 。

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

代码清单 8-4 中的代码实现了图 8-3 中所展示的服务器的引导过程。

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

从Channel引导客户端

假设你的服务器正在处理一个客户端的请求,这个请求需要它充当第三方系统的客户端。当 一个应用程序(如一个代理服务器)必须要和组织现有的系统(如 Web 服务或者数据库)集成 时,就可能发生这种情况。在这种情况下,将需要从已经被接受的子 Channel 中引导一个客户 端 Channel 。

你可以按照 8.2.1 节中所描述的方式创建新的 Bootstrap 实例,但是这并不是最高效的解 决方案,因为它将要求你为每个新创建的客户端 Channel 定义另一个 EventLoop 。这会产生 额外的线程,以及在已被接受的子 Channel 和客户端 Channel 之间交换数据时不可避免的上 下文切换。

一个更好的解决方案是:通过将已被接受的子 Channel 的 EventLoop 传递给 Bootstrap 的 group() 方法来共享该 EventLoop 。因为分配给 EventLoop 的所有 Channel 都使用同一 个线程,所以这避免了额外的线程创建,以及前面所提到的相关的上下文切换。这个共享的解决 方案如图 8-4 所示。

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

实现 EventLoop 共享涉及通过调用 group() 方法来设置 EventLoop ,如代码清单 8-5 所示。

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

我们在这一节中所讨论的主题以及所提出的解决方案都反映了编写 Netty 应用程序的一个一 般准则:尽可能地重用 EventLoop ,以减少线程创建所带来的开销。

在引导过程中添加多个ChannelHandler

在所有我们展示过的代码示例中,我们都在引导的过程中调用了 handler() 或者 child- Handler() 方法来添加单个的 ChannelHandler 。这对于简单的应用程序来说可能已经足够了,但是它不能满足更加复杂的需求。例如,一个必须要支持多种协议的应用程序将会有很多的 ChannelHandler ,而不会是一个庞大而又笨重的类。

正如你经常所看到的一样,你可以根据需要,通过在 ChannelPipeline 中将它们链接在一起来 部署尽可能多的 ChannelHandler 。但是,如果在引导的过程中你只能设置一个 ChannelHandler , 那么你应该怎么做到这一点呢?

正是针对于这个用例,Netty 提供了一个特殊的 ChannelInboundHandlerAdapter 子类:

1
2
public abstract class ChannelInitializer<C extends Channel> 
  extends ChannelInboundHandlerAdapter

它定义了下面的方法:

1
protected abstract void initChannel(C ch) throws Exception;

这个方法提供了一种将多个 ChannelHandler 添加到一个 ChannelPipeline 中的简便 方法。 你只需要简单地向 Bootstrap 或 ServerBootstrap 的实例提供你的 Channel- Initializer 实现即可,并且一旦 Channel 被注册到了它的 EventLoop 之后,就会调用你的 initChannel() 版本。在该方法返回之后, ChannelInitializer 的实例将会从 Channel- Pipeline 中移除它自己。

代码清单 8-6 定义了 ChannelInitializerImpl 类, 并通过 ServerBootstrap 的 childHandler() 方法注册它 。你可以看到,这个看似复杂的操作实际上是相当简单直接的。

注册到 ServerChannel 的子 Channel 的 ChannelPipeline。

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

在大部分的场景下,如果你不需要使用只存在于SocketChannel上的方法,使用ChannelInitializer<Channel>就可以了,否则你可以使用 ChannelInitializer<SocketChannel>,其中 SocketChannel扩展了 Channel。

如果你的应用程序使用了多个 ChannelHandler ,请定义你自己的 ChannelInitializer实现来将它们安装到 ChannelPipeline 中。

使用Netty的ChannelOption属性

在每个 Channel 创建时都手动配置它可能会变得相当乏味。幸运的是,你不必这样做。相 反,你可以使用 option() 方法来将 ChannelOption 应用到引导。你所提供的值将会被自动 应用到引导所创建的所有 Channel 。可用的 ChannelOption 包括了底层连接的详细信息,如 keep-alive 或者超时属性以及缓冲区设置。

Netty 应用程序通常与组织的专有软件集成在一起,而像 Channel 这样的组件可能甚至会在 正常的 Netty 生命周期之外被使用。 在某些常用的属性和数据不够用时, Netty 提供了 AttributeMap 抽象(一个由 Channel 和引导类提供的集合)以及 AttributeKey<T> (一 个用于插入和获取属性值的泛型类)。使用这些工具,便可以安全地将任何类型的数据项与客户端和服务器 Channel (包含 ServerChannel 的子 Channel )相关联了。

例如,考虑一个用于跟踪用户和 Channel 之间的关系的服务器应用程序。这可以通过将用 户的 ID 存储为 Channel 的一个属性来完成。类似的工作可以被用来基于用户的 ID 将消息路由给用户,或者关闭活动较少的 Channel 。

代码清单 8-7 展示了可以如何使用 ChannelOption来配置Channel,以及如何使用属性来存储整型值。

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

引导DatagramChannel

前面的引导代码示例使用的都是基于 TCP 协议的 SocketChannel ,但是 Bootstrap 类 也可以被用于无连接的协议。为此,Netty 提供了各种 DatagramChannel 的实现。唯一区别就 是,不再调用 connect() 方法,而是只调用 bind() 方法,如代码清单 8-8 所示。

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

关闭

引导使你的应用程序启动并且运行起来,但是迟早你都需要优雅地将它关闭。当然,你也可 以让 JVM 在退出时处理好一切,但是这不符合优雅的定义,优雅是指干净地释放资源。关闭 Netty 应用程序并没有太多的魔法,但是还是有些事情需要记在心上。

最重要的是,你需要关闭 EventLoopGroup ,它将处理任何挂起的事件和任务,并且随后 释放所有活动的线程。这就是调用 EventLoopGroup.shutdownGracefully() 方法的作用。 这个方法调用将会返回一个 Future ,这个 Future 将在关闭完成时接收到通知。需要注意的是, shutdownGracefully() 方法也是一个异步的操作,所以你需要阻塞等待直到它完成,或者向 所返回的 Future 注册一个监听器以在关闭完成时获得通知。

代码清单 8-9 符合优雅关闭的定义。

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

或者,你也可以在调用 EventLoopGroup.shutdownGracefully() 方法之前,显式地 在所有活动的 Channel 上调用 Channel.close() 方法。但是在任何情况下,都请记得关闭 EventLoopGroup 本身。

小结

在本章中,你学习了如何引导 Netty 服务器和客户端应用程序,包括那些使用无连接协议的 应用程序。我们也涵盖了一些特殊情况,包括在服务器应用程序中引导客户端 Channel ,以及 使用 ChannelInitializer 来处理引导过程中的多个 ChannelHandler 的安装。你看到了 如何设置 Channel 的配置选项,以及如何使用属性来将信息附加到 Channel 。最后,你学习了 如何优雅地关闭应用程序,以有序地释放所有的资源。

在下一章中,我们将研究 Netty 提供的帮助你测试你的 ChannelHandler 实现的工具。