預置的ChannelHandler和編解碼器
Netty 爲許多通用協議提供了編解碼器和處理器,幾乎可以開箱即用,這減少了你在那些相 當繁瑣的事務上本來會花費的時間與精力。在本章中,我們將探討這些工具以及它們所帶來的好 處,其中包括 Netty 對於 SSL/TLS 和 WebSocket 的支持,以及如何簡單地通過數據壓縮來壓榨 HTTP,以獲取更好的性能。
通過SSL/TLS保護Netty應用程序
爲了支持 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 和數據流都是一致的。
構建基於Netty的Http/Https應用程序
HTTP/HTTPS 是最常見的協議套件之一,並且隨着智能手機的成功,它的應用也日益廣泛, 因爲對於任何公司來說,擁有一個可以被移動設備訪問的網站幾乎是必須的。這些協議也被用於 其他方面。許多組織導出的用於和他們的商業合作伙伴通信的 WebService API 一般也是基於 HTTP(S)的。
HTTP解碼器、編碼器和編解碼器
HTTP 是基於請求/響應模式的:客戶端向服務器發送一個 HTTP 請求,然後服務器將會返回 一個 HTTP 響應。Netty 提供了多種編碼器和解碼器以簡化對這個協議的使用。
HTTP請求:
HTTP響應:
一個 HTTP 請求/響應可能由多個數據部分組成,並且它總是以一 個 LastHttpContent 部分作爲結束。FullHttpRequest 和 FullHttpResponse 消息是特 殊的子類型,分別代表了完整的請求和響應。所有類型的 HTTP 消息都實現了 HttpObject 接口。
HTTP解碼器和編碼器:
示例:添加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());
}
}
}
聚合HTTP消息
在 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壓縮
當使用 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
啓用 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
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 中。