Chapter 2、结构概览
这一节我们将确认 Netty 提供的核心功能是什么,以及它们怎么构成一个完整的网络应用开发堆栈。
1、丰富的缓存数据结构
Netty 使用它自己的缓存 API 来表示字节序列而不是 NIO 的 ByteBuffer。Netty 的新缓存类——ChannelBuffer,彻底解决了 ByteBuffer 的问题,满足了网络应用开发者的日常需求。这里列举几个很酷的特点:
- 需要的话你可以自定义缓存类
- 通过一个内置的组合缓存类实现零拷贝组合
- 提供开箱即用的动态缓存,自动扩容,就像 StringBuffer 一样
- 不再需要调用 flip 方法
- 通常比 ByteBuffer 要快
组合和切分 ChannelBuffer
当在通信层传输数据时,数据经常需要被组合或者切分。比如一次装载被分成多个包,但是经常需要组合起来解码。
传统方式来说,多个包的数据通过复制到一个新的字节缓存而组合起来。
Netty 支持零复制实现,通过让一个新的 ChannelBuffer“指向”所有要求组合的缓存,从而避免了复制操作。
2、统一异步 I /O API
传统 Java 的 I /O API 对不同的传输类型提供不同的类和方法。举个例子,java.net.Socket 和 java.net.DatagramSocket 没有公共父类因此它们在执行 socket I/ O 的方式完全不一样。
这种不匹配的结果就是使得应用程序从一种传输方式移植成另一种变得十分困难。也就是说你需要支持额外的传输方式因为应用程序的网络层经常涉及到重写。逻辑上,许多协议可以在不只一种传输方式,比如 TCP/IP、UDO/IP、SCTP 和串行通信上运行。
更糟的是,Java 的新 I /O(NIO)API 继承了老阻塞 I /O(OIO)API 的不兼容性,并且在下一个版本 NIO.2(AIO)继续这样做。因为这些 API 在设计和表现特点上完全不同,你经常被迫在应用程序开始实现之前就需要确定好依赖那种 API。
举个例子,你可能想用 OIO 方式开始因为你服务的客户数量很小且用 OIO 写一个 socket 服务比用 NIO 要简单得多。但是,当你的业务呈指数级上升然后你需要同时服务成千上万的客户时,麻烦就大了。你也可以用 NIO 方式开始,但是这可能会阻碍开发速度,毕竟 NIO 的选择器 API 很复杂。
Netty 拥有统一的异步 I / O 接口——Channel,它抽象了点对点通信要求的所有操作。只要你用一种 Netty 传输写的应用程序,可以在其他 Netty 传输上运行。Nett 通过统一的 API 提供了必要的传输方式:
- 基于 NIO 的 TCP/IP 传输(见 org.jboss.netty.channel.socket.nio)
- 基于 OIO 的 TCP/IP 传输(见 org.jboss.netty.channel.socket.oio)
- 基于 OIO 的 UDP/IP 传输
- 本地传输(见 org.jboss.netty.channel.local)
要从一种传输方式切换到另一种,只需要改动几行代码,比如选择另一个 ChannelFactory 实现。
你甚至可以使用还没发布的新传输方式,比如串行通信传输,同样替换几行构造器调用的代码。再者,你也可以通过继承核心 API 实现自己的传输方式。
3、基于过滤链模式的事件模型
一个定义良好的并可扩展性高的事件模型是一个事件驱动应用程序的前提。Netty 针对 I / O 有一个定义良好的事件模型。你可以在不破坏已有代码的情况下实现你自己的事件类,因为每一个事件类都根据一个严格的类层次体系区别于其他事件类。这是另一个和其他框架的不同点。很多 NIO 框架都没有或者很有限的事件模型概念。如果它们提供了扩展,就会经常在尝试增加自定义事件类时破坏已有代码。
一个 ChannelEvent 由一个 ChannelPipeline 里的一列 ChannelHandler 来处理。这个管道实现了一个过滤链模式的高级形式,让使用者完全控制一个事件如何被处理以及管道内的 handler 如何相互联系。举个例子,你可以规定数据从 socket 读取时做点什么:
public class MyReadHandler implements SimpleChannelHandler {public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) {Object message = evt.getMessage();
// Do something with the received message.
...
// And forward the event to the next handler.
ctx.sendUpstream(evt);
}
}
你也可以规定一个 handler 在接收一个写请求时做点什么:
public class MyWriteHandler implements SimpleChannelHandler {public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) {Object message = evt.getMessage();
// Do something with the message to be written.
...
// And forward the event to the next handler.
ctx.sendDownstream(evt);
}
}
4、支持更快速开发的高级组件
4.1、编解码框架
之前“用 POJO 替代 ChannelBuffer”那一节证实了,把协议编解码从业务逻辑中分离出来通常是一个好主意。但是从零开始实现有一些困难。你需要处理信息分片。有一些协议是多层的(建立在其他低级协议基础上)。还有一些协议在单机系统上实现特别困难。
因此,一个好的网络应用框架需要提供一个可扩展的、可重用的、可单元测试的和多层级的编解码器框架,产生可维护的用户编解码器。
Netty 提供了一系列基础和高级的编解码器,可以解决你在写一个协议编解码器是遭遇的大多数问题,无论编解码器是否简单,是二进制的还是文本的。
4.2、SSL/TLS 支持
不像阻塞 I /O,在 NIO 支持 SSL 是个值得正视对任务。你不能简单得包装一个流对数据进行加密解密,你必须要用到 javax.net.ssl.SSLEngine。SSLEngine 是一个跟 SSL 本身一样复杂的状态机。你需要处理所有可能的状态,如密码套件、加密密钥协商、证书交换以及验证。此外,SSL 甚至不是完全线程安全的。
在 Netty,SslHandler 负责对外屏蔽 SSLEngine 所有细枝末节,你只需要配置 SslHandler 然后添加到你的 ChannelPipeline 中。SslHandler 同时让你可以很容易实现类似 StartTLS 的高级特性。
4.3、HTTP 实现
HTTP 绝对是互联网最流行的协议。现在已经有一系列 HTTP 实现,比如 Servlet 容器。那么为什么 Netty 还要自己实现 HTTP 呢?
Netty 的 HTTP 支持和现有的 HTTP 库很不一样。它让你可以完全控制 HTTP 在底层的消息交换。因为从根本上来说,它就是一个 HTTP 编解码器和 HTTP 消息类的结合,没有类似强制线程模型这样的限制。也就是说,你可以按照你希望的工作方式来实现 HTTP 客户端和服务端。你拥有所有 HTTP 规范内的控制权,包括线程模型、连接生命周期和分块编码。
基于它的高可定制性,你可以写一个高效的 HTTP 服务:
- 要求持续连接和服务推送技术的聊天服务(如 Comet);
- 需要保持连接直到整个媒体文件流传输完毕的媒体流服务(如两小时的电影);
- 无内存压力上传大文件的文件服务(如上传 1G 文件);
- 可以异步连接成千上万的第三方 web 服务的可伸缩的混合客户端
4.4、WebSocket 实现
WebSocket 在单 TCP socket 上实现一个双向全双工通信通道,用于 web 浏览器和 web 服务端端数据交互。
WebSocket 协议由 IETF 标准化为 RFC 6455。
Netty 实现了 RFC 6455 和一系列旧版本规范。
4.4、Google 协议缓存整合
Google Protocal Buffers 是一个高效二进制协议的快速实现。凭借 ProtobufEncoder 和 ProtobufDecoder,你可以将 Goodgle Protocal Buffers 编译程序生成的信息类转化为 Netty 编解码器。
5、总结
这一节,我们从特点出发回顾了 Netty 的总体架构。Netty 拥有一个简单且至今仍强大的架构。它由三个组件组成——buffer,channel 和事件模型,所有高级特性都是在这三个组件的基础上建立的。只要你理解了这三个组件是如何一起工作的,你就不难理解这节简要提及的那些高级特性。