讀古今文學網 > Netty實戰 > 第3章 Netty的組件和設計 >

第3章 Netty的組件和設計

本章主要內容

  • Netty的技術和體系結構方面的內容
  • ChannelEventLoopChannelFuture
  • ChannelHandlerChannelPipeline
  • 引導

在第1章中,我們給出了Java高性能網絡編程的歷史以及技術基礎的小結。這為Netty的核心概念和構件塊的概述提供了背景。

在第2章中,我們把我們的討論範圍擴大到了應用程序的開發。通過構建一個簡單的客戶端和服務器,你學習了引導,並且獲得了最重要的ChannelHandler API的實戰經驗。與此同時,你也驗證了自己的開發工具都能正常運行。

由於本書剩下的部分都建立在這份材料的基礎之上,所以我們將從兩個不同的但卻又密切相關的視角來探討Netty:類庫的視角以及框架的視角。對於使用Netty編寫高效的、可重用的和可維護的代碼來說,兩者缺一不可。

從高層次的角度來看,Netty解決了兩個相應的關注領域,我們可將其大致標記為技術的和體系結構的。首先,它的基於Java NIO的異步的和事件驅動的實現,保證了高負載下應用程序性能的最大化和可伸縮性。其次,Netty也包含了一組設計模式,將應用程序邏輯從網絡層解耦,簡化了開發過程,同時也最大限度地提高了可測試性、模塊化以及代碼的可重用性。

在我們更加詳細地研究Netty的各個組件時,我們將密切關注它們是如何通過協作來支撐這些體系結構上的最佳實踐的。通過遵循同樣的原則,我們便可獲得Netty所提供的所有益處。牢記這個目標,在本章中,我們將回顧到目前為止我們介紹過的主要概念和組件。

3.1 Channel、EventLoop和ChannelFuture

接下來的各節將會為我們對於ChannelEventLoopChannelFuture類進行的討論增添更多的細節,這些類合在一起,可以被認為是Netty網絡抽像的代表:

  • Channel——Socket;
  • EventLoop——控制流、多線程處理、並發;
  • ChannelFuture——異步通知。

3.1.1 Channel接口

基本的I/O操作(bindconnectreadwrite)依賴於底層網絡傳輸所提供的原語。在基於Java的網絡編程中,其基本的構造是class Socket。Netty的Channel接口所提供的API,大大地降低了直接使用Socket類的複雜性。此外,Channel也是擁有許多預定義的、專門化實現的廣泛類層次結構的根,下面是一個簡短的部分清單:

  • EmbeddedChannel;
  • LocalServerChannel;
  • NioDatagramChannel;
  • NioSctpChannel;
  • NioSocketChannel。

3.1.2 EventLoop接口

EventLoop定義了Netty的核心抽像,用於處理連接的生命週期中所發生的事件。我們將在第7章中結合Netty的線程處理模型的上下文對EventLoop進行詳細的討論。目前,圖3-1在高層次上說明了ChannelEventLoopThread以及EventLoopGroup之間的關係。

圖3-1 ChannelEventLoopEventLoopGroup

這些關係是:

  • 一個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章中深入地討論EventLoopEventLoopGroup

3.2 ChannelHandler和ChannelPipeline

現在,我們將更加細緻地看一看那些管理數據流以及執行應用程序處理邏輯的組件。

3.2.1 ChannelHandler接口

從應用程序開發人員的角度來看,Netty的主要組件是ChannelHandler,它充當了所有處理入站和出站數據的應用程序邏輯的容器。這是可行的,因為ChannelHandler的方法是由網絡事件(其中術語「事件」的使用非常廣泛)觸發的。事實上,ChannelHandler可專門用於幾乎任何類型的動作,例如將數據從一種格式轉換為另外一種格式,或者處理轉換過程中所拋出的異常。

舉例來說,ChannelInboundHandler是一個你將會經常實現的子接口。這種類型的ChannelHandler接收入站事件和數據,這些數據隨後將會被你的應用程序的業務邏輯所處理。當你要給連接的客戶端發送響應時,也可以從ChannelInboundHandler沖刷數據。你的應用程序的業務邏輯通常駐留在一個或者多個ChannelInboundHandler中。

3.2.2 ChannelPipeline接口

ChannelPipelineChannelHandler鏈提供了容器,並定義了用於在該鏈上傳播入站和出站事件流的API。當Channel被創建時,它會被自動地分配到它專屬的ChannelPipeline

ChannelHandler安裝到ChannelPipeline中的過程如下所示:

  • 一個ChannelInitializer的實現被註冊到了ServerBootstrap中[1];
  • ChannelInitializer.initChannel方法被調用時,ChannelInitializer將在ChannelPipeline中安裝一組自定義的ChannelHandler
  • ChannelInitializer將它自己從ChannelPipeline中移除。

