當前位置:
首頁 > 最新 > Netty實戰十一之預置的ChannelHandler和編解碼器

Netty實戰十一之預置的ChannelHandler和編解碼器

Netty為許多通用協議提供了編解碼器和處理器,幾乎可以開箱即用,這減少了你在那些相當繁瑣的事務上本來會花費的時間與精力。我們將探討這些工具以及它們所帶來的好處,其中包括Netty對於SSL/TLS和WebSocket的支持,以及如何簡單地通過數據壓縮來壓榨HTTP,以獲取更好的性能。

1、通過SSL/TLS保護Netty應用程序

SSL和TLS這樣的安全協議,它們層疊在其他協議之上,用以實現數據安全。我們在訪問安全網站時遇到過這些協議,但是它們也可用於其他不是基於HTTP的應用程序,如安全SMTP(SMTPS)郵件伺服器甚至是關係型資料庫系統。

下圖展示了使用SslHandler的數據流以下代碼展示了如何使用ChannelInitializer來將SslHandler添加到ChannelPipeline中。

在大多數情況下,SslHandler將是ChannelPipeline中的第一個ChannelHandler。這確保了只有在所有其他的ChannelHandler將它們的邏輯應用到數據之後,才會進行加密。

例如,在握手階段,兩個節點將相互驗證並且商定一種加密方式。你可以通過配置SslHandler來修改它的行為,或者在SSL/TLS握手一旦完成之後提供通知,握手階段完成之後,所有的數據都將會被加密。SSL/TLS握手將會被自動執行。

2、構建基於Netty的HTTP/HTTPS應用程序

HTTP/HTTPS是最常見的協議套件之一,並且隨著智能手機的成功,它的應用也日益廣泛,因為對於任何公司來說,擁有一個可以被移動設備訪問的網站幾乎是必須的。這些協議也被用於其他方面。

HTTP是基於請求/響應模式的:客戶端向伺服器發送一個HTTP請求,然後伺服器將會返回一個HTTP響應。Netty提供了多種編碼器和解碼器以簡化對這個協議的使用。

下圖分別展示了生產和消費HTTP請求和HTTP響應的方法。

正如上圖所示,一個HTTP請求/響應可能由多個數據部分組成,並且它總是以一個LastHttpContent部分作為結束。FullHttpRequest和FullHttpResponse消息是特殊的子類型,分別代表了完整的請求和響應。所有類型的HTTP消息都實現HttpObject介面。

以下代碼中的HttpPipelineInitializer類展示了將HTTP支持添加到你的應用程序時多麼簡單——幾乎只需要將正確的ChannelHandler添加到ChannelPipeline中。

3、聚合HTTP消息

在ChannelInitializer將ChannelHandler安裝到ChannelPipeline中之後,你便可以處理不同類型的HttpObject消息了。但是由於HTTP的請求和響應可能由許多部分組成,因此你需要聚合它們以形成完整的消息。為了消除這項繁瑣的任務,Netty提供了一個聚合器,它可以將多個消息部分合併為FullHttpRequest或者FullHttpResponse消息。通過這樣的方式,你將總是看到完整的消息內容。

由於消息分段需要被緩衝,直到可以轉發一個完整的消息給下一個ChannelInboundHandler,所以這個操作有輕微的開銷。其所帶來的好處便是你不必關心消息碎片了。

引入這種自動聚合機制只不過是向ChannelPipeline中添加另外一個ChannelHandler罷了。如以下代碼所示。

4、HTTP壓縮

當使用HTTP時,建議開啟壓縮功能以儘可能多地減少傳輸數據的大小。雖然壓縮會帶來一些CPU時鐘周期上的開銷,但是通常來說它都是一個好主意,特別是對於文本數據來說。

Netty為壓縮和解壓縮提供了ChannelHandler實現,它們同時支持gzip和deflate編碼。

HTTP請求的頭部信息,客戶端可以通過提供以下頭部信息來指示伺服器它所支持的壓縮格式:

Get /encrypted-area HTTP/1.1

Accept-Encoding:gzip,deflate

然而,需要注意的是,伺服器沒有義務壓縮它所發送的數據。

以下代碼展示了一個例子。

5、使用HTTPS

以下代碼顯示,啟用HTTPS只需要將SslHandler添加到ChannelPipeline的ChannelHandler組合中。

