Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相 当繁琐的事务上本来会花费的时间与精力。在本章中,我们将探讨这些工具以及它们所带来的好 处,其中包括 Netty 对于 SSL/TLS 和 WebSocket 的支持,以及如何简单地通过数据压缩来压榨 HTTP,以获取更好的性能。
为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 类使得实现解密和加密相当简单直接。Netty 通过一个名为 SslHandler 的 ChannelHandler 实现利用了这个 API,其中 SslHandler 在内部使用 SSLEngine 来完成实际的工作。
示例:
使用 ChannelInitializer 来将 SslHandler 添加到 Channel- Pipeline 中。
public class SslChannelInitializer extends ChannelInitializer<Channel> {
private final SslContext context;
private final boolean startTls;
public SslChannelInitializer(SslContext context, boolean startTls) {
//传入要使用的SslContext
this.context = context;
//如果设置为true,第一个写入的消息不会被加密(客户端应该设置为true)
this.startTls = startTls;
}
@Override
protected void initChannel(Channel ch) throws Exception {
//对于每个SslHandler实例,都使用Channel的ByteBufAllocator从SslContext获取一个新的SSLEngine
SSLEngine engine = context.newEngine(ch.alloc());
//将SslHandler作为第一个ChannelHandler添加到ChannelPipeline
ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));
}
}
在大多数情况下,SslHandler将是ChannelPipeline中的第一个ChannelHandler。 这确保了只有在所有其他的 ChannelHandler 将它们的逻辑应用到数据之后,才会进行加密。
SslHandler 具有一些有用的方法,如下表所示。例如,在握手阶段,两个节点将相互 验证并且商定一种加密方式。你可以通过配置 SslHandler 来修改它的行为,或者在 SSL/TLS 握手一旦完成之后提供通知,握手阶段完成之后,所有的数据都将会被加密。SSL/TLS 握手将会 被自动执行。
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 和数据流都是一致的。
HTTP/HTTPS 是最常见的协议套件之一,并且随着智能手机的成功,它的应用也日益广泛, 因为对于任何公司来说,拥有一个可以被移动设备访问的网站几乎是必须的。这些协议也被用于 其他方面。许多组织导出的用于和他们的商业合作伙伴通信的 WebService API 一般也是基于 HTTP(S)的。
HTTP 是基于请求/响应模式的:客户端向服务器发送一个 HTTP 请求,然后服务器将会返回 一个 HTTP 响应。Netty 提供了多种编码器和解码器以简化对这个协议的使用。
一个 HTTP 请求/响应可能由多个数据部分组成,并且它总是以一 个 LastHttpContent 部分作为结束。FullHttpRequest 和 FullHttpResponse 消息是特 殊的子类型,分别代表了完整的请求和响应。所有类型的 HTTP 消息都实现了 HttpObject 接口。
示例:添加HTTP支持
public class HttpPipelineInitailizer extends ChannelInitializer<Channel> {
private final boolean client;
public HttpPipelineInitailizer(boolean client) {
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
//如果是客户端,则添加HttpResponseDecoder以处理来自服务器的响应
pipeline.addLast("decoder", new HttpResponseDecoder());
//如果是客户端,则添加 HttpRequestEncoder 以向服务器发送请求
pipeline.addLast("encoder", new HttpRequestEncoder());
} else {
//如果是服务器,则添加 HttpRequestDecoder 以接收来自客户端的请求
pipeline.addLast("decoder", new HttpResponseDecoder());
//如果是服务器,则添加 HttpResponseEncoder 以向客户端发送响应
pipeline.addLast("encoder", new HttpResponseEncoder());
}
}
}
在 ChannelInitializer 将 ChannelHandler 安装到 ChannelPipeline 中之后,你 便可以处理不同类型的 HttpObject 消息了。但是由于 HTTP 的请求和响应可能由许多部分组 成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty 提供了一个聚合 器,它可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息
。通过 这样的方式,你将总是看到完整的消息内容。
由于消息分段需要被缓冲,直到可以转发一个完整的消息给下一个 ChannelInboundHandler,所以这个操作有轻微的开销。其所带来的好处便是你不必关心消息碎片了。
引入这种自动聚合机制只不过是向 ChannelPipeline 中添加另外一个 ChannelHandler 罢了。
//自动聚合HTTP的消息片段
public class HttpAggregatorInitalizer extends ChannelInitializer<Channel> {
private final boolean isClient;
public HttpAggregatorInitalizer(boolean isClient) {
this.isClient = isClient;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//如果是客户端,则添加HttpClientCodec
if (isClient) {
pipeline.addLast("codec", new HttpClientCodec());
} else {
//如果是服务器,则添加HttpServerCodec
pipeline.addLast("codec", new HttpServerCodec());
}
//将最大的消息大小为512KB的HttpObjectAggregator添加到ChannelPipeline
pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
}
}
当使用 HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一 些 CPU 时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。
Netty 为压缩和解压缩提供了 ChannelHandler 实现,它们同时支持 gzip 和 deflate 编 码。
HTTP 请求的头部信息
客户端可以通过提供以下头部信息来指示服务器它所支持的压缩格式:
GET /encrypted-area HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip, deflate
然而,需要注意的是,服务器没有义务压缩它所发送的数据。
//自动压缩HTTP消息
public class HttpCompressionInitializer extends ChannelInitializer<Channel> {
private final boolean isClient;
public HttpCompressionInitializer(boolean isClient) {
this.isClient = isClient;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (isClient) {
//如果是客户端,则添加HttpClientCodec
pipeline.addLast("codec", new HttpClientCodec());
//如果是客户端,则添加HttpContentDecompressor以处理来自服务器的压缩内容
pipeline.addLast("decompressor", new HttpContentCompressor());
} else {
// 如果是服务器,则添 加 HttpServerCodec
pipeline.addLast("codec", new HttpServerCodec());
// 如果是服务器,则添加 HttpContentCompressor 来压缩数据(如果客户端支持它)
pipeline.addLast("compressor", new HttpContentCompressor());
}
}
}
如果你正在使用的是 JDK 6 或者更早的版本,那么你需要将 JZlib(www.jcraft.com/jzlib/)添加到 CLASSPATH 中以支持压缩功能。
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jzlib</artifactId>
<version>1.1.3</version>
</dependency>
启用 HTTPS 只需要将 SslHandler 添加到 ChannelPipeline 的 ChannelHandler 组合中:
public class HttpsCodecInitializer extends ChannelInitializer<Channel> {
private final SslContext context;
private final boolean isClient;
public HttpsCodecInitializer(SslContext context, boolean isClient) {
this.context = context;
this.isClient = isClient;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
SSLEngine engine = context.newEngine(ch.alloc());
//将 SslHandler 添加到 ChannelPipeline 中以 使用 HTTPS
pipeline.addFirst("ssl", new SslHandler(engine));
if (isClient) {
pipeline.addLast("codec", new HttpClientCodec());
} else {
pipeline.addLast("codec", new HttpServerCodec());
}
}
}
WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的 交互序列,那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。还有其他的一些或多或少的取巧方式 ,但是最终它们仍然属于扩展性受限的变通之法。 WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。
简单地说,WebSocket提供了“在一个单个的TCP连接上提供双向的通信......结合WebSocket API......它为网 页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。
”
也就是说,WebSocket 在客户端和服务器之间提供了真正的双向数据交换。我们不会深入地 描述太多的内部细节,但是我们还是应该提到,尽管最早的实现仅限于文本数据,但是现在已经 不是问题了;WebSocket 现在可以用于传输任意类型的数据,很像普通的套接字。
通信将作为普通的 HTTP 协议 开始,随后升级到双向的 WebSocket 协议:
要想向你的应用程序中添加对于 WebSocket 的支持,你需要将适当的客户端或者服务器 WebSocketChannelHandler
添加到 ChannelPipeline 中。这个类将处理由 WebSocket 定义 的称为帧的特殊消息类型。
如下表所示,WebSocketFrame 可以被归类为数据帧或者控制帧。
示例:
因为Netty主要是一种服务器端的技术,所以在这里我们重点创建WebSocket服务器 。
下面代码展示了一个使用WebSocketServerProtocolHandler的简单示例,这个类处理协议升级握手,以及 3 种控制帧——Close、Ping和Pong。Text和Binary数据帧将会被传递给 下一个(由你实现的)ChannelHandler进行处理。
public class WebSocketServerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(
new HttpServerCodec(),
// 为握手提供聚合的HttpRequest
new HttpObjectAggregator(65535),
// 如果被请求 的端点是 "/websocket", 则处理该 升级握手
new WebSocketServerProtocolHandler("/websocket"),
// TextFrameHandler 处理 TextWebSocketFrame
new TextFrameHandler(),
new BinaryFrameHandler(),
// ContinuationFrameHandler 处理 ContinuationWebSocketFrame
new ContinuationFrameHandler());
}
public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//Handler text frame
}
}
public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
//Handle binary frame
}
}
public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
//Handle continuation frame
}
}
}
要想为 WebSocket 添加安全性,只需要将 SslHandler 作为第一个 ChannelHandler 添加到
ChannelPipeline 中。