目录

预置的ChannelHandler和编解码器

本章主要内容

  • 通过 SSL/TLS 保护 Netty 应用程序
  • 构建基于 Netty 的 HTTP/HTTPS 应用程序
  • 处理空闲的连接和超时
  • 解码基于分隔符的协议和基于长度的协议
  • 写大型数据

Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相 当繁琐的事务上本来会花费的时间与精力。在本章中,我们将探讨这些工具以及它们所带来的好 处,其中包括 Netty 对于 SSL/TLS 和 WebSocket 的支持,以及如何简单地通过数据压缩来压榨 HTTP,以获取更好的性能。

通过 SSL/TLS 保护 Netty 应用程序

如今,数据隐私是一个非常值得关注的问题,作为开发人员,我们需要准备好应对它。至少, 我们应该熟悉像SSL和TLS(传输层安全协议) 这样的安全协议,它们层叠在其他协议之上,用以实现数据安全。我 们在访问安全网站时遇到过这些协议,但是它们也可用于其他不是基于HTTP的应用程序,如安 全SMTP(SMTPS)邮件服务器甚至是关系型数据库系统。

为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 类使得实现解密和加密相当简单直接。Netty 通过一个名为 SslHandler 的 ChannelHandler 实现利用了这个 API,其中 SslHandler 在内部使用 SSLEngine 来完成实际的工作。

图 11-1 展示了使用 SslHandler 的数据流。

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

Netty 的 OpenSSL/SSLEngine 实现

Netty 还提供了使用 OpenSSL 工具包(www.openssl.org)的 SSLEngine实现。这个 OpenSslEngine 类提供了比 JDK 提供的 SSLEngine 实现更好的性能。

如果 OpenSSL 库可用,可以将 Netty 应用程序(客户端和服务器)配置为默认使用 OpenSslEngine 。 如果不可用,Netty 将会回退到 JDK 实现。有关配置 OpenSSL 支持的详细说明,参见 Netty 文档: http://netty.io/wiki/forked-tomcat-native.html#wikih2-1

注意,无论你使用 JDK 的SSLEngine 还是使用 Netty 的 OpenSslEngine ,SSL API 和数据流都 是一致的。

代码清单11-1展示了如何使用 ChannelInitializer 来将 SslHandler 添加到 Channel- Pipeline 中。回想一下, ChannelInitializer 用于在 Channel 注册好时设置 ChannelPipeline 。

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

在大多数情况下, SslHandler 将是 ChannelPipeline 中的第一个 ChannelHandler 。 这确保了在出站时只有在所有其他的 ChannelHandler 将它们的逻辑应用到数据之后,才会进行加密。

SslHandler 具有一些有用的方法,如表 11-1 所示。例如,在握手阶段,两个节点将相互 验证并且商定一种加密方式。你可以通过配置 SslHandler 来修改它的行为,或者在 SSL/TLS 握手一旦完成之后提供通知,握手阶段完成之后,所有的数据都将会被加密。SSL/TLS 握手将会 被自动执行。

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

构建基于 Netty 的 HTTP/HTTPS 应用程序

HTTP/HTTPS 是最常见的协议套件之一,并且随着智能手机的成功,它的应用也日益广泛, 因为对于任何公司来说,拥有一个可以被移动设备访问的网站几乎是必须的。这些协议也被用于 其他方面。许多组织导出的用于和他们的商业合作伙伴通信的 WebService API 一般也是基于 HTTP(S)的。

什么是WebService

WebService是一种跨编程语言、跨操作系统平台的远程调用工作。

远程调用工作:远程调用是指一台设备上的程序A可以调用另一台设备上的方法B。比如:银联提供给商场的pos刷卡系统,商场的pos机转账调用的转账方法的代码其实是跑在银行服务器上的。再比如,amazon,天气预报系统,淘宝网,校内网,百度等把自己的系统服务以WebService服务的形式暴露出来,让第三方网站和程序可以调用这些服务功能,这样扩展了自己系统的市场占有率。

跨编程语言:是指服务端、客户端程序的编程语言可以不同

跨操作系统平台:是指服务端、客户端可在不同的操作系统上运行

从表面上看,WebService是指一个应用程序向外界暴露了一个能通过Web调用的API接口,我们把调用这个WebService的应用程序称作客户端,把提供这个WebService的应用程序称作服务端。