以上例子,說明了Netty的架構方式是如何將代碼重用變為槓桿作用的。只需要簡單地將一個ChannelHandler添加到ChannelPipeline中,便可以提供一項新的功能,甚至像加密這樣重要的功能都能提供。

6、WebSocket

WebSocket解決了一個長期存在的問題:既然底層的協議(HTTP)是一個請求/響應模式的交互序列,那麼如何實時地發布信息?AJAX提供了一定程度上的改善,但是數據流仍然是由客戶端所發送的請求驅動。

WebSocket規範以及它的實現代表了對一種更加有效的解決方案的嘗試。簡單地說,WebSocket提供了「在一個單個的TCP連接上提供雙向的通信·······結合WebSocketAPI·····它為網頁和遠程伺服器之間的雙向通信提供了一種替代HTTP輪詢的方案。」

WebSocket在客戶端和伺服器之間提供了真正的雙向數據交換。WebSocket現在可以用於傳輸任意類型的數據,很像普通的套接字。

下圖給出了WebSocket協議的一般概念。在這個場景下,通信將作為普通的HTTP協議開始,隨後升級到雙向的WebSocket協議。

要想向你的應用程序中添加對於WebSocket的支持,你需要將適當的客戶端或者伺服器WebSocket ChannelHandler添加到ChannelPipeline中。這個類將處理由WebSocket定義的稱為幀的特殊消息類型。

因為Netty主要是一種伺服器端的技術,所以在這裡我們重點創建WebSocket伺服器。代碼如下所示,這個類處理協議升級握手,以及3種控制幀——Close、Ping和Pong。Text和Binary數據幀將會被傳遞給下一個ChannelHandler進行處理。

保護WenSocket:要想為WebSocket添加安全性,只需要將SslHandler作為第一個ChannelHandler添加到ChannelPipeline中。

7、空閑的連接和超時

只要你有效地管理你的網路資源,這些技術就可以使得你的應用程序更加高效、易用和安全。

檢測空閑連接以及超時對於及時釋放資源來說是至關重要的。

讓我們仔細看看在實踐中使用得最多的IdleStateHandler。以下代碼展示了當使用通常的發送心跳信息到遠程節點的方法時,如果在60秒之內沒有接收或發送任何的數據,我們將如何得到通知;如果沒有響應,則連接會被關閉。

以上示例演示了如何使用IdleStateHandler來測試遠程節點是否仍然還活著,並且在它失活時通過關閉連接來釋放資源

如果連接超過60秒沒有接收或者發送任何的數據,那麼IdleStateHandler將會使用一個IdleStateEvent事件來調用fireUserEventTriggered()方法。HeartbeatHandler實現了userEventTriggered()方法,如果這個方法檢測到IdleStateEvent事件,它將會發送心跳消息,並且添加一個將在發送操作失敗時關閉該連接的ChannelFutureListener。

8、解碼基於分隔符的協議和基於長度的協議

基於分隔符的(delimited)消息協議使用定義的字元來標記的消息或消息段(通常被稱為幀)的開頭或者結尾。由RFC文檔正式定義的許多協議(如SMTP、POP3、IMAP以及Telnet)都是這樣。

下圖展示了當幀由行尾序列
分割時是如何被處理的。以下代碼展示了如何使用LineBasedFrameDecoder來處理上圖的場景。

如果你正在使用除了行尾符之外的分隔符的幀,那麼你可以以類似的方法使用DelimiterBasedFrameDecoder,只需要將特定的分隔符序列指定到其構造函數即可。

這些解碼器是實現你自己的基於分隔符的協議的工具。作為示例,我們將使用下面的協議規範:

——傳入數據流是一系列的幀,每個幀都由換行符(
)分割

——每個幀都由一系列的元素組成,每個元素都由單個空格字元分割

——一個幀的內容代表了一個命令、定義為一個命令名稱後跟著數目可變的參數

我們用於這個協議的自定義解碼器將定義以下類:

——Cmd——將幀(命令)的內容存儲在ByteBuf中,一個ByteBuf用於名稱,另一個用於參數

——CmdDecoder——從被重寫了的decode()方法中獲取一行字元串,並從它的內容構建一個Cmd的實例

——CmdHandler——從CmdDecoder獲取解碼的Cmd對象,並對它進行一些處理;

——CmdHandlerinitializer——為了簡便起見,我們將會把前面的這些類定義為專門的ChannelInitializer的嵌套類,其將會把這些ChannelInboundHandler安裝到ChannelPipeline中。

