《Netty In Action中文版》第六章:ChannelHandler,
註:本篇內容出自《Netty In Action》一書;
註:本人原創譯文,轉載請註明出處!
本章介紹
- ChannelPipeline
- ChannelHandlerContext
- ChannelHandler
- Inbound vs outbound(入站和出站)
接受串連或建立他們只是你的應用程式的一部分,雖然這些任何很重要,但是一個網路應用程式WangWang是更複雜的,需要更多的代碼編寫,如處理傳入和傳出的資料。Netty提供了一個強大的處理這些事情的功能,允許使用者自訂ChannelHandler的實現來處理資料。使得ChannelHandler更強大的是可以串連每個ChannelHandler來實現任務,這有助於代碼的整潔和重用。但是處理資料只是ChannelHandler所做的事情之一,也可以壓制I/O操作,例如寫請求。所有這些都可以動態實現。
6.1 ChannelPipeline
ChannelPipeline是ChannelHandler執行個體的列表,用於處理或截獲通道的接收和發送資料。ChannelPipeline提供了一種進階的截取過濾器模式,讓使用者可以在ChannelPipeline中完全控制一個事件及如何處理ChannelHandler與ChannelPipeline的互動。 對於每個新的通道,會建立一個新的ChannelPipeline並附加至通道。一旦串連,Channel和ChannelPipeline之間的耦合是永久性的。Channel不能附加其他的ChannelPipeline或從ChannelPipeline分離。 描述了ChannelHandler在ChannelPipeline中的I/O處理,一個I/O操作可以由一個ChannelInboundHandler或ChannelOutboundHandler進行處理,並通過調用ChannelInboundHandler處理入站IO或通過ChannelOutboundHandler處理出站IO。
如所示,ChannelPipeline是ChannelHandler的一個列表;如果一個入站I/O事件被觸發,這個事件會從第一個開始依次通過ChannelPipeline中的ChannelHandler;若是一個入站I/O事件,則會從最後一個開始依次通過ChannelPipeline中的ChannelHandler。ChannelHandler可以處理事件並檢查類型,如果某個ChannelHandler不能處理則會跳過,並將事件傳遞到下一個ChannelHandler。ChannelPipeline可以動態添加、刪除、替換其中的ChannelHandler,這樣的機制可以提高靈活性。
修改ChannelPipeline的方法:
- addFirst(...),添加ChannelHandler在ChannelPipeline的第一個位置
- addBefore(...),在ChannelPipeline中指定的ChannelHandler名稱之前添加ChannelHandler
- addAfter(...),在ChannelPipeline中指定的ChannelHandler名稱之後添加ChannelHandler
- addLast(ChannelHandler...),在ChannelPipeline的末尾添加ChannelHandler
- remove(...),刪除ChannelPipeline中指定的ChannelHandler
- replace(...),替換ChannelPipeline中指定的ChannelHandler
ChannelPipeline pipeline = ch.pipeline();FirstHandler firstHandler = new FirstHandler();pipeline.addLast("handler1", firstHandler);pipeline.addFirst("handler2", new SecondHandler());pipeline.addLast("handler3", new ThirdHandler());pipeline.remove("handler3");pipeline.remove(firstHandler);pipeline.replace("handler2", "handler4", new FourthHandler()); 被添加到ChannelPipeline的ChannelHandler將通過IO-Thread處理事件,這意味了必須不能有其他的IO-Thread阻塞來影響IO的整體處理;有時候可能需要阻塞,例如JDBC。因此,Netty允許通過一個EventExecutorGroup到每一個ChannelPipeline.add*方法,自訂的事件會被包含在EventExecutorGroup中的EventExecutor來處理,預設的實現是DefaultEventExecutorGroup。 ChannelPipeline除了一些修改的方法,還有很多其他的方法,具體是方法及使用可以看API文檔或源碼。6.2 ChannelHandlerContext
每個ChannelHandler被添加到ChannelPipeline後,都會建立一個ChannelHandlerContext並與之建立的ChannelHandler關聯綁定。ChannelHandlerContext允許ChannelHandler與其他的ChannelHandler實現進行互動,這是相同ChannelPipeline的一部分。ChannelHandlerContext不會改變添加到其中的ChannelHandler,因此它是安全的。6.2.1 通知下一個ChannelHandler
在相同的ChannelPipeline中通過調用ChannelInboundHandler和ChannelOutboundHandler中各個方法中的一個方法來通知最近的handler,通知開始的地方取決你如何設定。顯示了ChannelHandlerContext、ChannelHandler、ChannelPipeline的關係:
如果你想有一些事件流全部通過ChannelPipeline,有兩個不同的方法可以做到:
- 調用Channel的方法
- 調用ChannelPipeline的方法
這兩個方法都可以讓事件流全部通過ChannelPipeline。無論從頭部還是尾部開始,因為它主要依賴於事件的性質。如果是一個“入站”事件,它開始於頭部;若是一個“出站”事件,則開始於尾部。 下面的代碼顯示了一個寫事件如何通過ChannelPipeline從尾部開始:
@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//Event via ChannelChannel channel = ctx.channel();channel.write(Unpooled.copiedBuffer("netty in action", CharsetUtil.UTF_8));//Event via ChannelPipelineChannelPipeline pipeline = ctx.pipeline();pipeline.write(Unpooled.copiedBuffer("netty in action", CharsetUtil.UTF_8));}});} 表示通過Channel或ChannelPipeline的通知:
可能你想從ChannelPipeline的指定位置開始,不想流經整個ChannelPipeline,如下情況:
- 為了節省開銷,不感興趣的ChannelHandler不讓通過
- 排除一些ChannelHandler
在這種情況下,你可以使用ChannelHandlerContext的ChannelHandler通知起點。它使用ChannelHandlerContext執行下一個ChannelHandler。下面代碼顯示了直接使用ChannelHandlerContext操作:
// Get reference of ChannelHandlerContextChannelHandlerContext ctx = ..;// Write buffer via ChannelHandlerContextctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));該訊息流程經ChannelPipeline到下一個ChannelHandler,在這種情況下使用ChannelHandlerContext開始下一個ChannelHandler。顯示了事件流:
如顯示的,從指定的ChannelHandlerContext開始,跳過前面所有的ChannelHandler,使用ChannelHandlerContext操作是常見的模式,最常用的是從ChannelHanlder叫用作業,也可以在外部使用ChannelHandlerContext,因為這是安全執行緒的。
6.2.2 修改ChannelPipeline
調用ChannelHandlerContext的pipeline()方法能訪問ChannelPipeline,能在運行時動態增加、刪除、替換ChannelPipeline中的ChannelHandler。可以保持ChannelHandlerContext供以後使用,如外部Handler方法觸發一個事件,甚至從一個不同的線程。 下面代碼顯示了儲存ChannelHandlerContext供之後使用或其他線程使用:
public class WriteHandler extends ChannelHandlerAdapter {private ChannelHandlerContext ctx;@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {this.ctx = ctx;}public void send(String msg){ctx.write(msg);}} 請注意,ChannelHandler執行個體如果帶有@Sharable註解則可以被添加到多個ChannelPipeline。也就是說單個ChannelHandler執行個體可以有多個ChannelHandlerContext,因此可以調用不同ChannelHandlerContext擷取同一個ChannelHandler。如果添加不帶@Sharable註解的ChannelHandler執行個體到多個ChannelPipeline則會拋出異常;使用@Sharable註解後的ChannelHandler必須在不同的線程和不同的通道上安全使用。怎麼是不安全的使用?看下面代碼:
@Sharablepublic class NotSharableHandler extends ChannelInboundHandlerAdapter {private int count;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {count++;System.out.println("channelRead(...) called the " + count + " time");ctx.fireChannelRead(msg);}}上面是一個帶@Sharable註解的Handler,它被多個線程使用時,裡面count是不安全的,會導致count值錯誤。
為什麼要共用ChannelHandler?使用@Sharable註解共用一個ChannelHandler在一些需求中還是有很好的作用的,如使用一個ChannelHandler來統計串連數或來處理一些全域資料等等。6.3 狀態模型
Netty有一個簡單但強大的狀態模型,並完美映射到ChannelInboundHandler的各個方法。下面是Channel生命週期四個不同的狀態:
- channelUnregistered
- channelRegistered
- channelActive
- channelInactive
Channel的狀態在其生命週期中變化,因為狀態變化需要觸發,顯示了Channel狀態變化:
還可以看到額外的狀態變化,因為使用者允許從EventLoop中登出Channel暫停事件執行,然後再重新註冊。在這種情況下,你會看到多個channelRegistered和channelUnregistered狀態的變化,而永遠只有一個channelActive和channelInactive的狀態,因為一個通道在其生命週期內只能串連一次,之後就會被回收;重新串連,則是建立一個新的通道。 顯示了從EventLoop中登出Channel後再重新註冊的狀態變化:
6.4 ChannelHandler和其子類
Netty中有3個實現了ChannelHandler介面的類,其中2個是介面,一個是抽象類別。如:
6.4.1 ChannelHandler中的方法
Netty定義了良好的類型階層來表示不同的處理常式類型,所有的類型的父類是ChannelHandler。ChannelHandler提供了在其生命週期內添加或從ChannelPipeline中刪除的方法。
- handlerAdded,ChannelHandler添加到實際上下文中準備處理事件
- handlerRemoved,將ChannelHandler從實際上下文中刪除,不再處理事件
- exceptionCaught,處理拋出的異常
上面三個方法都需要傳遞ChannelHandlerContext參數,每個ChannelHandler被添加到ChannelPipeline時會自動建立ChannelHandlerContext。ChannelHandlerContext允許在本地通道安全的儲存和檢索值。Netty還提供了一個實現了ChannelHandler的抽象類別:ChannelHandlerAdapter。ChannelHandlerAdapter實現了父類的所有方法,基本上就是傳遞事件到ChannelPipeline中的下一個ChannelHandler直到結束。
6.4.2 ChannelInboundHandler
ChannelInboundHandler提供了一些方法再接收資料或Channel狀態改變時被調用。下面是ChannelInboundHandler的一些方法:
- channelRegistered,ChannelHandlerContext的Channel被註冊到EventLoop;
- channelUnregistered,ChannelHandlerContext的Channel從EventLoop中登出
- channelActive,ChannelHandlerContext的Channel已啟用
- channelInactive,ChannelHanderContxt的Channel結束生命週期
- channelRead,從當前Channel的對端讀取訊息
- channelReadComplete,訊息讀取完成後執行
- userEventTriggered,一個使用者事件被處罰
- channelWritabilityChanged,改變通道的可寫狀態,可以使用Channel.isWritable()檢查
- exceptionCaught,重寫父類ChannelHandler的方法,處理異常
Netty提供了一個實現了ChannelInboundHandler介面並繼承ChannelHandlerAdapter的類:ChannelInboundHandlerAdapter。ChannelInboundHandlerAdapter實現了ChannelInboundHandler的所有方法,作用就是處理訊息並將訊息轉寄到ChannelPipeline中的下一個ChannelHandler。ChannelInboundHandlerAdapter的channelRead方法處理完訊息後不會自動釋放訊息,若想自動釋放收到的訊息,可以使用SimpleChannelInboundHandler<I>。 看下面代碼:
/** * 實現ChannelInboundHandlerAdapter的Handler,不會自動釋放接收的訊息對象 * @author c.k * */public class DiscardHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {//手動釋放訊息ReferenceCountUtil.release(msg);}}
/** * 繼承SimpleChannelInboundHandler,會自動釋放訊息對象 * @author c.k * */public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {//不需要手動釋放}} 如果需要其他狀態改變的通知,可以重寫Handler的其他方法。通常自訂訊息類型來解碼位元組,可以實現ChannelInboundHandler或ChannelInboundHandlerAdapter。有一個更好的解決方案,使用轉碼器的架構可以很容的實現。使用ChannelInboundHandler、ChannelInboundHandlerAdapter、SimpleChannelInboundhandler這三個中的一個來處理接收訊息,使用哪一個取決於需求;大多數時候使用SimpleChannelInboundHandler處理訊息,使用ChannelInboundHandlerAdapter處理其他的“入站”事件或狀態改變。
ChannelInitializer用來初始化ChannelHandler,將自訂的各種ChannelHandler添加到ChannelPipeline中。
6.4.3 ChannelOutboundHandler
ChannelOutboundHandler用來處理“出站”的資料訊息。ChannelOutboundHandler提供了下面一些方法:
- bind,Channel綁定本地地址
- connect,Channel串連操作
- disconnect,Channel中斷連線
- close,關閉Channel
- deregister,登出Channel
- read,讀取訊息,實際是截獲ChannelHandlerContext.read()
- write,寫操作,實際是通過ChannelPipeline寫訊息,Channel.flush()屬性到實際通道
- flush,重新整理訊息到通道
ChannelOutboundHandler是ChannelHandler的子類,實現了ChannelHandler的所有方法。所有最重要的方法採取ChannelPromise,因此一旦請求停止從ChannelPipeline轉寄參數則必須得到通知。Netty提供了ChannelOutboundHandler的實現:ChannelOutboundHandlerAdapter。ChannelOutboundHandlerAdapter實現了父類的所有方法,並且可以根據需要重寫感興趣的方法。所有這些方法的實現,在預設情況下,都是通過調用ChannelHandlerContext的方法將事件轉寄到ChannelPipeline中下一個ChannelHandler。 看下面的代碼:
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {ReferenceCountUtil.release(msg);promise.setSuccess();}} 重要的是要記得釋放致遠並直通ChannelPromise,若ChannelPromise沒有被通知可能會導致其中一個ChannelFutureListener不被通知去處理一個訊息。
如果訊息被消費並且沒有被傳遞到ChannelPipeline中的下一個ChannelOutboundHandler,那麼就需要調用ReferenceCountUtil.release(message)來釋放訊息資源。一旦訊息被傳遞到實際的通道,它會自動寫入訊息或在通道關閉是釋放。