从深层上看,WebService是建立可互操作的分布式应用程序的新平台,是一个平台,是一套标准。它定义了应用程序如何通过Web实现互操作性,通过WebService标准对服务进行查询和访问。

如何实现WebService

远程调用

远程调用的过程是这样的,先从客户端和服务端的角度考虑,客户端向服务端发送服务请求,服务端接收请求并处理,再向客户端回复请求,客户端接收回复。接着,从请求本身的角度考虑,请求和回复各自表现为一组数据,数据具有某种表示形式(XML)和类型标准(XSD),数据通过某传输协议(HTTP)通过网络进行传输。

客户端进行服务的远程调用前,需要知道服务的地址与服务有什么方法可以调用。因此,WebService服务端通过一个文件(WSDL)来说明自己家里有啥服务可以对外调用,服务是什么(服务中有哪些方法,方法接受 的参数是什么,返回值是什么),服务的网络地址用哪个url地址表示,服务通过什么方式来调用。WSDL(Web Services Description Language)是一个基于XML的语言,用于描述Web Service及其函数、参数和返回值。它是WebService客户端和服务器端都能理解的标准格式。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的,这将是一个很大的好处。一些最新的开发工具既能根据你的 Web service生成WSDL文档,又能导入WSDL文档,生成调用相应WebService的代理类代码。WSDL 文件保存在Web服务器上,通过一个url地址就可以访问到它。客户端要调用一个WebService服务之前,要知道该服务的WSDL文件的地址。 WebService服务提供商可以通过两种方式来暴露它的WSDL文件地址:1.注册到UDDI服务器,以便被人查找;2.直接告诉给客户端调用者。

接下来,我们来看看 Netty 提供的ChannelHandler ,你可以用它来处理 HTTP 和 HTTPS协议,而不必编写自定义的编解码器。

Http解码器、编码器和编解码器

HTTP 是基于请求/响应模式的:客户端向服务器发送一个 HTTP 请求,然后服务器将会返回 一个 HTTP 响应。Netty 提供了多种编码器和解码器以简化对这个协议的使用。图 11-2 和图 11-3 分别展示了生产和消费 HTTP 请求和 HTTP 响应的方法。

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

如图 11-2 和图 11-3 所示,一个 HTTP 请求/响应可能由多个数据部分组成,并且它总是以一 个 LastHttpContent 部分作为结束。 FullHttpRequest 和 FullHttpResponse 消息是特 殊的子类型,分别代表了完整的请求和响应。所有类型的 HTTP 消息( FullHttpRequest 、 LastHttpContent 以及代码清单 11-2 中展示的那些)都实现了 HttpObject 接口。

表 11-2 概要地介绍了处理和生成这些消息的 HTTP 解码器和编码器。

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

Encoder对应出站,Decoder对应入站

代码清单 11-2 中的 HttpPipelineInitializer 类展示了将 HTTP 支持添加到你的应用 程序是多么简单—几乎只需要将正确的 ChannelHandler 添加到 ChannelPipeline 中。

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

聚合HTTP消息

在 ChannelInitializer 将 ChannelHandler 安装到 ChannelPipeline 中之后,你 便可以处理不同类型的 HttpObject 消息了。但是由于 HTTP 的请求和响应可能由许多部分组 成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty 提供了一个聚合器它可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息。通过 这样的方式,你将总是看到完整的消息内容。

由于消息分段需要被缓冲, 直到可以转发一个完整的消息给下一个 ChannelInboundHandler ,所以这个操作有轻微的开销。其所带来的好处便是你不必关心消息碎片了。

引入这种自动聚合机制只不过是向 ChannelPipeline中添加另外一个 ChannelHandler 罢了。代码清单 11-3 展示了如何做到这一点。

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

HTTP压缩

当使用 HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一 些 CPU 时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。

Netty 为压缩和解压缩提供了 ChannelHandler 实现,它们同时支持 gzip 和 deflate 编码。

HTTP 请求的头部信息

客户端可以通过提供以下头部信息来指示服务器它所支持的压缩格式:

GET /encrypted-area HTTP/1.1

Host: www.example.com

Accept-Encoding: gzip, deflate

然而,需要注意的是,服务器没有义务压缩它所发送的数据。

代码清单 11-4 展示了一个例子。

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

压缩及其依赖

如果你正在使用的是 JDK 6 或者更早的版本,那么你需要将 JZlib(www.jcraft.com/jzlib/)添加到 CLASSPATH 中以支持压缩功能。

