本文是Netty文集中“Netty in action”系列的文章。主要是對Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一書簡要翻譯,同時對重要點加上一些自己補充和擴展。
概要
- ChannelHandler 和 ChannelPipeline 的 API
- 資源泄漏檢測
- 異常處理
The ChannelHandler family
Channel生命周期
Channel接口定義了簡單但強大的狀態模式來緊密的聯系ChannelInboundHandler API。包括了4種狀態
Channel正常生命周期狀態改變如下圖:
ChannelHandler的生命周期
當一個ChannelHandler添加到一個ChannelPipeline或從一個ChannelPipeline中移除時,會調用ChannelHandler生命周期中相應的方法。每個方法都會接收一個ChannelHandlerContext參數。
注意,Channel生命周期狀態改變時會有相應的事件產生,并且該事件會在ChannelPipeline中的ChannelHandlers傳遞。但,ChannelHandler生命周期狀態改變時并不會有相應的事件產生與ChannelPipeline中傳播,只有回調當前這個ChannelHandler的某個方法而已。這是很好理解的,因為ChannelPipeline中所有的ChannelHandler都是服務于一個Channel的,因此Channel的狀態改變了,ChannelPipeline中的ChannelHandler都有可能需要做出相應的處理。而ChannelHandler自身的狀態和其他ChannelHandler并無關系,所以ChannelHandler狀態發生改變時,會有當前這個ChannelHandler相應的方法會被回調而已。
Netty定義了兩個非常重要的ChannelHandler的子接口:
- ChannelInboundHandler —— 處理入站數據和所有類型狀態的改變
- ChannelOutboundHandler —— 處理所有的出站數據和允許攔截所有的操作。
ChannelInboundHandler 接口
下表列出了ChannelInboundHandler接口生命周期的所有方法。這些方法將被調用在接收數據或者Channel相關狀態改變時。正如我們早前提及的,這些方法與Channel的生命周期緊密映射。
當一個ChannelInboundHandler的實現重寫了channelRead(),它需要負責明確釋放ByteBuf實例相關的內存。為了達到這個目的,Netty提供了一個實用的方法:ReferenceCountUtil.release()
Netty會通過警告級別的日志消息打印未釋放的資源,這將能夠簡單的從代碼中尋找出未釋放的實例。但是通過這種方式管理資源是非常笨拙的。一個簡單的替換方式是使用SimpleChannelInboundHandler,如下所示
ChannelOutboundHandler 接口
通過ChannelOutboundHandler進行出站的操作和數據的處理。ChannelOutboundHandler的方法可以通過Channel、ChannelPipeline、ChannelHandlerContext調用。
ChannelOutboundHandler有一個強大的功能是根據需要可延遲一個操作或事件,這將允許通過一個復雜的方式去處理請求。例如,如果寫數據到遠端被暫停了,你能夠延遲刷新操作并稍后恢復寫操作。
ChannelPromise VS ChannelFuture
ChannelOutboundHandler的許多方法都會接受一個ChannelPromise參數用于當操作完成時得到一個通知。ChannelPromise是ChannelFuture的一個子類,ChannelPromise定義了可寫入的方法,比如:setSuccess()或setFailure(),它使得ChannelFuture不可變( 因為,方法參數使用final域修飾,如下例子所示 )。
也就是說,ChannelPromise是一個特殊的ChannelFuture,ChannelPromise是可寫的。從源碼上看,ChannelOutboundHandler的write方法返回的ChannelFuture實際上就是ChannelPromise,ChannelPromise會在ChannelPipeline中傳遞。
ChannelHandler 適配器
你能夠使用ChannelInboundHandlerAdapter類和ChannelOutboundHandlerAdapter類作為你自己ChannelHandlers的入口。這兩個適配器分別提供了ChannelInboundHandler和ChannelOutboundHandler的基本實現。ChannelHandlerAdapter提供了一個非常實用的方法isSharable()。如果ChannelHandlerAdapter的實現類被注解了"@Sharable”,那么該方法將返回true,表明著該實現類實例能被添加到多個ChannelPipeline中,或者可以被添加到同個ChannelPipeline中多次。
ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter中方法通過調用ChannelHandlerContext等價的方法來將事件傳遞給pipeline中的下一個ChannelHandler。如:
你自己的處理器可以簡單的實現適配器類,然后重寫你想要自定義的方法。
資源管理
無論你通過調用 ChannelInboundHandler.channelRead() 或 ChannelOutboundHandler.writer() 來處理你的數據,你需要確保沒有資源的泄漏。
Netty使用引用計數來處理ByteBufs。所以在你使用完一個ByteBuf時去調整引用計數是非常重要的。
為了幫助你診斷可能存在的問題,Netty提供了ResourceLeakDetector類,它將獲取你應用已經分配的緩存中的1%數據作為采樣來進行內存泄漏的檢查。它的開銷是非常小的。
泄漏檢查等級的設置是通過在下面的java系統屬性來設置的:java -Dio.netty.leakDetectionLevel=ADVANCED
或者通過調用ResourceLeakDetector.setLevel()方法來設置。
當入站消息不再傳遞給下一個ChannelInboundHandler時,通過ReferenceCountUtil.release(msg);來釋放資源。
注意,這里不僅需要釋放資源,還需要通知ChannelPromise(即,上面的例子是通過調用promise.setSuccess()來通知異步的write操作已經成功完成)。
另一方面,還一種情況可能出現:ChannelFutureListener可能沒有收到關于一個消息已經被處理的通知。
總而言之,用戶有責任去通過調用ReferenceCountUtil.release()來釋放一個已經被消費的消息或廢棄并不會傳遞給ChannelPipeline中下一個ChannelOutboundHandler的消息。如果消息到真實的傳輸層,當他寫完或Channel被關閉時將會被自動釋放。
重要:
-
內存泄漏針對于使用了池的ByteBuf,在從池中分配完ByteBuf后使用完又沒有放回到池中。如,從池中分配ByteBuf,ctx.alloc()默認返回PooledByteBufAllocator:
而在使用EmbeddedChannel測試入站操作時,直接將rep傳給writeAndFlush(…)也是可以測出內存泄漏的,因為EmbeddedChannel測試入站操作時沒有走出站流程,所以就導致從池中分配的ByteBuf一直無法得到釋放。
內存泄漏報告: -
writeAndFlush(Bytebuf buf),如果是EmbeddedChannel單元測試且測試入站數據時,需要手動釋放資源
Q:所以如果是涉及到I/O操作的就不需要用戶去釋放出站資源了嗎?因為數據在網絡層傳輸出去后會被自動釋放?
A:展開下encoder的底層:
MessageToMessageEncoder:如果有執行encode(…)函數,則會在finally中釋放資源,因為調用了encode函數則會生成新的ByteBuf或其他對象傳遞給下一個Encoder,那么當前傳入的msg就已經不會再使用了,所以需要釋放msg。
如果沒有執行encode(…)函數,則直接將msg傳給下一個Encoder,由下一個Encoder執行一樣的操作。
MessageToByteEncoder:也會有相關的釋放資源的操作。
ChannelPipeline 接口
每一個新的Channel被創建時都會分配一個新的ChannelPipeline。他們的關系是永久不變的,Channel既不能依附于其他ChannelPipeline也不能和當前ChannelPipeline分離。這是一個固定的操作在Netty組件的生命周期中并且不需要開發者做任何的額外的操作。
一個事件要么被一個ChannelInboundHandler處理要么被一個ChannelOutboundHandler處理。隨后該事件通過一個ChannelHandlerContext來實現傳遞給下一個具有一樣父類的處理器,即同一個方向的處理器。
ChannelHandlerContext
ChannelHandlerContext使一個ChannelHandler能和它所在的ChannelPipeline中的其他的ChannelHandler交互。一個處理器能夠通知ChannelPipeline中下一個ChannelHandler并且能夠動態修改它所屬的ChannelPipeline。
也就是說,事件是通過ChannelHandlerContext在ChannelPipeline中的ChannelHandler傳遞的。處理器在獲得ChannelPipeline后,可以對其進行動態修改,即修改后即生效。
ChannelHandler在ChannelPipeline中的順序是由我們通過ChannelPipeline.add*()方法來確定的。
當管道傳播一個事件時,它會確定是否管道中下一個ChannelHandler符合移動的方向。如果不符合,ChannelPipeline會跳過這個ChannelHandler并繼續操作下一個,直到它找到一個符合它想要的方向的ChannelHandler。(當然,一個handler可能同時實現了ChannelInboundHandler和ChannelOutboundHandler接口)
修改一個ChannelPipeline
ChannelHandler能夠實時修改ChannelPipeline的布局,通過添加、刪除、替換為其他ChannelHandlers ( 它也能將自己從ChannelPipeline中移除 )。這是ChannelHandler非常重要的功能。ChannelHandler 的執行和阻塞
通常,ChannelPipeline中的每個ChannelHandler將事件傳遞給它所在的EventLoop ( I/O線程 )進行處理。非常重要的是不要阻塞這個線程,這樣做會對該線程上所有的I/O處理造成負面影響。
有時候我們可能需要對接使用了阻塞的遺留代碼。在這種情況下,可以通過ChannelPipeline的add()方法來接收一個EventExecutorGroup參數。如果一個事件被傳給了一個自定義的EventExecutorGroup,它將被這個EventExecutorGroup中的某個EventExecutor所處理,并且會從Channel所在的EventLoop中移除。對于這種使用情況Netty提供了一個叫做DefaultEventExecutorGroup的實現。
在通過ChannelHandlerContext將事件傳播給下一個ChannelHandler的時候,又會依據下一個ChannelHandler關聯的ChannelHandlerContext的executor()獲得執行ChannelHandler的執行器,如果在將ChannelHandler添加到ChannelPipeline中時沒有指定執行的EventExecutorGroup,那么默認就是在Channel所注冊到的EventLoop所在線程上執行。
事件觸發
ChannelPipeline API 暴露了附加的方法用于調用入站和出站的操作。總結:
- 一個ChannelPipeline持有一個Channel相關的所有ChannelHandlers。
- 能夠根據需要通過添加和刪除來動態修改一個ChannelPipeline。
- ChannelPipeline擁有豐富的API用于調用action來回應入站和出站事件。
ChannelHandlerContext 接口
一個ChannelHandlerContext代表一個ChannelHandler和一個ChannelPipeline之間的關聯,并且無論何時一個ChannelHandler被添加到一個ChannelPipeline時它都會被創建。ChannelHandlerContext一個主要的功能是管理與它相關的ChannelHandler在ChannelPipeline中與其他ChannelHandler間的交互。
ChannelHandlerContext有許多的方法,有些方法也出現在了Channel和ChannelPipeline類中,但是它們有著很重要的不同處。如果你通過一個Channel實例或ChannelPipeline實例調用這些方法,它們將會傳播通過整個管道。但相同的方法通過ChannelHandlerContext被調用時,它將從當前關聯的ChannelHandler開始并傳播給管道中下一個能夠處理該事件的ChannelHandler。
也就是說,ChannelPipeline和Channel中調用的方法都會通過整個管道;而ChannelHandlerContext調用的方法會從當前ChannelHandler對應方向的下一個ChannelHandler開始執行( 當前ChannelHandler也不會處理該事件 )。
從源碼上看write操作是當前ChannelHandler對應的下一個ChannelOutboundHandler開始處理寫操作,注意,出站的方向是從tail -> head;
Q:從??的API來看,read方法是會從pipeline中第一個入站緩沖區,read是通過整個pipeline嗎?
A:是的,Channel會將每次讀取到的數據傳到pipeline中。注意,這里是每一次讀到的數據,而不是讀到完整的消息或全部讀完數據,所以才有后面需要handler來解析收到的數組以組裝成一個消息。
ChannelPipeline中的head的invokeChannelRead(..)的操作就是執行AbstractChannelHandlerContext的fireChannelRead方法,將msg傳遞給pipeline中的下個ChannelInboundHandler。
當使用ChannelHandlerContext API時請注意下面幾點:
- ChannelHandlerContext關聯的ChannelHandler是不會改變的,所以緩存ChannelHandlerContext引用是安全的。
- 與其他類中有著相同名字的有效方法比較,ChannelHandlerContext方法提供了一個更短的事件流。這可被利用于提供一個更好的性能。
ChannelHandlerContext 的使用
上面兩個例子是相同的。這里重要的是要注意,當通過Channel或ChannelPipeline調用write()的時候,事件會傳播通過整個管道。它們通過ChannelHandlerContext將事件從一個處理器傳遞到下一個處理器。
為什么你想要傳播一個事件從ChannelPipeline中的指定的某一點開始?
- 減小傳遞事件通過不感興趣的ChannelHandlers的開銷。即,通過事件不傳遞給不感興趣的ChannelHandler來減小開銷。
- 為了防止對事件感興趣的處理器處理事件
為了調用程序從一個指定的ChannelHandler開始,你必須引用一個指定ChannelHandler前一個的ChannelHandler相關聯的ChannelHandlerContext。
??這里再次說明,通過ChannelHandlerContext調用方法時,當前ChannelHandler是不會對事件進行處理的,而是從下一個ChannelHandler開始對事件進行處理。
ChannelHandler 和 ChannelHandlerContext 的高級用法
你能夠通過ChannelHandlerContext的pipeline()方法獲取的一個ChannelPipeline引用。它能在運行時操作管道中的ChannelHandlers,這能夠被利用與實現復雜的設計。比如,你能夠添加一個ChannelHandler到一個管道以支持一個動態協議的改變。
另一個高級用法是支持緩存一個ChannelHandlerContext引用,用于稍后使用,該引用能在任何ChannelHandler以外的方法中使用,甚至可以在不同的線程中使用。
因為一個ChannelHandler能夠屬于多個ChannelPipeline,它能夠被綁定到多個ChannelHandlerContext實例上。一個用于該目的的ChannelHandler必須有@Sharable注解;如果一個不具有@Sharable注解的ChannelHandler被嘗試去添加到多個ChannelPipeline中將會觸發一個異常。顯然,為了安全的在多個并發Channel中使用ChannelHandler,可共享的ChannelHandler必須是線程安全的。
為什么要共享一個ChannelHandler ? 一個常見的原因是:構建一個單例的ChannelHandler在多個ChannelPipeline中為了跨越多個Channels來收集統計資料。
ChannelHandler、ChannelHandlerContext 補充:
如果一個ChannelHandler被標識為了’Sharable’的,并且“該ChannelHandler對象被添加到一個ChannelPipeline中多次” 或者 “該ChannelHandler對象被添加到多個ChannelPipeline中一次或者多次” 都會導致該單一ChannelHandler擁有了多個ChannelHandlerContext。
當然,如果你添加一個非共享(即,未被標示為’Sharable’)的ChannelHandle到一個ChannelPipeline多次或則添加到多個ChannelPipeline中都會導致拋出一個ChannelPipelineException異常。
異常的處理
異常處理是非常重要的部分在任何實質應用中,并且它能通過多種方式進行處理。因此,Netty提供了幾種選擇用于處理異常的拋出在入站或出站處理中。
處理入站異常
如果一個異常在處理一個入站事件期間被拋出,它將從被觸發該異常的ChannelInboundHandler所在的位置開始流經ChannelPipeline。
Q:這句話的意思是異常也是從拋出異常的當前handler開始處理,然后傳遞到管道中的下一個ChannelInboundHandler?
A:是的。異常會從當前的handler開始處理,如果當前的ChannelHandler的exceptionCaught方法有調用‘ctx.fireExceptionCaught(cause);’或者‘super.exceptionCaught(ctx, cause);[其底層就是調用ctx.fireExceptionCaught(cause)]’就會將異常傳遞給管道中的一個ChannelInboundHandler
因為異常將繼續流進入站方向的ChannelHandler( 就和其他入站事件一樣 ),實現該邏輯的ChannelInboundHandler通常位于ChannelPipeline中的最后面( 即,最后一個ChannelInboundHandler )。這能確保所有入站中的異常總能被處理,無論該異常在ChannelPipeline中的哪里發生。
如何應對一個異常可能和你的應用的具體情況而定。你可能想要關閉這個channel或者你可能試圖恢復這個channel。如果你沒有實現任何入站異常的處理( 或者說,沒有消費任何異常 ),Netty將日志記錄未處理異常的真實情況。
總結:
- ChannelHandler.exceptionCaught()方法的默認實現是傳遞當前異常到管道中的下一個處理器中。
- 如果一個異常到達了管道的結尾,該異常將被記錄為未處理。
- 你可以通過重寫exceptionCaught()方法來自定義異常處理。然后你能夠覺得是否要讓該異常跨過該點( 即,是否需要將該異常傳遞到管道中的下一個處理器中 )。
處理出站異常
在出站操作中處理正常完成和異常的選項是基于下面的通知機制:
- 每一個出站操作將返回一個ChannelFuture。注冊ChannelFutureListeners到一個ChannelFuture中,無論該事件成功與否ChannelFutureListeners在事件完成時都將得到一個通知。
-
幾乎所有的ChannelOutboundHandler方法都會傳遞一個ChannelPromise實例( 作為方法的參數來傳遞 )。作為ChannelFuture的子類,ChannelPromise也能給指定的監聽進行異步事件的通知。但是ChannelPromise還提供了可寫的方法來提供即時通知:
6.14示例和6.13示例是等價的。 但使用6.14示例的方式來處理異常不如6.13示例來的專業,而6.14示例通過自定義OutboundExceptionHandler方式處理異常雖然不專業但實現更簡單。
總的來說,入站操作異常由exceptionCaught()方法處理;出站異常由ChannelFuture處理。
出站異常示例:
控制臺:
ChannelPromise 的可寫方法
通過調用ChannelPromise的setSuccess()和setFailure()方法,你能立即得知操作的狀態在該ChannelHandler方法返回給調用者后。
我想這里想要表述的是,一旦調用了ChannelPromise的setSuccess()和setFailure()方法,則說明當前的異步操作就完成了。所以在ChannelPromise的setSuccess()和setFailure()方法調用后,你就能夠得到異步操作的狀態了。
如果你的ChannelOutboundHandler自己拋出了一個異常,那么將會發生什么?在這種情況下,Netty將會通知所有已經注冊到相應ChannelPromise的監聽器
后記
本文主要對Netty的ChannelHandler和ChannelPipeline進行了介紹,這兩個都是是Netty中非常重要的組件,也涉及到了不少的知識點。
若文章有任何錯誤,望大家不吝指教:)