本章主要內容
- Netty的技術和體系結構方面的內容
Channel
、EventLoop
和ChannelFuture
ChannelHandler
和ChannelPipeline
- 引導
在第1章中,我們給出了Java高性能網絡編程的歷史以及技術基礎的小結。這為Netty的核心概念和構件塊的概述提供了背景。
在第2章中,我們把我們的討論範圍擴大到了應用程序的開發。通過構建一個簡單的客戶端和服務器,你學習了引導,並且獲得了最重要的ChannelHandler
API的實戰經驗。與此同時,你也驗證了自己的開發工具都能正常運行。
由於本書剩下的部分都建立在這份材料的基礎之上,所以我們將從兩個不同的但卻又密切相關的視角來探討Netty:類庫的視角以及框架的視角。對於使用Netty編寫高效的、可重用的和可維護的代碼來說,兩者缺一不可。
從高層次的角度來看,Netty解決了兩個相應的關注領域,我們可將其大致標記為技術的和體系結構的。首先,它的基於Java NIO的異步的和事件驅動的實現,保證了高負載下應用程序性能的最大化和可伸縮性。其次,Netty也包含了一組設計模式,將應用程序邏輯從網絡層解耦,簡化了開發過程,同時也最大限度地提高了可測試性、模塊化以及代碼的可重用性。
在我們更加詳細地研究Netty的各個組件時,我們將密切關注它們是如何通過協作來支撐這些體系結構上的最佳實踐的。通過遵循同樣的原則,我們便可獲得Netty所提供的所有益處。牢記這個目標,在本章中,我們將回顧到目前為止我們介紹過的主要概念和組件。
3.1 Channel、EventLoop和ChannelFuture
接下來的各節將會為我們對於Channel
、EventLoop
和ChannelFuture
類進行的討論增添更多的細節,這些類合在一起,可以被認為是Netty網絡抽像的代表:
Channel
——Socket;
EventLoop
——控制流、多線程處理、並發;ChannelFuture
——異步通知。
3.1.1 Channel接口
基本的I/O操作(bind
、connect
、read
和write
)依賴於底層網絡傳輸所提供的原語。在基於Java的網絡編程中,其基本的構造是class Socket
。Netty的Channel
接口所提供的API,大大地降低了直接使用Socket
類的複雜性。此外,Channel
也是擁有許多預定義的、專門化實現的廣泛類層次結構的根,下面是一個簡短的部分清單:
EmbeddedChannel;
LocalServerChannel;
NioDatagramChannel;
NioSctpChannel;
NioSocketChannel。
3.1.2 EventLoop接口
EventLoop
定義了Netty的核心抽像,用於處理連接的生命週期中所發生的事件。我們將在第7章中結合Netty的線程處理模型的上下文對EventLoop
進行詳細的討論。目前,圖3-1在高層次上說明了Channel
、EventLoop
、Thread
以及EventLoopGroup
之間的關係。
圖3-1 Channel
、EventLoop
和EventLoopGroup
這些關係是:
- 一個
EventLoopGroup
包含一個或者多個EventLoop
; - 一個
EventLoop
在它的生命週期內只和一個Thread
綁定; - 所有由
EventLoop
處理的I/O事件都將在它專有的Thread
上被處理; - 一個
Channel
在它的生命週期內只註冊於一個EventLoop
; - 一個
EventLoop
可能會被分配給一個或多個Channel
。
注意,在這種設計中,一個給定Channel
的I/O操作都是由相同的Thread
執行的,實際上消除了對於同步的需要。
3.1.3 ChannelFuture接口
正如我們已經解釋過的那樣,Netty中所有的I/O操作都是異步的。因為一個操作可能不會立即返回,所以我們需要一種用於在之後的某個時間點確定其結果的方法。為此,Netty提供了ChannelFuture
接口,其addListener
方法註冊了一個ChannelFutureListener
,以便在某個操作完成時(無論是否成功)得到通知。
關於ChannelFuture的更多討論 可以將
ChannelFuture
看作是將來要執行的操作的結果的佔位符。它究竟什麼時候被執行則可能取決於若干的因素,因此不可能準確地預測,但是可以肯定的是它將會被執行。此外,所有屬於同一個Channel
的操作都被保證其將以它們被調用的順序被執行。
我們將在第7章中深入地討論EventLoop
和EventLoopGroup
。
3.2 ChannelHandler和ChannelPipeline
現在,我們將更加細緻地看一看那些管理數據流以及執行應用程序處理邏輯的組件。
3.2.1 ChannelHandler接口
從應用程序開發人員的角度來看,Netty的主要組件是ChannelHandler
,它充當了所有處理入站和出站數據的應用程序邏輯的容器。這是可行的,因為ChannelHandler
的方法是由網絡事件(其中術語「事件」的使用非常廣泛)觸發的。事實上,ChannelHandler
可專門用於幾乎任何類型的動作,例如將數據從一種格式轉換為另外一種格式,或者處理轉換過程中所拋出的異常。
舉例來說,ChannelInboundHandler
是一個你將會經常實現的子接口。這種類型的ChannelHandler
接收入站事件和數據,這些數據隨後將會被你的應用程序的業務邏輯所處理。當你要給連接的客戶端發送響應時,也可以從ChannelInboundHandler
沖刷數據。你的應用程序的業務邏輯通常駐留在一個或者多個ChannelInboundHandler
中。
3.2.2 ChannelPipeline接口
ChannelPipeline
為ChannelHandler
鏈提供了容器,並定義了用於在該鏈上傳播入站和出站事件流的API。當Channel
被創建時,它會被自動地分配到它專屬的ChannelPipeline
。
ChannelHandler
安裝到ChannelPipeline
中的過程如下所示:
- 一個
ChannelInitializer
的實現被註冊到了ServerBootstrap
中[1]; - 當
ChannelInitializer.initChannel
方法被調用時,ChannelInitializer
將在ChannelPipeline
中安裝一組自定義的ChannelHandler
; Channel
Initializer將它自己從ChannelPipeline
中移除。
為了審查發送或者接收數據時將會發生什麼,讓我們來更加深入地研究ChannelPipeline
和ChannelHandler
之間的共生關係吧。
ChannelHandler
是專為支持廣泛的用途而設計的,可以將它看作是處理往來Channel- Pipeline
事件(包括數據)的任何代碼的通用容器。圖3-2說明了這一點,其展示了從Channel- Handler
派生的ChannelInboundHandler
和ChannelOutboundHandler
接口。
圖3-2 ChannelHandler
類的層次結構
使得事件流經ChannelPipeline
是ChannelHandler
的工作,它們是在應用程序的初始化或者引導階段被安裝的。這些對像接收事件、執行它們所實現的處理邏輯,並將數據傳遞給鏈中的下一個ChannelHandler
。它們的執行順序是由它們被添加的順序所決定的。實際上,被我們稱為ChannelPipeline
的是這些ChannelHandler
的編排順序。
圖3-3說明了一個Netty應用程序中入站和出站數據流之間的區別。從一個客戶端應用程序的角度來看,如果事件的運動方向是從客戶端到服務器端,那麼我們稱這些事件為出站的,反之則稱為入站的。
圖3-3 包含入站和出站ChannelHandler
的ChannelPipeline
圖3-3也顯示了入站和出站ChannelHandler
可以被安裝到同一個ChannelPipeline
中。如果一個消息或者任何其他的入站事件被讀取,那麼它會從ChannelPipeline
的頭部開始流動,並被傳遞給第一個ChannelInboundHandler
。這個ChannelHandler
不一定會實際地修改數據,具體取決於它的具體功能,在這之後,數據將會被傳遞給鏈中的下一個ChannelInboundHandler
。最終,數據將會到達ChannelPipeline
的尾端,屆時,所有處理就都結束了。
數據的出站運動(即正在被寫的數據)在概念上也是一樣的。在這種情況下,數據將從ChannelOutboundHandler
鏈的尾端開始流動,直到它到達鏈的頭部為止。在這之後,出站數據將會到達網絡傳輸層,這裡顯示為Socket
。通常情況下,這將觸發一個寫操作。
關於入站和出站ChannelHandler的更多討論
通過使用作為參數傳遞到每個方法的
ChannelHandlerContext
,事件可以被傳遞給當前ChannelHandler
鏈中的下一個ChannelHandler
。因為你有時會忽略那些不感興趣的事件,所以Netty提供了抽像基類ChannelInboundHandlerAdapter
和ChannelOutboundHandlerAdapter
。通過調用ChannelHandlerContext
上的對應方法,每個都提供了簡單地將事件傳遞給下一個ChannelHandler
的方法的實現。隨後,你可以通過重寫你所感興趣的那些方法來擴展這些類。
鑒於出站操作和入站操作是不同的,你可能會想知道如果將兩個類別的ChannelHandler
都混合添加到同一個ChannelPipeline
中會發生什麼。雖然ChannelInboundHandle
和ChannelOutboundHandle
都擴展自ChannelHandler
,但是Netty能區分ChannelIn-boundHandler
實現和ChannelOutboundHandler
實現,並確保數據只會在具有相同定向類型的兩個ChannelHandler
之間傳遞。
當ChannelHandler
被添加到ChannelPipeline
時,它將會被分配一個ChannelHandler-Context
,其代表了ChannelHandler
和ChannelPipeline
之間的綁定。雖然這個對象可以被用於獲取底層的Channel
,但是它主要還是被用於寫出站數據。
在Netty中,有兩種發送消息的方式。你可以直接寫到Channel
中,也可以寫到和Channel-Handler
相關聯的ChannelHandlerContext
對像中。前一種方式將會導致消息從Channel-Pipeline
的尾端開始流動,而後者將導致消息從ChannelPipeline
中的下一個Channel- Handler
開始流動。
3.2.3 更加深入地瞭解ChannelHandler
正如我們之前所說的,有許多不同類型的ChannelHandler
,它們各自的功能主要取決於它們的超類。Netty以適配器類的形式提供了大量默認的ChannelHandler
實現,其旨在簡化應用程序處理邏輯的開發過程。你已經看到了,ChannelPipeline
中的每個ChannelHandler
將負責把事件轉發到鏈中的下一個ChannelHandler
。這些適配器類(及它們的子類)將自動執行這個操作,所以你可以只重寫那些你想要特殊處理的方法和事件。
為什麼需要適配器類
有一些適配器類可以將編寫自定義的
ChannelHandler
所需要的努力降到最低限度,因為它們提供了定義在對應接口中的所有方法的默認實現。下面這些是編寫自定義ChannelHandler時經常會用到的適配器類:
ChannelHandlerAdapter
ChannelInboundHandlerAdapter
ChannelOutboundHandlerAdapter
ChannelDuplexHandler
接下來我們將研究3個ChannelHandler
的子類型:編碼器、解碼器和SimpleChannel-InboundHandler<T>
—— ChannelInboundHandlerAdapter
的一個子類。
3.2.4 編碼器和解碼器
當你通過Netty發送或者接收一個消息的時候,就將會發生一次數據轉換。入站消息會被解碼;也就是說,從字節轉換為另一種格式,通常是一個Java對象。如果是出站消息,則會發生相反方向的轉換:它將從它的當前格式被編碼為字節。這兩種方向的轉換的原因很簡單:網絡數據總是一系列的字節。
對應於特定的需要,Netty為編碼器和解碼器提供了不同類型的抽像類。例如,你的應用程序可能使用了一種中間格式,而不需要立即將消息轉換成字節。你將仍然需要一個編碼器,但是它將派生自一個不同的超類。為了確定合適的編碼器類型,你可以應用一個簡單的命名約定。
通常來說,這些基類的名稱將類似於ByteToMessageDecoder
或MessageToByte-Encoder
。對於特殊的類型,你可能會發現類似於ProtobufEncoder
和ProtobufDecoder
這樣的名稱——預置的用來支持Google的Protocol Buffers。
嚴格地說,其他的處理器也可以完成編碼器和解碼器的功能。但是,正如有用來簡化ChannelHandler
的創建的適配器類一樣,所有由Netty提供的編碼器/解碼器適配器類都實現了ChannelOutboundHandler
或者ChannelInboundHandler
接口。
你將會發現對於入站數據來說,channelRead
方法/事件已經被重寫了。對於每個從入站Channel
讀取的消息,這個方法都將會被調用。隨後,它將調用由預置解碼器所提供的decode
方法,並將已解碼的字節轉發給ChannelPipeline
中的下一個ChannelInboundHandler
。
出站消息的模式是相反方向的:編碼器將消息轉換為字節,並將它們轉發給下一個ChannelOutboundHandler
。
3.2.5 抽像類SimpleChannelInboundHandler
最常見的情況是,你的應用程序會利用一個ChannelHandler
來接收解碼消息,並對該數據應用業務邏輯。要創建一個這樣的ChannelHandler
,你只需要擴展基類SimpleChannel-InboundHandler<T>
,其中T
是你要處理的消息的Java類型。在這個ChannelHandler
中,你將需要重寫基類的一個或者多個方法,並且獲取一個到ChannelHandlerContext
的引用,這個引用將作為輸入參數傳遞給ChannelHandler
的所有方法。
在這種類型的ChannelHandler
中,最重要的方法是channelRead0(Channel-HandlerContext,T)
。除了要求不要阻塞當前的I/O線程之外,其具體實現完全取決於你。我們稍後將對這一主題進行更多的說明。
3.3 引導
Netty的引導類為應用程序的網絡層配置提供了容器,這涉及將一個進程綁定到某個指定的端口,或者將一個進程連接到另一個運行在某個指定主機的指定端口上的進程。
通常來說,我們把前面的用例稱作引導一個服務器,後面的用例稱作引導一個客戶端。雖然這個術語簡單方便,但是它略微掩蓋了一個重要的事實,即「服務器」和「客戶端」實際上表示了不同的網絡行為;換句話說,是監聽傳入的連接還是建立到一個或者多個進程的連接。
面向連接的協議 請記住,嚴格來說,「連接」這個術語僅適用於面向連接的協議,如TCP,其保證了兩個連接端點之間消息的有序傳遞。
因此,有兩種類型的引導:一種用於客戶端(簡單地稱為Bootstrap
),而另一種(ServerBootstrap
)用於服務器。無論你的應用程序使用哪種協議或者處理哪種類型的數據,唯一決定它使用哪種引導類的是它是作為一個客戶端還是作為一個服務器。表3-1比較了這兩種類型的引導類。
表3-1 比較Bootstrap
類
類 別
Bootstrap
ServerBootstrap
網絡編程中的作用
連接到遠程主機和端口
綁定到一個本地端口
EventLoopGroup
的數目
1
2[2]
這兩種類型的引導類之間的第一個區別已經討論過了:ServerBootstrap
將綁定到一個端口,因為服務器必須要監聽連接,而Bootstrap
則是由想要連接到遠程節點的客戶端應用程序所使用的。
第二個區別可能更加明顯。引導一個客戶端只需要一個EventLoopGroup
,但是一個ServerBootstrap
則需要兩個(也可以是同一個實例)。為什麼呢?
因為服務器需要兩組不同的Channel
。第一組將只包含一個ServerChannel
,代表服務器自身的已綁定到某個本地端口的正在監聽的套接字。而第二組將包含所有已創建的用來處理傳入客戶端連接(對於每個服務器已經接受的連接都有一個)的Channel
。圖3-4說明了這個模型,並且展示了為何需要兩個不同的EventLoopGroup
。
圖3-4 具有兩個EventLoopGroup
的服務器
與ServerChannel
相關聯的EventLoopGroup
將分配一個負責為傳入連接請求創建Channel
的EventLoop
。一旦連接被接受,第二個EventLoopGroup
就會給它的Channel
分配一個EventLoop
。
3.4 小結
在本章中,我們從技術和體系結構這兩個角度探討了理解Netty的重要性。我們也更加詳細地重新審視了之前引入的一些概念和組件,特別是ChannelHandler
、ChannelPipeline
和引導。
特別地,我們討論了ChannelHandler
類的層次結構,並介紹了編碼器和解碼器,描述了它們在數據和網絡字節格式之間來回轉換的互補功能。
下面的許多章節都將致力於深入研究這些組件,而這裡所呈現的概覽應該有助於你對整體的把控。
下一章將探索Netty所提供的不同類型的傳輸,以及如何選擇一個最適合於你的應用程序的傳輸。
[1] 或者用於客戶端的Bootstrap
。——譯者注
[2] 實際上,ServerBootstrap
類也可以只使用一個EventLoopGroup
,此時其將在兩個場景下共用同一個EventLoopGroup
。——譯者注