对于 Maven,请添加以下依赖项:

1
2
3
4
5
       <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jzlib</artifactId>
            <version>1.1.3</version>
        </dependency>

使用HTTPS

代码清单 11-5 显示,启用 HTTPS 只需要将SslHandler 添加到 ChannelPipeline 的ChannelHandler 组合中。

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

前面的代码是一个很好的例子,说明了 Netty 的架构方式是如何将代码重用变为杠杆作用的。 只需要简单地将一个 ChannelHandler 添加到 ChannelPipeline 中,便可以提供一项新功 能,甚至像加密这样重要的功能都能提供。

WebSocket

Netty 针对基于 HTTP 的应用程序的广泛工具包中包括了对它的一些最先进的特性的支持。在这一节中,我们将探讨 WebSocket ——一种在 2011 年被互联网工程任务组(IETF)标准化的 协议。

WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的 交互序列,那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。还有其他的一些或多或少的取巧方式 ,但是最终它们仍然属于扩展性受限的变通之法。

WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。 简单地说, WebSocket提供了“在一个单个的TCP连接上提供双向的通信……结合WebSocket API……它为网页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。

也就是说,WebSocket 在客户端和服务器之间提供了真正的双向数据交换。我们不会深入地 描述太多的内部细节,但是我们还是应该提到,尽管最早的实现仅限于文本数据,但是现在已经 不是问题了;WebSocket 现在可以用于传输任意类型的数据,很像普通的套接字。

图 11-4 给出了 WebSocket 协议的一般概念。在这个场景下,

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

通信将作为普通的 HTTP 协议 开始,随后升级到双向的 WebSocket 协议。

要想向你的应用程序中添加对于 WebSocket 的支持, 你需要将适当的客户端或者服务器 WebSocket ChannelHandler 添加到 ChannelPipeline 中。这个类将处理由 WebSocket 定义的称为帧的特殊消息类型。如表11-3所示,WebSocketFrame可以被归类为数据帧或者控制帧。

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

因为Netty主要是一种服务器端的工作,所以在这里我们重点创建WebSocket服务器 。代码 清单 11-6 展示了一个使用 WebSocketServerProtocolHandler 的简单示例,这个类处理协 议升级握手,以及 3 种控制帧—— Close 、 Ping 和 Pong 。 Text 和 Binary 数据帧将会被传递给 下一个(由你实现的) ChannelHandler 进行处理。

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

保护WebSocket

要想为 WebSocket 添加安全性,只需要将 SslHandler 作为第一个 ChannelHandler 添加到 ChannelPipeline 中。

更加全面的示例参见第 12 章,那一章会深入探讨实时 WebSocket 应用程序的设计。

空闲的连接和超时

到目前为止, 我们的讨论都集中在 Netty 通过专门的编解码器和处理器对 HTTP 的变型 HTTPS 和 WebSocket 的支持上。只要你有效地管理你的网络资源,这些工作就可以使得你的应 用程序更加高效、易用和安全。所以,让我们一起来探讨下首先需要关注的——连接管理吧。

检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务, Netty 特地为它提供了几个 ChannelHandler 实现。表 11-4 给出了它们的概述。

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

让我们仔细看看在实践中使用得最多的 IdleStateHandler 吧。代码清单 11-7 展示了当 使用通常的发送心跳消息到远程节点的方法时,如果在 60 秒之内没有接收或者发送任何的数据, 我们将如何得到通知;如果没有响应,则连接会被关闭。

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

这个示例演示了如何使用 IdleStateHandler 来测试远程节点是否仍然还活着,并且在它失活时通过关闭连接来释放资源。

如果连接超过 60 秒没有接收或者发送任何的数据,那么 IdleStateHandler 将会使用一个 IdleStateEvent 事件来调用 fireUserEventTriggered() 方法。 HeartbeatHandler 实现 了 userEventTriggered() 方法,如果这个方法检测到 IdleStateEvent 事件,它将会发送心 跳消息,并且添加一个将在发送操作失败时关闭该连接的 ChannelFutureListener 。

解码基于分隔符的协议和基于长度的协议

在使用 Netty 的过程中,你将会遇到需要解码器的基于分隔符和帧长度的协议。下一节将解释 Netty 所提供的用于处理这些场景的实现。

基于分隔符的协议

