Netty基礎招式——ChannelHandler的最佳實踐

阿丸發表於2021-08-09

本文是Netty系列第7篇

上一篇文章我們深入學習了Netty邏輯架構中的核心元件EventLoop和EventLoopGroup,掌握了Netty的執行緒模型,並且介紹了Netty4執行緒模型中的無鎖序列化設計。

今天,我們繼續學習Netty邏輯架構中的另一個核心元件ChannelHandler和ChannelPipeline。

如果說執行緒模型是Netty的 “核心內功”,那麼ChannelHandler就是Netty最著名的 “武功招式”,是我們日常使用Netty時接觸最多的元件。

Netty基礎招式——ChannelHandler的最佳實踐

 

引用《Netty in action》中的一句話

From the appliaction developer's standpoint, the primary component of Netty is the ChannelHandler.

所以,阿丸儘可能通過 圖 和 程式碼demo,來讓大家獲得最直觀的使用體驗。

本文預計閱讀時間約 10分鐘,將重點圍繞以下幾個問題展開:

  • 什麼是ChannelHandler和ChannelPipeline?
  • ChannelHandler的事件傳播機制
  • ChannelHandler的異常處理機制
  • ChannelHandler的最佳實踐

1. 什麼是ChannelHandler和ChannelPipeline?

ChannelHandler是一個包含所有應用處理邏輯的容器載體,用來對Netty的輸入輸出資料進行加工處理。

比如資料格式轉換、異常處理等

ChannelPipeline 則是 ChannelHandler 的容器載體,負責以鏈式的形式排程各個註冊的ChannelHandler。

我們回顧下之前介紹過的Netty邏輯架構,觀察下ChannelPipeline和ChannelHandler的位置。

Netty基礎招式——ChannelHandler的最佳實踐

 

再從區域性放大,可以更加明確地看到ChannelPipeline和ChannelHandler的作用。

Netty基礎招式——ChannelHandler的最佳實踐

 

如上圖所示,當EventLoop中監聽到事件後,會對I/O事件進行處理。而這個處理,就是交給ChannelPipeline進行,更嚴格地說,是交給ChannelPipeline中的各個ChannelHandler按照一定的順序進行處理。

根據資料的流向,Netty把ChannelHandler分為2類,InboundHandler和OutboundHandler。

Netty基礎招式——ChannelHandler的最佳實踐

 

如上圖所示,Netty接收到資料後,經過若干 InboundHandler 處理後接收成功。如果要輸出資料,就需要經過若干個 OutboundHandler 處理完成後傳送。

比如,我們經常需要對接收到的資料進行解碼,就是在某一個專門decode的InboundHandler中處理的。如果要傳送資料,往往需要編碼,就是在某一個專門encode的OutBoundHandler中處理的。

值得一提的是,雖然我們在使用Netty時,直接打交道的是ChannelPipeline和ChannelHandler,但是,它們之間有一座“隱形”的橋樑,名字叫做ChannelHandlerContext。

顧名思義,ChannelHanderContext就是ChannelHandler的上下文,每個 ChannelHandler 都對應一個 ChannelHandlerContext。

每一個 ChannelPipeline 都包含多個 ChannelHandlerContext,所有 ChannelHandlerContext 之間組成了雙向連結串列。如下圖所示。

Netty基礎招式——ChannelHandler的最佳實踐

 

其中,有兩個特殊的ChannelHandlerContext,分別是HeadContext和TailContext,表示雙向連結串列的頭尾節點。

Netty基礎招式——ChannelHandler的最佳實踐

 

從類圖上可以看到,HeadContext同時實現了ChannelInboundHandler和ChannelOutboundHandler。因此,HeadContext在讀取資料時作為頭節點,向後傳遞InBound事件,同時,在寫資料時作為尾節點,處理最後的OutBound事件。

TailContext只實現了ChannelInboundHandler。它在InBound事件傳遞的末尾,負責處理一些資源釋放的工作。在OutBound事件傳遞的第一個節點,不做任何處理,僅僅傳遞OutBound事件給prev節點。

