构建基于Netty 的HTTP/HTTPS 应用程序
HTTP/HTTPS是最常见的协议套件之一,并且随着智能手机的成功,它的应用也日益广泛,因为对于任何公司来说,拥有一个可以被移动设备访问的网站几乎是必须的。这些协议也被用于其他方面。许多组织导出的用于和他们的商业合作伙伴通信的WebService API 一般也是基于HTTP(S)的。
接下来,我们来看看Netty提供的ChannelHandler,你可以用它来处理HTTP 和HTTPS协议,而不必编写自定义的编解码器。
Netty的HTTP解码器、编码器和编解码器
HTTP是基于请求/响应模式的:客户端向服务器发送一个HTTP请求,然后服务器将会返回一个HTTP响应。Netty提供了多种编码器和解码器以简化对这个协议的使用。下面两图分别展示了生产和消费HTTP 请求和HTTP 响应的方法。
HTTP 请求的组成部分
HTTP 响应的组成部分
HTTP 解码器和编码器
HttpRequestEncoder 将HttpRequest、HttpContent 和LastHttpContent 消息编码为字节
HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和LastHttpContent 消息
HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息
添加HTTP 支持
public class HttpPipelineInitializer extends ChannelInitializer<Channel> { private final boolean client; public HttpPipelineInitializer(boolean client) { this.client = client; } @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if (client) { pipeline.addLast("decoder", new HttpResponseDecoder());// 如果是客户端,则添加HttpResponseDecoder以处理来自服务器的响应 pipeline.addLast("encoder", new HttpRequestEncoder());// 如果是客户端,则添加HttpRequestEncoder以向服务器发送请求 } else { pipeline.addLast("decoder", new HttpRequestDecoder());// 如果是服务器,则添加HttpRequestDecoder以接收来自客户端的请求 pipeline.addLast("encoder", new HttpResponseEncoder());// 如果是服务器,则添加HttpResponseEncoder以向客户端发送响应 } } }
聚合HTTP 消息
在ChannelInitializer将ChannelHandler安装到ChannelPipeline中之后,你便可以处理不同类型的HttpObject消息了。但是由于HTTP的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty提供了一个聚合器,它可以将多个消息部分合并为FullHttpRequest或者FullHttpResponse 消息。通过这样的方式,你将总是看到完整的消息内容。
由于消息分段需要被缓冲,直到可以转发一个完整的消息给下一个ChannelInboundHandler,所以这个操作有轻微的开销。其所带来的好处便是你不必关心消息碎片了。
引入这种自动聚合机制只不过是向ChannelPipeline 中添加另外一个ChannelHandler罢了。下面代码展示了如何做到这点。
public class HttpAggregatorInitializer extends ChannelInitializer<Channel> { private final boolean isClient; public HttpAggregatorInitializer(boolean isClient) { this.isClient = isClient; } @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if (isClient) { pipeline.addLast("codec", new HttpClientCodec());// 如果是客户端,则添加HttpClientCodec } else { pipeline.addLast("codec", new HttpServerCodec());// 如果是服务器,则添加HttpServerCodec } pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));// 将最大的消息大小为512KB的HttpObjectAggregator添加到ChannelPipeline } }
HTTP 压缩
当使用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) { pipeline.addLast("codec", new HttpClientCodec());// 如果是客户端,则添加HttpClientCodec pipeline.addLast("decompressor", new HttpContentDecompressor());// 如果是客户端,则添 加HttpContentDecompressor以处理来自服务器的压缩内容 } else { pipeline.addLast("codec", new HttpServerCodec());// 如果是服务器,则添加HttpServerCodec pipeline.addLast("compressor", new HttpContentCompressor());// 如果是服务器,则添加HttpContentCompressor来压缩数据(如果客户端支持它) } } }
版本注意点
如果你正在使用的是JDK 6 或者更早的版本,那么你需要将JZlib(www.jcraft.com/jzlib/)添加到CLASSPATH 中以支持压缩功能。
对于Maven,请添加以下依赖项:
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jzlib</artifactId>
<version>1.1.3</version>
</depe ndency>
Netty中使用HTTPS
启用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()); pipeline.addFirst("ssl", new SslHandler(engine));// 将SslHandler添加到ChannelPipeline中以使用HTTPS if (isClient) { pipeline.addLast("codec", new HttpClientCodec());// 如果是客户端,则添加HttpClientCodec } else { pipeline.addLast("codec", new HttpServerCodec());// 如果是服务器,则添加HttpServerCodec } } }
前面的代码是一个很好的例子,说明了Netty的架构方式是如何将代码重用变为杠杆作用的。只需要简单地将一个ChannelHandler添加到ChannelPipeline中,便可以提供一项新功能,甚至像加密这样重要的功能都能提供。
Netty中使用WebSocket
WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的交互序列,那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。还有其他的一些或多或少的取巧方式,但是最终它们仍然属于扩展性受限的变通之法。
WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。简单地说,WebSocket提供了“在一个单个的TCP连接上提供双向的通信……结合WebSocket API……它为网页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。”
也就是说,WebSocket在客户端和服务器之间提供了真正的双向数据交换。我们不会深入地描述太多的内部细节,但是我们还是应该提到,尽管最早的实现仅限于文本数据,但是现在已经不是问题了;WebSocket现在可以用于传输任意类型的数据,很像普通的套接字。
要想向你的应用程序中添加对于WebSocket的支持,你需要将适当的客户端或者服务器WebSocketChannelHandler添加到ChannelPipeline中。这个类将处理由WebSocket定义的称为帧的特殊消息类型。
WebSocketFrame类型
BinaryWebSocketFrame 数据帧:二进制数据
TextWebSocketFrame 数据帧:文本数据
ContinuationWebSocketFrame 数据帧:属于上一个BinaryWebSocketFrame或者TextWebSocketFrame的文本的或者二进制数据
CloseWebSocketFrame 控制帧:一个CLOSE请求、关闭的状态码以及关闭的原因
PingWebSocketFrame 控制帧:请求一个PongWebSocketFrame
PongWebSocketFrame 控制帧:对PingWebSocketFrame请求的响应
因为Netty主要是一种服务器端的技术,所以在这里我们重点创建WebSocket服务器。以下代码清单展示了一个使用WebSocketServerProtocolHandler的简单示例,这个类处理协议升级握手,以及3种控制帧——Close、Ping和Pong。Text和Binary数据帧将会被传递给下一个(由你实现的)ChannelHandler进行处理。以下展示Netty在服务器端支持WebSocket
public class WebSocketServerInitializer extends ChannelInitializer<Channel>{ @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast( new HttpServerCodec(), new HttpObjectAggregator(65536), // 为握手提供聚合的HttpRequest new WebSocketServerProtocolHandler("/websocket"),// 如果被请求的端点是"/websocket",则处理该升级握手 new TextFrameHandler(),// TextFrameHandler处理TextWebSocketFrame new BinaryFrameHandler(),// BinaryFrameHandler处理BinaryWebSocketFrame new ContinuationFrameHandler());// ContinuationFrameHandler处理ContinuationWebSocketFrame public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override public void channelRead0(ChannelHandlerContext ctx,TextWebSocketFrame msg) throws Exception { // Handle text frame } } public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> { @Override public void channelRead0(ChannelHandlerContext ctx,BinaryWebSocketFrame msg) throws Exception { // Handle binary frame } } public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> { @Override public void channelRead0(ChannelHandlerContext ctx,ContinuationWebSocketFrame msg) throws Exception { // Handle continuation frame } } } }
保护WebSocket
要想为WebSocket 添加安全性,只需要将SslHandler作为第一个ChannelHandler添加到ChannelPipeline中。
关于WebSocket的客户端示例,请参考Netty源代码中所包含的例子:https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/websocketx/client。