基于分隔符的(delimited)消息协议使用定义的字符来标记消息或者消息段(通常被称 为帧)的开头或者结尾。由RFC文档正式定义的许多协议(如SMTP、POP3、IMAP以及Telnet ) 都是这样的。此外,当然,私有组织通常也拥有他们自己的专有格式。无论你使用什么样的协 议,表 11-5 中列出的解码器都能帮助你定义可以提取由任意标记(token)序列分隔的帧的自 定义解码器。

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

图 11-5 展示了当帧由行尾序列\r\n(回车符+换行符)分隔时是如何被处理的。

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

代码清单 11-8 展示了如何使用 LineBasedFrameDecoder 来处理图 11-5 所示的场景。

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

如果你正在使用除了行尾符之外的分隔符分隔的帧,那么你可以以类似的方式使用 DelimiterBasedFrameDecoder ,只需要将特定的分隔符序列指定到其构造函数即可。

这些解码器是实现你自己的基于分隔符的协议的工具。作为示例,我们将使用下面的协议 规范:

  • 传入数据流是一系列的帧,每个帧都由换行符(\n)分隔;
  • 每个帧都由一系列的元素组成,每个元素都由单个空格字符分隔;
  • 一个帧的内容代表一个命令,定义为一个命令名称后跟着数目可变的参数。

我们用于这个协议的自定义解码器将定义以下类:

  • Cmd —将帧(命令)的内容存储在 ByteBuf 中,一个 ByteBuf 用于名称,另一个用于参数;
  • CmdDecoder —从被重写了的 decode() 方法中获取一行字符串,并从它的内容构建 一个 Cmd 的实例;
  • CmdHandler —从 CmdDecoder 获取解码的 Cmd 对象,并对它进行一些处理;
  • CmdHandlerInitializer——为了简便起见,我们将会把前面的这些类定义为专门 的 ChannelInitializer 的嵌套类,其将会把这些 ChannelInboundHandler 安装 到 ChannelPipeline 中。

正如将在代码清单 11-9 中所能看到的那样,这个解码器的关键是扩展 LineBasedFrameDecoder 。

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

基于长度的协议

基于长度的协议通过将它的长度编码到帧的头部来定义帧,而不是使用特殊的分隔符来标记 它的结束。 表 11-6 列出了Netty提供的用于处理这种类型的协议的两种解码器。

对于固定帧大小的协议来说,不需要将帧长度编码到头部。

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

图 11-6 展示了FixedLengthFrameDecoder的功能,其在构造时已经指定了帧长度为 8 字节。

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

你将经常会遇到被编码到消息头部的帧大小不是固定值的协议。为了处理这种变长帧,你可 以使用 LengthFieldBasedFrameDecoder ,它将从头部字段确定帧长,然后从数据流中提取 指定的字节数。

图 11-7 展示了一个示例,其中长度字段在帧中的偏移量为 0,并且长度为 2 字节。

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

LengthFieldBasedFrameDecoder 提供了几个构造函数来支持各种各样的头部配置情 lengthField- 况。代码清单 11-10 展示了如何使用其 3 个构造参数分别为 maxFrameLength 、 Offset 和 lengthFieldLength 的构造函数。在这个场景中,帧的长度被编码到了帧起始的前 8 个字节中。

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

你现在已经看到了 Netty 提供的,用于支持那些通过指定协议帧的分隔符或者长度(固定的 或者可变的)以定义字节流的结构的协议的编解码器。你将会发现这些编解码器的许多用途,因 为许多的常见协议都落到了这些分类之一中。

写大型数据

因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。由于写 操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture 。当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险。所以在写大型数据 时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。让 我们考虑下将一个文件内容写出到网络的情况。

在我们讨论传输(见 4.2 节)的过程中,提到了 NIO 的零拷贝特性,这种特性消除了将文件的内容从文件系统移动到网络栈的复制过程。所有的这一切都发生在 Netty 的核心中,所以应用程序所有需要做的就是使用一个 FileRegion 接口的实现,其在 Netty 的 API 文档中的定义是: “通过支持零拷贝的文件传输的 Channel 来发送的文件区域。”

代码清单 11-11 展示了如何通过从 FileInputStream 创建一个 DefaultFileRegion ,并 将其写入 Channel ,从而利用零拷贝特性来传输一个文件的内容。

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

这个示例只适用于文件内容的直接传输,不包括应用程序对数据的任何处理在需要将数据从文件系统复制到用户内存中时,可以使用 ChunkedWriteHandler ,它支持异步写大型数据流,而又不会导致大量的内存消耗。