而我們平時自定義的ChannelHandler,就是插在這兩個頭尾節點之間的。

至此,我們對ChannelHandler和ChannelPipeline有了基本的認識。具體到實踐上,我們該如何正確地使用ChannelHandler呢?

對ChannelHandler的使用,必須先了解ChannelHandler的事件傳播機制和異常處理機制。

2. ChannelHandler的事件傳播機制

前面我們提到了Netty中的兩種事件型別,Inbound事件和Outbound事件,分別對應InboundHandler和OutbountHandler進行處理。

當我們使用Netty進行開發的時候,必須瞭解Inbound事件和Outbound事件在ChannelPipeline中如何進行“事件傳播”,註冊InboundHandler和OutboundHandler的順序有什麼影響。

話不多說,我們先來一個demo直觀地感受一下。

自定義一個ChannelInboundHandler

Netty基礎招式——ChannelHandler的最佳實踐

 

自定義一個ChannelOutboundHandler

Netty基礎招式——ChannelHandler的最佳實踐

 

簡單組裝一下EchoPipelineServer,特別注意一下 6個handler 的註冊順序。

Netty基礎招式——ChannelHandler的最佳實踐

 

然後我們通過命令列簡單訪問一下這個Netty Server

curl localhost:8081

可以看到控制檯的如下輸出

Netty基礎招式——ChannelHandler的最佳實踐

 

這樣就清楚了事件傳播順序:
- 對於Inbound事件,InboundHandler的處理順序是和註冊順序一致
- 對於Outbound事件,OutboundHandler的處理順序和註冊順序相反

結合上一節說的HeadContext和TailContext,我們畫個圖來更直觀地看一下這個ChannelPipeline中的handler構建順序是怎樣的。

Netty基礎招式——ChannelHandler的最佳實踐

 

在上面的ChannelInitializer中,我們按需新增了3個InboundHandler和3個OutboundHandler。所以,在頭節點HeadContext和TailContext之間,有序構成了雙向連結串列。

而InboundHandler3中,通過呼叫 ctx.channel.writeAndFlush( msg ) 方法,將訊息從TailContext開始,依據OutboundHandler的路徑向HeadContext方向傳播出去。具體可以看下DefaultChannelPipeline類中的實現

Netty基礎招式——ChannelHandler的最佳實踐

 

雖然這裡是雙向連結串列,但是無論是Inbound事件還是Outbound事件,在按序訪問連結串列節點時,會根據事件型別進行過濾。

3. ChannelHandler的異常傳播機制

我們已經瞭解了ChannelPipeline的鏈式傳遞規則,如果雙向連結串列中任意一個handler丟擲了異常,那麼應該怎麼處理呢?

3.1 InboundHandler的異常處理

我們修改下示例中的TestInboudHandler進行模擬。

  • channelRead方法中丟擲異常
  • 重寫exceptionCaught方法,列印當前節點捕獲異常情況
Netty基礎招式——ChannelHandler的最佳實踐

 

得到輸出如下

Netty基礎招式——ChannelHandler的最佳實踐

 

可以看到,雖然在InboundHander1中丟擲了異常,但是仍然會被3個InboundHandler都捕獲一次,並按序向tail節點方向傳遞,然後丟擲異常。

我們也看到了,Netty給出了會警告,在最後的節點沒有進行異常處理。

An exceptionCaught() event was fired, and it reached at the tail of the pipeline. 
It usually means the last handler in the pipeline did not handle the exception.

3.2 OutboundHandler的異常處理

OutboundHandler也是這麼操作嗎?
我們來做個實驗。

  • 在write操作中丟擲異常
  • 重寫下exceptionCaught方法(這個方法在OutboundHandler中被標記為廢棄)

重寫組裝下channelPipeline,第二個OutboundHandler中丟擲異常

Netty基礎招式——ChannelHandler的最佳實踐

 

結果得到的輸出如下

Netty基礎招式——ChannelHandler的最佳實踐

 

咦?異常被吃掉了!!
不僅沒有走進exceptionCaught方法,也沒有其他異常丟擲。
只是對後續handler的write方法不再執行,而flush方法還是都執行了一遍。