以下代碼,這個解碼器的關鍵是擴展LineBasedFrameDecoder。

9、基於長度的協議

基於長度的協議通過將它的長度編碼到幀的頭部來定義幀,而不是使用特殊的分隔符來標記它的結束。

下圖展示了FixedLengthFrameDecoder的功能,其在構造時已經指定了幀長度為8位元組。你將經常會遇到被編碼到消息頭部的幀大小不是固定值的協議。為了處理這種變長幀,你可以使用LengthFieldBasedFrameDecoder,它將從頭部欄位確定幀長,然後從數據流中提取指定的位元組數。

下圖展示了示例,其中長度欄位在幀中的偏移量為0,並且長度為2位元組。LengthFieldBasedFrameDecoder提供了幾個構造函數來支持各種各樣的頭部配置情況。以下代碼展示了如何使用其3個構造參數分別為maxFrameLength、lengthFieldOffset和lengthFieldLength的構造函數。在這個場景中,幀的長度被編碼到了幀起始的前8個位元組中。

10、寫大型數據

因為網路飽和的可能性,如何在非同步框架中高效地寫大塊的數據是一個特殊的問題。由於寫操作是非阻塞的,所以即使沒有寫出所有的數據,寫操作也會在完成時返回並通知ChannelFuture。當這種情況發生時,如果仍然不停地寫入,就有內存耗盡的風險,所以在寫大型數據時,需要準備好處理到遠程節點的連接是慢速連接的情況,這種情況會導致內存釋放的延遲。

NIO的零拷貝特性,這種特性消除了將文件的內容從文件系統移動到網路棧的複製過程。所有的一切都發生在Netty的核心中,所以應用程序所有需要做的就是使用一個FileRegion介面的實現,其在Netty的API文檔中的定義是:「通過支持零拷貝的文件傳輸的Channel來發送的文件區域。」

以下代碼展示了如何通過從FileInputStream創建一個DefaultFileRegion,並將其寫入Channel,從而利用零拷貝特性來傳輸一個文件的內容。

這個示例只適用於文件內容的直接傳輸,不包括應用程序對數據的任何處理。在需要將數據從文件系統複製到用戶內存中時,可以使用ChunkedWriteHandler,它支持非同步寫大型數據流,而又不會導致大量的內存消耗。

關鍵是interface ChunkedInput,其中類型參數B是readChunk()方法返回的類型。Netty預置了該介面的4個實現。

以下代碼說明了ChunkedStream的用法,它是實踐中最常用的實現。所示的類使用了一個File以及一個SslContext進行實例化。當initChannel()方法被調用時,它將使用所示的ChannelHandler鏈初始化該Channel。當Channel的狀態變為活動時,WriteStreamHandler將會逐塊地把來自文件中的數據作為ChunkedStream寫入。數據在傳輸之前將會由SslHandler加密。

逐塊輸入:要使用你自己的ChunkedInput實現,請在ChannelPipeline中安裝一個ChunkedWriteHandler

11、JDK序列化數據

JDK提供了ObjectOutputStream和ObjectInputStream,用於通過網路對POJO的基本數據類型和圖進行序列化和反序列化。該API並不複雜,而且可以被應用於任何實現了java.io.Serializable介面的對象。但是它的性能也不是非常高效。

如果你的應用程序必須要和使用了ObjectOutputStream和ObjectInputStream的遠程節點交互,並且兼容性也是你最關心的,那麼JDK序列化將是正確的選擇。

12、使用了JBoss Marshalling進行序列化

如果你可以自由地使用外部依賴,那麼JBoss Marshalling將是一個理想的選擇:他比JDK序列化最多快3倍,而且也更加緊湊。

以下代碼展示了如何使用MarshallingDecoder和MarshallingEncoder。同樣,幾乎只是適當地配置ChannelPipeline罷了。

13、通過Protocol Buffers序列化

Netty序列化的最後一個解決方案是利用Protocol Buffers的編解碼器,它是一個由Google公司開發的、現在已經開源的數據交換格式。

Protocol Buffers以一種緊湊而高效的方式對結構化的數據進行編碼以及解碼。它具有許多編程語言綁定,使得它很適合跨語言的項目。

在這裡我們又看到了,使用protobuf只不過是將正確的ChannelHandler添加到ChannelPipeline中,如下代碼。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 全球大搜羅 的精彩文章:

這15種手握壽司你都認識嗎?
事故觀賞守則

TAG:全球大搜羅 |