為了審查發送或者接收數據時將會發生什麼,讓我們來更加深入地研究ChannelPipelineChannelHandler之間的共生關係吧。

ChannelHandler是專為支持廣泛的用途而設計的,可以將它看作是處理往來Channel- Pipeline事件(包括數據)的任何代碼的通用容器。圖3-2說明了這一點,其展示了從Channel- Handler派生的ChannelInboundHandlerChannelOutboundHandler接口。

圖3-2 ChannelHandler類的層次結構

使得事件流經ChannelPipelineChannelHandler的工作,它們是在應用程序的初始化或者引導階段被安裝的。這些對像接收事件、執行它們所實現的處理邏輯,並將數據傳遞給鏈中的下一個ChannelHandler。它們的執行順序是由它們被添加的順序所決定的。實際上,被我們稱為ChannelPipeline的是這些ChannelHandler的編排順序。

圖3-3說明了一個Netty應用程序中入站和出站數據流之間的區別。從一個客戶端應用程序的角度來看,如果事件的運動方向是從客戶端到服務器端,那麼我們稱這些事件為出站的,反之則稱為入站的。

圖3-3 包含入站和出站ChannelHandlerChannelPipeline

圖3-3也顯示了入站和出站ChannelHandler可以被安裝到同一個ChannelPipeline中。如果一個消息或者任何其他的入站事件被讀取,那麼它會從ChannelPipeline的頭部開始流動,並被傳遞給第一個ChannelInboundHandler。這個ChannelHandler不一定會實際地修改數據,具體取決於它的具體功能,在這之後,數據將會被傳遞給鏈中的下一個ChannelInboundHandler。最終,數據將會到達ChannelPipeline的尾端,屆時,所有處理就都結束了。

數據的出站運動(即正在被寫的數據)在概念上也是一樣的。在這種情況下,數據將從ChannelOutboundHandler鏈的尾端開始流動,直到它到達鏈的頭部為止。在這之後,出站數據將會到達網絡傳輸層,這裡顯示為Socket。通常情況下,這將觸發一個寫操作。

關於入站和出站ChannelHandler的更多討論

通過使用作為參數傳遞到每個方法的ChannelHandlerContext,事件可以被傳遞給當前ChannelHandler鏈中的下一個ChannelHandler。因為你有時會忽略那些不感興趣的事件,所以Netty提供了抽像基類ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter。通過調用ChannelHandlerContext上的對應方法,每個都提供了簡單地將事件傳遞給下一個ChannelHandler的方法的實現。隨後,你可以通過重寫你所感興趣的那些方法來擴展這些類。

鑒於出站操作和入站操作是不同的,你可能會想知道如果將兩個類別的ChannelHandler都混合添加到同一個ChannelPipeline中會發生什麼。雖然ChannelInboundHandleChannelOutboundHandle都擴展自ChannelHandler,但是Netty能區分ChannelIn-boundHandler實現和ChannelOutboundHandler實現,並確保數據只會在具有相同定向類型的兩個ChannelHandler之間傳遞。

ChannelHandler被添加到ChannelPipeline時,它將會被分配一個ChannelHandler-Context,其代表了ChannelHandlerChannelPipeline之間的綁定。雖然這個對象可以被用於獲取底層的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為編碼器和解碼器提供了不同類型的抽像類。例如,你的應用程序可能使用了一種中間格式,而不需要立即將消息轉換成字節。你將仍然需要一個編碼器,但是它將派生自一個不同的超類。為了確定合適的編碼器類型,你可以應用一個簡單的命名約定。

通常來說,這些基類的名稱將類似於ByteToMessageDecoderMessageToByte-Encoder。對於特殊的類型,你可能會發現類似於ProtobufEncoderProtobufDecoder這樣的名稱——預置的用來支持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將分配一個負責為傳入連接請求創建ChannelEventLoop。一旦連接被接受,第二個EventLoopGroup就會給它的Channel分配一個EventLoop

3.4 小結

在本章中,我們從技術和體系結構這兩個角度探討了理解Netty的重要性。我們也更加詳細地重新審視了之前引入的一些概念和組件,特別是ChannelHandlerChannelPipeline和引導。

特別地,我們討論了ChannelHandler類的層次結構,並介紹了編碼器和解碼器,描述了它們在數據和網絡字節格式之間來回轉換的互補功能。

下面的許多章節都將致力於深入研究這些組件,而這裡所呈現的概覽應該有助於你對整體的把控。

下一章將探索Netty所提供的不同類型的傳輸,以及如何選擇一個最適合於你的應用程序的傳輸。


[1] 或者用於客戶端的Bootstrap。——譯者注

[2] 實際上,ServerBootstrap類也可以只使用一個EventLoopGroup,此時其將在兩個場景下共用同一個EventLoopGroup。——譯者注