Netty 框架總結「ChannelHandler 及 EventLoop」

雪中亮發表於2018-01-17

「部落格搬家」 原地址: 簡書 原發表時間: 2017-05-05

學習了一段時間的 Netty,將重點與學習心得總結如下,本文主要總結ChannelHandler 及 EventLoop 的知識點和基本用法,本文章節排序參照《Netty in Action》的章節排序。

以下內容主要參考「併發程式設計網」的 《Netty in Action》中文版 以及《Netty in Action》原版圖書,輔助參考 Essential Netty in Action 《Netty 實戰(精髓)》 以及 Netty 官網的 Netty 4.1 JavaDoc

6. ChannelHandler 和 ChannelPipeline

一個 Channel 正常的生命週期如下圖所示。隨著狀態發生變化,相應的 event 產生。這些 event 被轉發到 ChannelPipeline 中的 ChannelHandler 來採取相應的操作。

Channel狀態模型

6.1 ChannelHandler

ChannelHandler 有兩個重要的子介面:

  • 「ChannelInboundHandler」處理輸入資料和所有型別的狀態變化
  • 「ChannelOutboundHandler」處理輸出資料,可以攔截所有操作

6.1.1 ChannelInboundHandler

下表列出介面 ChannelInboundHandler 的方法。當收到資料或相關 Channel 的狀態改變時,這些方法被呼叫,這些方法和Channel的生命週期密切相關

方法 描述
channelRegistered 當一個Channel註冊到EventLoop上,可以處理I/O時被呼叫
channelUnregistered 當一個Channel從它的EventLoop上解除註冊,不再處理I/O時被呼叫
channelActive 當Channel變成活躍狀態時被呼叫;Channel是連線/繫結、就緒的
channelInactive 當Channel離開活躍狀態,不再連線到某個遠端時被呼叫
channelReadComplete 當Channel上的某個讀操作完成時被呼叫
channelRead 當從Channel中讀資料時被呼叫

6.1.2 ChannelOutboundHandler

輸出的操作和資料由 ChannelOutBoundHandler 處理。它的方法可以被 Channel,ChannelPipeline 和 ChannelHandlerContext 呼叫,子介面 ChannelOutboundHandler 的主要方法如下:

方法 描述
bind(ChannelHandlerContext,SocketAddress,ChannelPromise) 請求繫結 Channel 到一個本地地址
connect(ChannelHandlerContext, SocketAddress,SocketAddress,ChannelPromise) 請求連線 Channel 到遠端
disconnect(ChannelHandlerContext, ChannelPromise) 請求從遠端斷開 Channel
close(ChannelHandlerContext,ChannelPromise) 請求關閉 Channel
deregister(ChannelHandlerContext, ChannelPromise) 請求 Channel 從它的 EventLoop 上解除註冊
read(ChannelHandlerContext) 請求從 Channel 中讀更多的資料
flush(ChannelHandlerContext) 請求通過 Channel 刷佇列資料到遠端
write(ChannelHandlerContext,Object, ChannelPromise) 請求通過 Channel 寫資料到遠端

6.1.3 ChannelHandler 介面卡類

ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 這兩個介面卡類分別提供了 ChannelInboundHandler 和 ChannelOutboundHandler 的基本實現,它們繼承了共同的父介面 ChannelHandler 的方法,擴充套件了抽象類 ChannelHandlerAdapter。

ChannelHandlerAdapter類層級關係

  • ChannelHandlerAdapter 提供了工具方法 isSharable()。如果類實現帶 @Sharable 註解,那麼這個方法就會返回 true,意味著這個物件可以被新增到多個 ChannelPipeline 中。

  • ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 中的方法呼叫相關 ChannelHandlerContext 中的等效方法,因此將事件轉發到管道中的下一個ChannelHandler。

6.1.4 ChannelFuture 和 ChannelPromise

  • ChannelPromise 是 ChannelFuture 的子介面
  • 而 ChannelFuture 是不可變物件
  • ChannelPromise 定義了可寫的方法,比如 setSuccess(), setFailure()

6.1.5 釋放資源

1. 輸入方向「Inbound」 當一個 ChannelInboundHandler 實現類重寫 channelRead() 方法時,它要負責釋放 ByteBuf 相關的記憶體。可使用 Netty 提供的工具方法:

    ReferenceCountUtil.release(「ByteBuf 的物件」)
複製程式碼

更簡單的,可使用子類 SimpleChannelInboundHandler ,一條訊息在被 ChannelRead0() 讀取後,會被自動釋放資源,此時任何對訊息的引用都會變成無效,所以不能儲存這些引用待後來使用。

2. 輸出方向「Outbound」 在輸出方向,如果處理一個 write() 操作並且丟棄一條訊息(沒有寫入 Channel),就應該負責釋放這條訊息。