我們從原始碼找找原因吧。跟一下斷點,馬上就找到了原因:

Netty基礎招式——ChannelHandler的最佳實踐

 


AbstractChannelHandlerContext中,對OutboundHandler的write方法做了異常捕獲,然後對ChannelPromise進行了通知。
後續原始碼就不展開了,有興趣的同學自己打斷點跟一下,比較清楚。

那麼問題來了,怎麼在OutboundHandler中捕獲異常呢?很明顯就是直接新增ChannelPromise的回撥。
上程式碼:

Netty基礎招式——ChannelHandler的最佳實踐

 

在前面提到的ExceptionHandler中,複寫write方法,然後註冊一個ChannelPromise的Listener就行了。
當然,這個ExceptionHandler同樣要註冊到ChannelPipeline。

千萬注意!!這裡ExceptionHandler同樣是新增到ChannelPipeline的tail方向的最後,而不是新增在head方向。
無論是inboundHandler或者是outboundHandler的異常,都是按序向tail方向傳遞的。

異常就這樣抓到了。

Netty基礎招式——ChannelHandler的最佳實踐

 

4. ChannelHandler的最佳實踐

其實前面已經對ChannelHandler的常用機制做了介紹,這裡簡單再介紹下兩個最佳實踐。

4.1 不在ChannelHandler中做耗時處理

這一點其實在前一篇《 深入Netty邏輯架構,從Reactor執行緒模型開始》已經提到過,這裡作為自定義ChannelHandler的最佳實踐再強調一下,不在ChannelHandler中做耗時處理。

這裡包括兩點。

一是不在I/O執行緒中直接處理耗時操作。

二是也不把耗時操作放進EventLoop的任務佇列中。

由於Netty4的無鎖序列化設計,一旦任何耗時操作阻塞了某個EventLoop,那麼這個EventLoop上的各個channel都會被阻塞。更詳細內容可以參考上一篇《 深入Netty邏輯架構,從Reactor執行緒模型開始》。

所以,我們對於耗時操作,我們要放在自己的業務執行緒池中進行處理,如果需要傳送response,需要提交任務到EventLoop的任務佇列中執行。

給個簡單的demo。

Netty基礎招式——ChannelHandler的最佳實踐

 

4.2 統一的異常處理

在本文的第三節中,講解了ChannelHandler的異常傳播機制。

對於InboundHandler來說,如果你有跟handler特定相關的異常,可以直接在handler裡進行exceptionCaught。如果是一些通用的異常,可以自定義ExceptionHandler註冊到ChannelPipeline的末尾進行統一攔截。

對於OutboudHandler來說,就是通過自定義ExceptionHandler,重寫對應方法,並註冊ChannelPromise的Listener。同樣的,ExceptionHandler註冊到ChannelPipeline的末尾進行統一攔截。

所以,總結下如何新增一個“統一”的異常攔截器呢?

  • 自定義ExceptionHandler繼承ChannelDuplexHandler,並註冊到 tail節點前(ChannelPipeline的最後一個節點)
  • 對於Inbound事件,我們需要在exceptionCaught()進行處理
  • 對於Outbound事件,我們需要不同的ChannelFutureListener

異常攔截器的註冊位置應該在tail方向的最後一個Handler。

Netty基礎招式——ChannelHandler的最佳實踐

 

注意,統一異常處理除了更優雅處理通用異常外,也是排查故障的好幫手。比如有時候對於編解碼異常,可以在統一處理異常處捕獲,快速定位問題。

5.小結

來簡單回顧下吧。

本文介紹了什麼是ChannelHandler和ChannelPipeline。能釐清InboundChannelHandler、OutboundChannelHandler、ChannelHandlerContext是什麼嗎?

然後對ChannelHandler的事件傳播機制、異常處理機制做了詳細介紹。

最後說明了日常開發中ChannelHandler的最佳實踐。

希望對大家有所幫助。

 

參考書目:
《Netty in Action》

 

都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回覆【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)

相關文章