Netty實戰(三)

2023-05-25 12:00:28

一、Channel、EventLoop 和 ChannelFuture

上一篇博文我們在構建伺服器端和使用者端中出現了一些新的類,可能有些同學還有些不瞭解它們的具體功能。沒關係,接下來我們對於 Channel、EventLoop 和 ChannelFuture 類進行的討論增添更多的細節,這些類合在一起,可以被認為是 Netty 網路抽象的代表:

  • Channel : Socket;
  • EventLoop : 控制流、多執行緒處理、並行;
  • ChannelFuture : 非同步通知。

1.1 Channel 介面

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

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

1.2 EventLoop 介面

EventLoop 定義了 Netty 的核心抽象,用於處理連線的生命週期中所發生的事件。如圖在高層次上說明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之間的關係。

這些關係可以表述為:

  • 一個 EventLoopGroup 包含一個或者多個 EventLoop;
  • 一個 EventLoop 在它的生命週期內只和一個 Thread 繫結;
  • 所有由 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理;
  • 一個 Channel 在它的生命週期內只註冊於一個 EventLoop;
  • 一個 EventLoop 可能會被分配給一個或多個 Channel。

注意,在這種設計中,一個給定 Channel 的 I/O 操作都是由相同的 Thread 執行的,實際上消除了對於同步的需要

1.3 ChannelFuture 介面

Netty 中所有的 I/O 操作都是非同步的。因為一個操作可能不會立即返回,所以我們需要一種用於在之後的某個時間點確定其結果的方法。為此,Netty 提供了ChannelFuture 介面,其 addListener()方法註冊了一個ChannelFutureListener,以便在某個操作完成時(無論是否成功)得到通知。

可以將 ChannelFuture 看作是將來要執行的操作的結果的預留位置。它究竟什麼時候被執行則可能取決於若干的因素,因此不可能準確地預測,但是可以肯定的是它將會被執行。此外,所有屬於同一個 Channel 的操作都被保證其將以它們被呼叫的順序被執行。

二、ChannelHandler 和 ChannelPipeline

2.1 ChannelHandler 介面

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


舉例來說,ChannelInboundHandler 是一個我們會經常實現的子介面。這種型別的ChannelHandler 接收入站事件和資料,這些資料隨後將會被你的應用程式的業務邏輯所處理。當我們要給連線的使用者端傳送響應時,也可以從 ChannelInboundHandler 沖刷資料。我們的應用程式的業務邏輯通常駐留在一個或者多個 ChannelInboundHandler 中。

Netty 以介面卡類的形式提供了大量預設的 ChannelHandler 實現,其旨在簡化應用程式處理邏輯的開發過程。如ChannelPipeline中的每個ChannelHandler將負責把事件轉發到鏈中的下一個 ChannelHandler。這些介面卡類(及它們的子類)將自動執行這個操作,所以我們只重寫那些你想要特殊處理的方法和事件。

那麼為什麼要用介面卡的形式提供這些?

那是因為有一些介面卡類可以將編寫自定義的 ChannelHandler 所需要的努力降到最低限度,因為它們提供了定義在對應介面中的所有方法的預設實現。下面這些是編寫自定義 ChannelHandler 時經常會用到的介面卡類:

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandler

2.2 ChannelPipeline 介面

ChannelPipeline 提供了 ChannelHandler 鏈的容器,並定義了用於在該鏈上傳播入站和出站事件流的 API。當 Channel 被建立時,它會被自動地分配到它專屬的 ChannelPipeline。ChannelHandler 安裝到 ChannelPipeline 中的過程如下所示:

  • 一個ChannelInitializer的實現被註冊到了ServerBootstrap中或用於使用者端的Bootstrap
  • 當 ChannelInitializer.initChannel()方法被呼叫時,ChannelInitializer將在 ChannelPipeline 中安裝一組自定義的 ChannelHandler;
  • ChannelInitializer 將它自己從 ChannelPipeline 中移除。

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

ChannelHandler 是專為支援廣泛的用途而設計的,可以將它看作是處理往來 ChannelPipeline 事件(包括資料)的任何程式碼的通用容器。如圖,其展示了從 ChannelHandler 派生的 ChannelInboundHandler 和ChannelOutboundHandler 介面。

使得事件流經 ChannelPipeline 是 ChannelHandler 的工作,它們是在應用程式的初始化或者引導階段被安裝的。這些物件接收事件、執行它們所實現的處理邏輯,並將資料傳遞給鏈中的下一個 ChannelHandler(有點類似責任鏈模式)。它們的執行順序是由它們被新增的順序所決定的。實際上,被我們稱為 ChannelPipeline 的是這些 ChannelHandler 的編排順序。

如圖,說明了一個 Netty 應用程式中入站和出站資料流之間的區別。從一個使用者端應用程式的角度來看,如果事件的運動方向是從使用者端到伺服器端,那麼我們稱這些事件為出站的,反之則稱為入站的。

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

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

ps:通過使用作為引數傳遞到每個方法的 ChannelHandlerContext事件可以被傳遞給當前ChannelHandler 鏈中的下一個ChannelHandler。因為你有時會忽略那些不感興趣的事件,所以 Netty提供了抽象基礎類別
ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter。 ChannelHandlerContext 上的對應方法,每個都提供了簡單地將事件傳遞給下一ChannelHandler的方法的實現。隨後,我們可以通過重寫你所感興趣的那些方法來擴充套件這些類。

上圖中出站和入站的ChannelHandler都在同一個ChannelPipeline中,那麼ChannelPipeline是如何區分和處理這兩種不同的類別的呢?