@ChannelHandler.Sharable public 
class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    ReferenceCountUtil.release(msg);  //使用 ReferenceCountUtil.release(...) 釋放資源
    promise.setSuccess();  //通知 ChannelPromise 資料已經被處理
}
複製程式碼

如果一個訊息被“消費”或者丟棄,沒有送到 ChannelPipeline 中的下一個 ChannelOutboundHandler,使用者就要負責呼叫 ReferenceCountUtil.release()。如果訊息到達了真正的傳輸層,在它被寫到 Socket 中或者 Channel 關閉時,會被自動釋放,使用者不用管。

6.2 ChannelPipeline 介面

  • 每個新建立的 Channel 都會分配一個新的 ChannelPipeline,Channel 不可以更換或解除當前的 ChannelPipeline,在 Netty 元件的整個生命週期中這個關係是固定的。

  • 一個 ChannelPipeline 可看成是一串 ChannelHandler 例項,攔截穿過 Channel 的輸入輸出 event。

  • 根據來源,一個 event 可以被一個 ChannelInboundHandler 或 ChannelOutboundHandler 處理。接下來,通過呼叫 ChannelHandlerContext 的方法,event 會被轉發到下一個同型別的 handler。

6.2.1 ChannelHandlerContext

  • 通過 ChannelHandlerContext,一個 handler 可以通知 ChannelPipeline 中的下一個ChannelHandler,甚至動態改動下一個ChannelHandler 所屬的 ChannelPipeline。

  • ChannelPipeline 主要由一系列 ChannelHandler 組成的。ChannelPipeline 提供在 ChannelPipeline 中傳送 event 的方法。

  • ChannelHandlerContext 的一些方法和其他類(Channel 和 ChannelPipeline)的方法名字相似,但是 ChannelHandlerContext 的方法採用了更短的 event 傳遞路程。我們應該儘可能利用這一點來實現最好的效能。

  • 如果你在 Channel 或者 ChannelPipeline 例項上呼叫這些方法,它們的呼叫會穿過整個 pipeline。而在 ChannelHandlerContext 上呼叫的同樣的方法,僅僅從當前 ChannelHandler 開始,走到 pipeline 中下一個可以處理這個 event 的 ChannelHandler。

ChannelPipeline 和 ChannelHandlers

「本節參考」 第六章 ChannelHandler 和 ChannelPipeline

7. EventLoop 和 EventLoopGroup

7.1 Java 基本的執行緒池模式

  • 從池中空閒的執行緒中選出一個,分配一個提交的task「一個Runnable的實現」
  • 當task完成,執行緒返回池中,等待複用「下一次task分配」

7.2 EventLoop「事件迴圈」

  • EventLoop 始終由一個執行緒驅動
  • 一個 EventLoop 可以被指派來服務多個 Channel
  • 一個 Channel 只擁有一個 EventLoop

task (Runnable或Callable) 可以直接提交到 EventLoop 實現即刻或者延後執行。根據配置和可用的CPU核,可以建立多個 EventLoop 來優化資源利用。

一個 event 的本質決定了它將如何被處理;它可能從網路協議棧傳送資料到你的應用,或者反過來,或者做一些完全不一樣的事情。但是 event 處理邏輯必須足夠通用和靈活,來對付所有可能的情況。

所以,在 Netty 4,所有的 I/O 操作和 event 都是由分配給 EventLoop 的那一個 Thread 來處理的。Netty 4 採用的執行緒模型,在同一個執行緒的 EventLoop 中處理所有發生的事。

7.3 EventLoopGroup

  • EventLoopGroup 負責分配 EventLoop 到新建立的 Channel
  • 非同步實現只用了很少 EventLoop,這幾個 EventLoop 被所有 Channel 共享
  • 一但 Channel 被指派了一個 EventLoop,在它的整個生命週期過程中,都會用這個 EventLoop

針對非阻塞傳輸的EventLoop分配

為 Channel 的 I/O 和 event 提供服務的 EventLoop 都包含在一個 EventLoopGroup 中。EventLoop 建立和分配的方式根據傳輸實現的不同而有所不同。

非同步實現只用了很少幾個 EventLoop(和它們關聯的執行緒),在目前 Netty 的模型中,這幾個 EventLoop 被所有 Channel 共享。這讓很多 Channel 被最少數量的執行緒服務,而不是每個 Channel 分配一個執行緒。

EventLoopGroup 負責分配一個 EventLoop 到每個新建立的 Channel。在目前的實現中,採用迴圈 (round-robin) 策略可以滿足一個平衡的分配,同一個 Eventloop 還可能會被分配到多個 Channel。

「本節參考」 第七章 EventLoop和執行緒模型

參考連結

  1. 《Netty in Action》中文版
  2. Essential Netty in Action 《Netty 實戰(精髓)》
  3. Netty 4.1 JavaDoc

相關文章