Netty基礎(chǔ)招式——ChannelHandler的優(yōu)秀實踐
今天,我們繼續(xù)學(xué)習(xí)Netty邏輯架構(gòu)中的另一個核心組件ChannelHandler和ChannelPipeline。
如果說線程模型是Netty的 “核心內(nèi)功”,那么ChannelHandler就是Netty最著名的 “武功招式”,是我們?nèi)粘J褂肗etty時接觸最多的組件。
引用《Netty in action》中的一句話
- From the appliaction developer's standpoint, the primary component of Netty is the ChannelHandler.
所以,阿丸盡可能通過 圖 和 代碼demo,來讓大家獲得最直觀的使用體驗。
本文預(yù)計閱讀時間約 10分鐘,將重點圍繞以下幾個問題展開:
- 什么是ChannelHandler和ChannelPipeline?
- ChannelHandler的事件傳播機制
- ChannelHandler的異常處理機制
- ChannelHandler的最佳實踐
1、什么是ChannelHandler和ChannelPipeline
ChannelHandler是一個包含所有應(yīng)用處理邏輯的容器載體,用來對Netty的輸入輸出數(shù)據(jù)進(jìn)行加工處理。
比如數(shù)據(jù)格式轉(zhuǎn)換、異常處理等
ChannelPipeline 則是 ChannelHandler 的容器載體,負(fù)責(zé)以鏈?zhǔn)降男问秸{(diào)度各個注冊的ChannelHandler。
我們回顧下之前介紹過的Netty邏輯架構(gòu),觀察下ChannelPipeline和ChannelHandler的位置。

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

如上圖所示,當(dāng)EventLoop中監(jiān)聽到事件后,會對I/O事件進(jìn)行處理。而這個處理,就是交給ChannelPipeline進(jìn)行,更嚴(yán)格地說,是交給ChannelPipeline中的各個ChannelHandler按照一定的順序進(jìn)行處理。
根據(jù)數(shù)據(jù)的流向,Netty把ChannelHandler分為2類,InboundHandler和OutboundHandler。

如上圖所示,Netty接收到數(shù)據(jù)后,經(jīng)過若干 InboundHandler 處理后接收成功。如果要輸出數(shù)據(jù),就需要經(jīng)過若干個 OutboundHandler 處理完成后發(fā)送。
比如,我們經(jīng)常需要對接收到的數(shù)據(jù)進(jìn)行解碼,就是在某一個專門decode的InboundHandler中處理的。如果要發(fā)送數(shù)據(jù),往往需要編碼,就是在某一個專門encode的OutBoundHandler中處理的。
值得一提的是,雖然我們在使用Netty時,直接打交道的是ChannelPipeline和ChannelHandler,但是,它們之間有一座“隱形”的橋梁,名字叫做ChannelHandlerContext。
顧名思義,ChannelHanderContext就是ChannelHandler的上下文,每個 ChannelHandler 都對應(yīng)一個 ChannelHandlerContext。
每一個 ChannelPipeline 都包含多個 ChannelHandlerContext,所有 ChannelHandlerContext 之間組成了雙向鏈表。如下圖所示。

其中,有兩個特殊的ChannelHandlerContext,分別是HeadContext和TailContext,表示雙向鏈表的頭尾節(jié)點。

從類圖上可以看到,HeadContext同時實現(xiàn)了ChannelInboundHandler和ChannelOutboundHandler。因此,HeadContext在讀取數(shù)據(jù)時作為頭節(jié)點,向后傳遞InBound事件,同時,在寫數(shù)據(jù)時作為尾節(jié)點,處理最后的OutBound事件。
TailContext只實現(xiàn)了ChannelInboundHandler。它在InBound事件傳遞的末尾,負(fù)責(zé)處理一些資源釋放的工作。在OutBound事件傳遞的第一個節(jié)點,不做任何處理,僅僅傳遞OutBound事件給prev節(jié)點。
而我們平時自定義的ChannelHandler,就是插在這兩個頭尾節(jié)點之間的。
至此,我們對ChannelHandler和ChannelPipeline有了基本的認(rèn)識。具體到實踐上,我們該如何正確地使用ChannelHandler呢?
對ChannelHandler的使用,必須先了解ChannelHandler的事件傳播機制和異常處理機制。
2、ChannelHandler的事件傳播機制
前面我們提到了Netty中的兩種事件類型,Inbound事件和Outbound事件,分別對應(yīng)InboundHandler和OutbountHandler進(jìn)行處理。
當(dāng)我們使用Netty進(jìn)行開發(fā)的時候,必須了解Inbound事件和Outbound事件在ChannelPipeline中如何進(jìn)行“事件傳播”,注冊InboundHandler和OutboundHandler的順序有什么影響。
話不多說,我們先來一個demo直觀地感受一下。
自定義一個ChannelInboundHandler

自定義一個ChannelOutboundHandler

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

然后我們通過命令行簡單訪問一下這個Netty Server
- curl localhost:8081
可以看到控制臺的如下輸出

這樣就清楚了事件傳播順序:
- - 對于Inbound事件,InboundHandler的處理順序是和注冊順序一致
- - 對于Outbound事件,OutboundHandler的處理順序和注冊順序相反
結(jié)合上一節(jié)說的HeadContext和TailContext,我們畫個圖來更直觀地看一下這個ChannelPipeline中的handler構(gòu)建順序是怎樣的。

在上面的ChannelInitializer中,我們按需添加了3個InboundHandler和3個OutboundHandler。所以,在頭節(jié)點HeadContext和TailContext之間,有序構(gòu)成了雙向鏈表。
而InboundHandler3中,通過調(diào)用 ctx.channel.writeAndFlush( msg ) 方法,將消息從TailContext開始,依據(jù)OutboundHandler的路徑向HeadContext方向傳播出去。具體可以看下DefaultChannelPipeline類中的實現(xiàn)
雖然這里是雙向鏈表,但是無論是Inbound事件還是Outbound事件,在按序訪問鏈表節(jié)點時,會根據(jù)事件類型進(jìn)行過濾。
3、ChannelHandler的異常傳播機制
我們已經(jīng)了解了ChannelPipeline的鏈?zhǔn)絺鬟f規(guī)則,如果雙向鏈表中任意一個handler拋出了異常,那么應(yīng)該怎么處理呢?
3.1 InboundHandler的異常處理
我們修改下示例中的TestInboudHandler進(jìn)行模擬。
- channelRead方法中拋出異常
- 重寫exceptionCaught方法,打印當(dāng)前節(jié)點捕獲異常情況
得到輸出如下
可以看到,雖然在InboundHander1中拋出了異常,但是仍然會被3個InboundHandler都捕獲一次,并按序向tail節(jié)點方向傳遞,然后拋出異常。
我們也看到了,Netty給出了會警告,在最后的節(jié)點沒有進(jìn)行異常處理。
- 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中被標(biāo)記為廢棄)
重寫組裝下channelPipeline,第二個OutboundHandler中拋出異常
結(jié)果得到的輸出如下:
咦?異常被吃掉了!!
不僅沒有走進(jìn)exceptionCaught方法,也沒有其他異常拋出。
只是對后續(xù)handler的write方法不再執(zhí)行,而flush方法還是都執(zhí)行了一遍。
我們從源碼找找原因吧。跟一下斷點,馬上就找到了原因:
在AbstractChannelHandlerContext中,對OutboundHandler的write方法做了異常捕獲,然后對ChannelPromise進(jìn)行了通知。
后續(xù)源碼就不展開了,有興趣的同學(xué)自己打斷點跟一下,比較清楚。
那么問題來了,怎么在OutboundHandler中捕獲異常呢?很明顯就是直接添加ChannelPromise的回調(diào)。
上代碼:
在前面提到的ExceptionHandler中,復(fù)寫write方法,然后注冊一個ChannelPromise的Listener就行了。
當(dāng)然,這個ExceptionHandler同樣要注冊到ChannelPipeline。
千萬注意!!這里ExceptionHandler同樣是添加到ChannelPipeline的tail方向的最后,而不是添加在head方向。
無論是inboundHandler或者是outboundHandler的異常,都是按序向tail方向傳遞的。
異常就這樣抓到了。
4、ChannelHandler的最佳實踐
其實前面已經(jīng)對ChannelHandler的常用機制做了介紹,這里簡單再介紹下兩個最佳實踐。
4.1 不在ChannelHandler中耗時處理
這一點其實在前一篇《 深入Netty邏輯架構(gòu),從Reactor線程模型開始》已經(jīng)提到過,這里作為自定義ChannelHandler的最佳實踐再強調(diào)一下,不在ChannelHandler中做耗時處理。
這里包括兩點。
- 不在I/O線程中直接處理耗時操作。
- 也不把耗時操作放進(jìn)EventLoop的任務(wù)隊列中。
由于Netty4的無鎖串行化設(shè)計,一旦任何耗時操作阻塞了某個EventLoop,那么這個EventLoop上的各個channel都會被阻塞。更詳細(xì)內(nèi)容可以參考上一篇《 深入Netty邏輯架構(gòu),從Reactor線程模型開始》。
所以,我們對于耗時操作,我們要放在自己的業(yè)務(wù)線程池中進(jìn)行處理,如果需要發(fā)送response,需要提交任務(wù)到EventLoop的任務(wù)隊列中執(zhí)行。
給個簡單的demo。
4.2 統(tǒng)一的異常處理
在本文的第三節(jié)中,講解了ChannelHandler的異常傳播機制。
對于InboundHandler來說,如果你有跟handler特定相關(guān)的異常,可以直接在handler里進(jìn)行exceptionCaught。如果是一些通用的異常,可以自定義ExceptionHandler注冊到ChannelPipeline的末尾進(jìn)行統(tǒng)一攔截。
對于OutboudHandler來說,就是通過自定義ExceptionHandler,重寫對應(yīng)方法,并注冊ChannelPromise的Listener。同樣的,ExceptionHandler注冊到ChannelPipeline的末尾進(jìn)行統(tǒng)一攔截。
所以,總結(jié)下如何添加一個“統(tǒng)一”的異常攔截器呢?
- 自定義ExceptionHandler繼承ChannelDuplexHandler,并注冊到 tail節(jié)點前(ChannelPipeline的最后一個節(jié)點)。
- 對于Inbound事件,我們需要在exceptionCaught()進(jìn)行處理。
- 對于Outbound事件,我們需要對OutboundHandler的不同方法(如write、flush)注冊ChannelFutureListener事件。
異常攔截器的注冊位置應(yīng)該在tail方向的最后一個Handler。
注意,統(tǒng)一異常處理除了更優(yōu)雅處理通用異常外,也是排查故障的好幫手。比如有時候?qū)τ诰幗獯a異常,可以在統(tǒng)一處理異常處捕獲,快速定位問題。
5、小結(jié)
來簡單回顧下吧。
本文介紹了什么是ChannelHandler和ChannelPipeline。能厘清InboundChannelHandler、OutboundChannelHandler、ChannelHandlerContext是什么嗎?
然后對ChannelHandler的事件傳播機制、異常處理機制做了詳細(xì)介紹。
最后說明了日常開發(fā)中ChannelHandler的最佳實踐。
希望對大家有所幫助。