关键是 interface ChunkedInput<B> ,其中类型参数 B 是 readChunk() 方法返回的 类型。Netty 预置了该接口的 4 个实现,如表 11-7 中所列出的。每个都代表了一个将由 ChunkedWriteHandler 处理的不定长度的数据流。

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

代码清单 11-12 说明了 ChunkedStream 的用法,它是实践中最常用的实现。所示的类使用 了一个 File 以及一个 SslContext 进行实例化。当 initChannel() 方法被调用时,它将使用所示的 ChannelHandler 链初始化该 Channel 。

当 Channel 的状态变为活动的时, WriteStreamHandler 将会逐块地把来自文件中的数据作为 ChunkedStream 写入。数据在传输之前将会由 SslHandler 加密。

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

逐块输入

要使用你自己的 ChunkedInput实现, 请在ChannelPipeline中安装一个ChunkedWriteHandler 。

在本节中, 我们讨论了如何通过使用零拷贝特性来高效地传输文件, 以及如何通过使用 ChunkedWriteHandler 来写大型数据而又不必冒着导致 OutOfMemoryError 的风险。在下一节中,我们将仔细研究几种序列化 POJO 的方法。

序列化数据

JDK 提供了ObjectOutputStream和ObjectInputStream,用于通过网络对 POJO 的 基本数据类型和图进行序列化和反序列化。 该 API 并不复杂, 而且可以被应用于任何实现了 java.io.Serializable 接口的对象。但是它的性能也不是非常高效的。在这一节中,我们 将看到 Netty 必须为此提供什么。

JDK序列化

如果你的应用程序必须要和使用了 ObjectOutputStream 和 ObjectInputStream 的远 程节点交互,并且兼容性也是你最关心的,那么JDK序列化将是正确的选择 。表 11-8 中列出了 Netty提供的用于和JDK进行互操作的序列化类。

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

使用JBoss Marshalling

如果你可以自由地使用外部依赖,那么JBoss Marshalling将是个理想的选择:它比JDK序列 化最多快 3 倍,而且也更加紧凑。在JBoss Marshalling官方网站主页 上的概述中对它是这么定 义的:

JBoss Marshalling 是一种可选的序列化 API,它修复了在 JDK 序列化 API 中所发现 的许多问题,同时保留了与 java.io.Serializable 及其相关类的兼容性,并添加 了几个新的可调优参数以及额外的特性,所有的这些都是可以通过工厂配置(如外部序 列化器、类/实例查找表、类解析以及对象替换等)实现可插拔的。

Netty 通过表 11-9 所示的两组解码器/编码器对为 Boss Marshalling 提供了支持。第一组兼容 只使用 JDK 序列化的远程节点。第二组提供了最大的性能,适用于和使用 JBoss Marshalling 的 远程节点一起使用。

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

代码清单 11-13 展示了如何使用 MarshallingDecoder 样,几乎只是适当地配置 ChannelPipeline 罢了。

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

通过Protocol Buffers

Netty序列化的最后一个解决方案是利用Protocol Buffers 的编解码器,它是一种由Google公司开发的、现在已经开源的数据交换格式。可以在https://github.com/google/protobuf找到源代码。

Protocol Buffers 以一种紧凑而高效的方式对结构化的数据进行编码以及解码。它具有许多的 编程语言绑定,使得它很适合跨语言的项目。表 11-10 展示了 Netty 为支持 protobuf 所提供的 ChannelHandler 实现。

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

在这里我们又看到了,使用 protobuf 只不过是将正确的 ChannelHandler 添加到 ChannelPipeline 中,如代码清单 11-14 所示。

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

在这一节中,我们探讨了由 Netty 专门的解码器和编码器所支持的不同的序列化选项:标准 JDK 序列化、JBoss Marshalling 以及 Google 的 Protocol Buffers。

小结

Netty 提供的编解码器以及各种 ChannelHandler 可以被组合和扩展,以实现非常广泛的处 理方案。此外,它们也是被论证的、健壮的组件,已经被许多的大型系统所使用。

需要注意的是,我们只涵盖了最常见的示例;Netty 的 API 文档提供了更加全面的覆盖。

在下一章中,我们将学习另一种先进的协议——WebSocket,它被开发用以改进 Web 应用程序的性能以及响应性。Netty 提供了你将会需要的工具,以便你快速、轻松地利用它强大的功能。