一、前言
前面學習了Netty的ByteBuf,接著學習ChannelHandler和ChannelPipeline。
二、ChannelHandler和ChannelPipeline
2.1 ChannelHandler
在ChannelPipeline中,ChannelHandler可以被鏈在一起處理使用者邏輯。
1. Channel生命週期
Channel介面定義了一個簡單但是強大的狀態模型,該模型與ChannelInboundHandler API緊密聯絡,Channel有如下四種狀態。
Channel的生命週期如下圖所示。
當狀態發生變化時,就會產生相應的事件。
2. ChannelHandler的生命週期
ChannelHandler定義的生命週期如下圖所示。
Netty定義了ChannelHandler的兩個重要的子類
· ChannelInboundHandler,處理各種入站的資料和狀態的變化。
· ChannelOutboundHandler,處理出站資料並允許攔截的所有操作。
3. ChannelInboundHandler介面
下圖展示了ChannelInboundHandler介面生命週期中的方法,當接受到資料或者其對應的Channel的狀態發生變化則會呼叫方法
當ChannelInboundHandler的實現覆蓋channelRead()方法時,它負責顯式釋放與池的ByteBuf例項相關聯的記憶體,可以使用ReferenceCountUtil.release() 方法進行釋放。如下程式碼展示了該方法的使用。
public class DiscardHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ReferenceCountUtil.release(msg); } }
上述顯式的釋放記憶體空間會顯得比較麻煩,而如下程式碼則無需顯式釋放記憶體空間。
@Sharable public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> { @Override public void channelRead0(ChannelHandlerContext ctx, Object msg) { // No need to do anything special } }
上述程式碼中,SimpleChannelInboundHandler會自動釋放資源,因此無需顯式釋放。
4. ChannelOutboundHandler介面
ChannelOutboundHandler處理出站操作和資料,它的方法會被Channel、ChannelPipeline、ChannelHandlerContext觸發。ChannelOutboundHandler可按需推遲操作或事件。例如對遠端主機的寫入被暫停,你可以延遲重新整理操作並在稍後重啟。
5. ChannelHandler介面卡
可以使用ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter類作為自己的ChannelHandler程式的起點,這些介面卡提供了ChannelInboundHandler和ChannelOutboundHandler的簡單實現,它們繼承了共同父類介面ChannelHandler的方法,其繼承結構如下圖所示
ChannelHandlerAdapter還提供了isSharable方法,如果有Sharable註釋則會返回true,這也表明它可以被新增至多個ChannelPipiline中。ChannelInboundHandlerAdapter and ChannelOutboundHandlerAdapter的方法體中會呼叫ChannelHandlerContext對應的方法,因此可以將事件傳遞到管道的下個ChannelHandler中。
6. 資源管理
無論何時呼叫ChannelInboundHandler.channelRead()和ChannelOutboundHandler.write()方法,都需要保證沒有資源洩露,由於Netty使用引用計數來管理ByteBuf,因此當使用完ByteBuf後需要調整引用計數。
為了診斷可能出現的問題,Netty提供了ResourceLeakDetector,它將抽取應用程式大約1%的緩衝區分配來檢查記憶體洩漏,其額外的開銷很小,記憶體檢測有如下四種級別
可以通過java -Dio.netty.leakDetectionLevel=ADVANCED 設定記憶體檢測級別。
當讀取資料時,可以在readChannel方法中呼叫ReferenceCountUtil.release(msg)方法釋放資源,或者實現SimpleChannelInboundHandler(會自動釋放資源);而當寫入資料時,可以在write方法中呼叫ReferenceCountUtil.release(msg)釋放資源,具體程式碼如下
@Sharable public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { ReferenceCountUtil.release(msg); promise.setSuccess(); } }
不僅需要釋放資源,並且需要通知ChannelPromise,否則ChannelFutureListener可能收不到事件已經被處理的通知。如果訊息到達實際的傳輸層,就可以在寫操作完成或者關閉通道時會自動釋放資源。
2.2 ChannelPipeline介面
如果將ChannelPipeline視為ChannelHandler例項鏈,可攔截流經通道的入站和出站事件,即可明白ChannelHandler之間的互動是如何構成應用程式資料和事件處理邏輯的核心的。當建立一個新的Channel時,都會分配了一個新的ChannelPipeline,該關聯是永久的,該通道既不能附加另一個ChannelPipeline也不能分離當前的ChannelPipeline。
一個事件要麼被ChannelInboundHander處理,要麼被ChannelOutboundHandler處理,隨後,它將通過呼叫ChannelHandlerContext的實現來將事件轉發至同一超型別的下一個處理器。ChannelHandlerContext允許ChannelHandler與其ChannelPipeline和其他ChannelHandler進行互動,一個處理器可以通知ChannelPipeline中的下一個處理器,甚至可以修改器隸屬於的ChannelPipeline。
下圖展示了ChannelHandlerPipeline、ChannelInboundHandler和ChannelOutboundHandler之間的關係
可以看到ChannelPipeline是由一系列ChannelHandlers組成,其還提供了通過自身傳播事件的方法,當進站事件觸發時,其從ChannelPipeline的頭部傳遞到尾部,而出站事件會從右邊傳遞到左邊。
當管道傳播事件時,其會確定下一個ChannelHandler的型別是否與移動方向匹配,若不匹配,則會跳過並尋找下一個,直至找到相匹配的ChannelHandler(一個處理器可以會同時實現ChannelInboundHandler和ChannelOutboundHandler)。
1. 修改ChannelPipeline
ChannelHandler可實時修改ChannelPipeline的佈局,如新增、刪除、替換其他ChannelHandler(其可從ChannelPipeline中移除自身),如如下圖所示的方法。
通常,ChannelPipeline中的每個ChannelHandler通過其EventLoop(I / O執行緒)處理傳遞給它的事件,不要阻塞該執行緒,因為它會對I/O的整體處理產生負面影響。
2.3 ChannelHandlerContext介面
ChannelHandlerContext代表了ChannelHandler與ChannelPipeline之間的關聯,當ChannelHandler被新增至ChannelPipeline中時其被建立,ChannelHandlerContext的主要功能是管理相關ChannelHandler與同一ChannelPipeline中的其他ChannelHandler的互動。
ChannelHandlerContext中存在很多方法,其中一些也存在於ChannelHandler和ChannelPipeline中,但是差別很大。如果在ChannelHandler或者ChannelPipeline中呼叫該方法,它們將在整個管道中傳播,而如果在ChannelHandlerContext中呼叫方法,那麼會僅僅傳遞至下個能處理該事件的ChannelHandler。
1. 使用ChannelHandlerContext
ChannelHandlerContext、ChannelHandler、ChannelHandlerContext、Channel之間的關係如下圖所示
可以通過ChannelHandlerContext來訪問Channel,並且當呼叫Channel的write方法時,寫事件會在管道中傳遞,程式碼如下
ChannelHandlerContext ctx = ..; Channel channel = ctx.channel(); channel.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
除了使用Channel的write方法寫入資料外,還可以使用ChannelPipeline的write方法寫入資料,程式碼如下
ChannelHandlerContext ctx = ..; ChannelPipeline pipeline = ctx.pipeline(); pipeline.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
上述兩段程式碼在管道中產生的效果相同,如下圖所示。
其中兩種方法的寫事件都會通過ChannelHandlerContext在管道中傳播。
若想從指定的ChannelHandler開始傳遞事件,那麼需要引用到指定ChannelHandler之前的一個ChannelHandlerContext,該ChannelHandlerContext將呼叫其關聯的ChannelHandler。
如下圖所示,展示了從指定ChannelHandler開始處理事件。
2. ChannelHandler和ChannelHandlerContext的高階用法
可以通過呼叫ChannelHandlerContext的pipeline方法獲得其對應的ChannelPipeline引用,這可以在執行中管理ChannelHandler,如新增一個ChannelHandler。
另一種高階用法是快取ChannelHandlerContext的引用,以供之後使用。如下程式碼展示了用法
public class WriteHandler extends ChannelHandlerAdapter { private ChannelHandlerContext ctx; @Override public void handlerAdded(ChannelHandlerContext ctx) { this.ctx = ctx; } public void send(String msg) { ctx.writeAndFlush(msg); } }
因為ChannelHandler可以屬於多個ChannelPipeline,所以它可以繫結到多個ChannelHandlerContext例項,當使用時必須使用@Sharable註釋,否則,當將其新增至多個ChannelPipeline時會丟擲異常。如下程式碼所示
@Sharable public class SharableHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.out.println("Channel read message: " + msg); ctx.fireChannelRead(msg); } }
@Sharable註釋沒有任何狀態,而如下程式碼會出現錯誤。
@Sharable public class UnsharableHandler extends ChannelInboundHandlerAdapter { private int count; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { count++; System.out.println("channelRead(...) called the " + count + " time"); ctx.fireChannelRead(msg(); } }
由於上述程式碼包含了狀態,即count計數,將此類的例項新增到ChannelPipeline時,在併發訪問通道時很可能會產生錯誤。可通過在channelRead方法中進行同步來避免錯誤。
2.4 異常處理
在出站和進站時,可能會發生異常,Netty提供了多種方法處理異常。
1. 處理進站異常
當處理進站事件時發生異常,它將從ChannelInboundHandler中被觸發的位置開始流過ChannelPipeline,為處理異常,需要在實現ChannelInboundHandler介面是重寫exceptionCaught方法。如下示例所示。
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
由於異常會隨著進站事件在管道中傳遞,包含異常處理的ChannelHandler通常放在了管道的尾部,因此可以保證無論異常發生在哪個ChannelHandler中,其最終都會被處理。
2. 處理出站異常
在出站操作中處理的正常完成和處理異常都基於以下通知機制。
· 每個出站操作返回一個ChannelFuture,在ChannelFuture註冊的ChannelFutureListeners在操作完成時通知成功或錯誤。
· ChannelOutboundHandler的幾乎所有方法都會傳遞ChannelPromise例項,ChannelPromise是ChannelFuture的子類,其也可以為非同步通知分配監聽器,並ChannelPromise還提供可寫的方法來提供即時通知。可通過呼叫ChannelFuture例項的addListener(ChannelFutureListener)方法新增一個ChannelFutureListener,最常用的是呼叫出站操作所返回的ChannelFuture的addListener方法,如write方法,具體程式碼如下所示。
ChannelFuture future = channel.write(someMessage); future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture f) { if (!f.isSuccess()) { f.cause().printStackTrace(); f.channel().close(); } } });
第二種方法是將ChannelFutureListener新增到ChannelPromise中,並將其作為引數傳遞給ChannelOutboundHandler的方法,具體程式碼如下所示
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture f) { if (!f.isSuccess()) { f.cause().printStackTrace(); f.channel().close(); } } }); } }
三、總結
本篇博文講解了ChannelHandler,以及其與ChannelPipeline、ChannelHandlerContext之間的關係,及其之間的互動,同時還了解了如何處理進站與出站時所丟擲的異常。謝謝各位園友的觀看~