雖然 ChannelInboundHandle 和ChannelOutboundHandle 都擴充套件自 ChannelHandler,但是 Netty 能區分 ChannelInboundHandler 實現和 ChannelOutboundHandler 實現,並確保資料只會在具有相同定向型別的兩個 ChannelHandler 之間傳遞。

當ChannelHandler 被新增到ChannelPipeline 時,它將會被分配一個ChannelHandlerContext,其代表了 ChannelHandler 和 ChannelPipeline 之間的繫結。雖然這個物件可以被用於獲取底層的 Channel,但是它主要還是被用於寫出站資料。

在 Netty 中,有兩種傳送訊息的方式。我們可以直接寫到 Channel 中,也可以 寫到和 ChannelHandler相關聯的ChannelHandlerContext物件中。前一種方式將會導致訊息從ChannelPipeline 的尾端開始流動,而後者將導致訊息從 ChannelPipeline 中的下一個 ChannelHandler 開始流動。

總結一下:

  • 將訊息寫入Channel 它將從尾端開始流動。
  • 將訊息寫入ChannelHandler中,它將會從下一個ChannelHandler開始流動。

2.3 編碼器和解碼器

當我們通過 Netty 傳送或者接收一個訊息的時候,就將會發生一次資料轉換。入站訊息會被解碼;也就是說,從位元組轉換為另一種格式,通常是一個 Java 物件。如果是出站訊息,則會發生相反方向的轉換:它將從它的當前格式被編碼為位元組。這兩種方向的轉換的原因很簡單:網路資料總是一系列的位元組。(編解碼)

對應於特定的需要,Netty 為編碼器和解碼器提供了不同型別的抽象類。例如,我們的應用程式可能使用了一種中間格式,而不需要立即將訊息轉換成位元組。我們仍然需要一個編碼器,但是它將派生自一個不同的超類。為了確定合適的編碼器型別,我們可以應用一個簡單的命名約定。通常來說,這些基礎類別的名稱將類似於 ByteToMessageDecoder 或 MessageToByteEncoder。對於特殊的型別,我們會發現類似於 ProtobufEncoder 和 ProtobufDecoder這樣的名稱——預置的用來支援 Google 的 Protocol Buffers。

嚴格地說,其他的處理器也可以完成編碼器和解碼器的功能。但是,正如有用來簡化ChannelHandler 的建立的介面卡類一樣,所有由 Netty 提供的編碼器/解碼器介面卡類都實現了 ChannelOutboundHandler 或者 ChannelInboundHandler 介面。

我們會發現對於入站資料來說,channelRead 方法/事件已經被重寫了。對於每個從入站Channel 讀取的訊息,這個方法都將會被呼叫。隨後,它將呼叫由預置解碼器所提供的 decode()方法,並將已解碼的位元組轉發給 ChannelPipeline 中的下一個 ChannelInboundHandler。
出站訊息的模式是相反方向的:編碼器將訊息轉換為位元組,並將它們轉發給下一個ChannelOutboundHandler。

2.4 抽象類 SimpleChannelInboundHandler

最常見的情況是,我們的應用程式會利用一個 ChannelHandler 來接收解碼訊息,並對該資料應用業務邏輯。要建立一個這樣的 ChannelHandler,我們只需要擴充套件基礎類別 SimpleChannelInboundHandler,其中 T 是我們要處理的訊息的 Java 型別 。在這個 ChannelHandler 中,我們需要重寫基礎類別的一個或者多個方法,並且獲取一個到 ChannelHandlerContext 的參照,這個參照將作為輸入引數傳遞給 ChannelHandler 的所有方法。

在這種型別的 ChannelHandler 中,最重要的方法是 channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞當前的 I/O 執行緒之外,其具體實現完全取決於我們。

三、引導

Netty 的引導類為應用程式的網路層設定提供了容器,這涉及將一個程序繫結到某個指定的埠(伺服器端),或者將一個程序連線到另一個執行在某個指定主機的指定埠上的程序(使用者端)。

嚴格來說,「連線」這個術語僅適用於面向連線的協定,如 TCP,其保證了兩個連線端點之間訊息的有序傳遞

因此,有兩種型別的引導:一種用於使用者端(簡單地稱為 Bootstrap),而另一種(ServerBootstrap)用於伺服器。無論我們的應用程式使用哪種協定或者處理哪種型別的資料,唯一決定它使用哪種引導類的是它是作為一個使用者端還是作為一個伺服器(後面我們單獨提出來說明引導)。

類別 Bootstrap ServerBootstrap
網路程式設計中的作用 連線到遠端主機和埠 繫結到一個本地埠
EventLoopGroup 的數目 1 2

ps:實際上,ServerBootstrap 類也可以只使用一個 EventLoopGroup,此時其將在兩個場景下共用同一個 EventLoopGroup

細心的同學應該發現了,ServerBootstrap使用了2個EventLoopGroup,這是因為伺服器需要兩組不同的 Channel。

  • 第一組將只包含一個 ServerChannel,代表伺服器自身的已係結到某個本地埠的正在監聽的通訊端。(專門用來建立Channel )
  • 而第二組將包含所有已建立的用來處理傳入使用者端連線(對於每個伺服器已經接受的連線都有一個)的 Channel。(專門為Channel分配EventLoop)

它們的關係如圖:

ServerChannel 相關聯的 EventLoopGroup 將分配一個負責為傳入連線請求建立Channel 的 EventLoop。一旦連線被接受,第二個 EventLoopGroup 就會給它的 Channel分配一個 EventLoop。