(譯)ChannelHandler 和 ChannelPipeline

這一章涵蓋以下內(nèi)容:

  • ChannelHandler 和 ChannelPipeline的APIs介紹
  • 資源泄漏檢測
  • 異常處理
    在前一章節(jié)你已經(jīng)學習了ByteBuf——Netty的數(shù)據(jù)容器。隨著在這一章研究Netty的數(shù)據(jù)流和處理組件,我們將建立你所學到的知識,并且同時你將開始發(fā)現(xiàn)框架中的重要元素。
    你已經(jīng)知道一個Channel-Pipeline中可以聲明多個ChannelHandlers,以便于組織處理邏輯。我們將檢查各種包含這些classes和一個重要的關聯(lián)類——ChannelHandlerContext的用例。
    理解這些組件之間的關系對于利用Netty建立模塊化的、可重用的實現(xiàn)是至關重要的。

6.1 ChannelHandler家族

為了準備ChannelHandler的詳細學習,我們將花時間了解這部分涉及的Netty組件模型的一些基礎知識。

6.11 Channel生命周期

Channel接口定義了一個簡單但作用巨大的狀態(tài)模型,這個模型和ChannelInboundHandler API緊密關聯(lián)。Channel的4種狀態(tài)如表6.1所示。

表6.1 Channel生命周期狀態(tài)

狀態(tài) 描述
ChannelUnregistered Channel已創(chuàng)建,但沒有注冊到EventLoop.
ChannelRegistered Channel已注冊到EventLoop
ChannelActive Channel是活躍的(連上遠端)。它可以接收和發(fā)送數(shù)據(jù)。
ChannelInactive Channel沒有連上遠端

Channel的正常生命周期如圖6.1所示。隨著這些狀態(tài)變化的出現(xiàn),響應事件隨之生成。這些事件被轉發(fā)到ChannelPipeline中的ChannelHandlers來執(zhí)行。

圖6.1 Channel狀態(tài)模型

[圖片上傳失敗...(image-63378d-1537327307594)]

6.1.2 ChannelHandler生命周期

ChannelHandler生命周期的方法定義在ChannelHandler接口中,如表6.2所示,它們在ChannelHandler加入Channel-Pipeline或者從Channel-Pipeline移除之后被調(diào)用。每個方法傳入一個ChannelHandlerContext參數(shù)。

表6.2 ChannelHandler生命周期方法

類型 描述
handlerAdded 當ChannelHandler加入ChannelPipeline時被調(diào)用
handlerRemoved 當ChannelHandler從ChannelPipeline移除時被調(diào)用
exceptionCaught ChannelPipeline在處理過程中發(fā)生錯誤時被調(diào)用

Netty定義了以下兩個重要的ChannelHandler的子接口:

  • ChannelInboundHandler——處理各種入站數(shù)據(jù)和狀態(tài)變化
  • ChannelOutboundHandler——處理出站數(shù)據(jù)且允許所有操作打斷

下一節(jié)我們將詳細討論這些接口。

6.1.3 ChannelInboundHandler接口

表6.3列出了ChannelInboundHandler接口的生命周期方法。這些方法在數(shù)據(jù)接收或者關聯(lián)Channel的狀態(tài)改變時被調(diào)用。正如我們之前所提到的,這些方法緊密映射到Channel生命周期。
表6.3 ChannelInboundHandler方法

類型 描述
channelRegistered Channel注冊到EventLoop且能處理I/O時調(diào)用
channelUnregistered Channel從EventLoop注銷且不能處理任何I/O時調(diào)用
channelActive Channel活躍時調(diào)用;Channel已連接/綁定且準備好了。
channelInactive Channel不再是活躍狀態(tài)且不再連接遠端時調(diào)用
channelReadComplete Channel的讀操作已經(jīng)完成時調(diào)用
channelRead 如果數(shù)據(jù)從Channel讀取就調(diào)用
channelWritabilityChanged Channel的可寫性狀態(tài)改變時調(diào)用。用戶可以確保寫操作不會完成太快(以此避免OutOfMemoryError)或者當Channel變成可重寫時可以恢復寫操作。Channel類的isWritable()方法可以用來檢查channel的可寫性。可寫性的閾值可以通過Channel.config().setWriteHighWaterMark()和Channel.config().setWriteLowWaterMark()來設置。

當ChannelInboundHandler實現(xiàn)重寫channelRead()方法時,釋放與連接池ByteBuf實例相關的內(nèi)存是非常有必要的。Netty為此提供了一個工具方法,ReferenceCountUtil.release(),如下所示。

碼單6.1 釋放消息資源

@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead0(ChannelHandlerContext ctx,Object msg) {
    // No need to do anything special
    }
}

因為SimpleChannelInboundHandler自動釋放資源,你不必要為后面的調(diào)用存儲任何消息的引用,也就是說這些引用將變成無效。
章節(jié)6.1.6提供了關于引用處理的更詳細的討論。

6.1.4 ChannelOutboundHandler接口

出站的操作和和數(shù)據(jù)用ChannelOutboundHandler來處理。它的方法通過Channel, ChannelPipeline, 和 ChannelHandlerContext調(diào)用。
ChannelOutboundHandler的一項強大的功能是按需延遲操作或者事件,此功能允許高級的方法來請求處理。舉個例子,如果寫到遠端被暫停,你可以延遲沖洗操作,然后恢復他們。
表6.4展示了所有ChannelOutboundHandler自身定義的方法(遺漏那些從ChannelHandler繼承的方法)

表6.4 ChannelOutboundHandler 方法

類型 描述
bind(ChannelHandlerContext,SocketAddress,ChannelPromise) 在綁定Channel到本地地址時調(diào)用
connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise) 在請求連接Channel到遠端時調(diào)用
disconnect(ChannelHandlerContext,ChannelPromise) 在請求斷開Channel和遠端的連接時調(diào)用
close(ChannelHandlerContext,ChannelPromise) 在請求關閉Channel時調(diào)用
deregister(ChannelHandlerContext,ChannelPromise) 請求從EventLoop注銷Channel時調(diào)用
read(ChannelHandlerContext) 請求從Channel讀取更多數(shù)據(jù)時調(diào)用
flush(ChannelHandlerContext) 請求通過Channel刷新排隊的數(shù)據(jù)到遠端時調(diào)用
write(ChannelHandlerContext,Object,ChannelPromise) 請求通過Channel寫數(shù)據(jù)到遠端時調(diào)用

CHANNELPROMISE VS.CHANNELFUTURE
ChannelOutboundHandler的大部分方法帶了一個ChannelPromise參數(shù),當操作完成時用它進行通知。ChannelPromise是ChannelFuture的一個子接口,它定義了一些可寫方法,比如setSuccess()或setFailure(),所以使ChannelFuture不可變。

接下來我們將著眼于這些簡化寫ChannelHandlers任務的類。

6.1.5 ChannelHandler適配器

你可以使用類ChannelInboundHandlerAdapter和
ChannelOutboundHandlerAdapter作為你自定義的ChannelHandlers的出發(fā)點。這些適配器分別提供了 ChannelInboundHandler和ChannelOutboundHandlerxed的基本實現(xiàn)。他們通過繼承抽象類ChannelHandlerAdapter來獲取他們公共子接口的方法。最終的類層次結構如圖6.2所示。
圖6.2 ChannelHandlerAdapter類層次結構
[圖片上傳失敗...(image-221e87-1537327307594)]
ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter中提供的方法體在關聯(lián)的ChannelHandlerContext上調(diào)用相同的方法,從而將事件轉發(fā)給管道中的下一個ChannelHandler。

為了將這些適配類使用在你自己的攔截器中,簡單繼承他們并且重寫你想去自定義的方法。

6.1.6 資源管理

無論何時你通過調(diào)用ChannelInboundHandler.channelRead()或ChannelOutboundHandler.write()操作數(shù)據(jù),你需要確保沒有資源泄露。正如你可能記得的前面章節(jié)所述,Netty使用引用計數(shù)來處理連接池ByteBufs。因此在你使用完ByteBuf后調(diào)整引用計數(shù)是很重要的。

為了幫助你診斷潛在的問題,Netty提供了類Resource-LeakDetector(資源-泄露檢測器),它將取樣你應用的緩沖區(qū)分配的1%來檢查內(nèi)存泄露。涉及的開銷非常小。

如果檢測到泄露,將產(chǎn)生類似如下日志信息:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable
advanced leak reporting to find out where the leak occurred. To enable
advanced leak reporting, specify the JVM option
'-Dio.netty.leakDetectionLevel=ADVANCED' or call
ResourceLeakDetector.setLevel().

Netty 目前定義了4種泄露檢查級別,如表6.5所示。
表6.5 泄露-檢查 級別

級別 描述
DISABLED 不啟用泄露檢查,僅在大量的測試后使用此級別
SIMPLE 使用默認的1%取樣率,報告任何發(fā)現(xiàn)的泄露。這是默認的級別且對大部分場景都比較適用。
ADVANCED 報告發(fā)現(xiàn)的泄露和信息被訪問的地方。適用默認的取樣率。
PARANOID 類似ADVANCED,除了每一次訪問被取樣外。此級別對性能有很大的影響,一般僅僅在調(diào)試模式中使用。

泄露檢查級別通過將以下Java系統(tǒng)屬性設置成表格中的一個值來定義:

java -Dio.netty.leakDetectionLevel=ADVANCED

如果你設置了JVM條件后重新啟動你的應用,你會發(fā)現(xiàn)最近你應用被訪問的泄露緩沖區(qū)的位置。以下是通過單元測試生成的一份嚴重的泄露報告:

Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1
#1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString(
AdvancedLeakAwareByteBuf.java:697)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(
XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(
XmlFrameDecoderTest.java:133)
...

當你實現(xiàn)Channel-InboundHandler.channelRead()和
ChannelOutboundHandler.write()方法時,怎么使用這個診斷工具來防止泄露呢?讓我們檢查實例中你的channelRead()操作消費入站消息的地方;也就是說,不調(diào)用ChannelHandlerContext.fireChannelRead()方法來傳遞給下一個ChannelInboundHandler。下面這個代碼清單展示了如何釋放消息。

碼單 6.3 消費并釋放入站消息

@Sharable
public class DiscardInboundHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ReferenceCountUtil.release(msg);
    }
}

消費入站消息簡便方式
因為消費入站數(shù)據(jù)并釋放時一個公共的任務,Netty提供了一個特別的Channel-
InboundHandler實現(xiàn)——SimpleChannelInboundHandler。一旦消息通過channelRead0()被消費,這個實現(xiàn)將自動釋放消息。

在出站一端,如果你處理write()操作并丟棄一個消息,你必須釋放它。以下代碼清單展示了一個丟棄所有寫入數(shù)據(jù)的實現(xiàn)。

碼單 6.4 丟棄并釋放出站數(shù)據(jù)

@Sharable
public class DiscardOutboundHandler
extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx,
    Object msg, ChannelPromise promise) {
        ReferenceCountUtil.release(msg);
        promise.setSuccess();
    }
}

我們不但要釋放資源而且要通知ChannelPromise。否則可能會出現(xiàn)這種情況,ChannelFutureListener沒有接到消息已經(jīng)被處理的通知。

總而言之,如果消息被消費或者被丟棄且沒有傳遞給ChannelPipeline的下一個ChannelOutbound-
Handler,用戶有必要調(diào)用ReferenceCountUtil.release()。如果消息到達實際的傳輸層,當它被寫入或者Channel關閉時,它將自動釋放。

6.2 ChannelPipeline接口

如果你把ChannelPipeline當做攔截流通Channel的入站和出站事件的一個ChannelHandler實例鏈,那么這些ChannelHandlers的相互作用如何能構造核心的應用數(shù)據(jù)和事件處理邏輯是顯而易見的。

每一個新建的Channel會分配一個新的ChannelPipeline。這個關聯(lián)是永久的;Channel既不能連接到另一個ChannelPipeline,也不能斷開當前連接的ChannelPipeline。這是Netty組件生命周期的修復操作,不需要開發(fā)者進行操作。

根據(jù)其來源,一個事件會被ChannelInbound-
Handler或者ChannelOutboundHandler處理。隨后它將通過調(diào)用ChannelHandlerContext實現(xiàn),轉發(fā)給同一父類型的下一個攔截器。

ChannelHandlerContext
ChannelHandlerContext促使ChannelHandler和它的Channel-
Pipeline以及其他的攔截器相互作用。一個攔截器可以通知ChannelPipeline的下一個ChannelHandler,且動態(tài)修改它屬于的ChannelPipeline。
ChannelHandlerContext有豐富的API來處理事件和執(zhí)行I/O操作。章節(jié)6.3會提供更多關于ChannelHandlerContext的信息。
圖6.3 ChannelPipeline 和 ChannelHandlers
[圖片上傳失敗...(image-8cbef1-1537327307594)]
圖6.3用入站和出站ChannelHandlers圖解了一個典型的ChannelPipeline布局,并且圖解了我們之前的論述——ChannelPipeline根本上是一系列的ChannelHandlers。ChannelPipeline同樣提供通過ChannelPipeline自身傳播事件的方法。如果入站事件被觸發(fā),它會貫穿整個ChannelPipeline進行傳播。在圖6.3中,一個出站I/O事件將在ChannelPipeline右端開始,然后行進到左端。

ChannelPipeline 相對性
從事件通過Channel-Pipeline傳輸?shù)倪@一觀點上,你可能會認為其開端依賴于事件是入站還是出站。但是Netty一直證明ChannelPipeline的入站入口(圖6.3左端)作為開始,而出站入口(圖6.3右端)作為結束。
當你使用ChannelPipeline.add*()方法完成增加你的ChannelPipeline的入站和出站混合攔截器,每個ChannelHandler的順序是從開始到結束的位置,正如我們剛剛給他們定義的一樣。因此,如果你給圖6.3中的攔截器從左至右編號,第一個入站事件可見的Channel-Handler將是1;第一個出站事件可見的Channel-Handler將是5。

當pipline傳播一個事件時,它決定pipline的下一個ChannelHandler的類型是否匹配移動方向。如果不匹配,ChannelPipeline跳過此ChannelHandler并傳遞給下一個,直到它發(fā)現(xiàn)一個能匹配期望方向的ChannelHandler為止。(當然,一個攔截器可能實現(xiàn)ChannelInboundHandler 和ChannelOutboundHandler兩個接口。)

6.2.1 修改ChannelPipeline

ChannelHandler可以通過新增、移除、替換其他的ChannelHandlers,實時修改ChannelPipeline的布局。(它也可以從ChannelPipeline移除自身。)這是Channel-Handler最重要的功能,所以我們將仔細觀察它是怎么做的。相關的方法如表6.6所列:
表6.6 ChannelHandler修改ChannelPipeline方法

方法名 描述
addFirst/addBefore/addAfter/addLast 增加一個ChannelHandler到ChannelPipeline
remove 移除從ChannelPipeline移除一個ChannelHandler
replace 在ChannelPipeline中用一個ChannelHandler替換另一個ChannelHandler

以下代碼清單展示了這些方法的用途:
碼單 6.5 修改ChannelPipeline

ChannelPipeline 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());

你隨后會發(fā)現(xiàn)這項用來輕松重組ChannelHandlers的能力,適用于極其復雜邏輯的實現(xiàn)。

ChannelHandler執(zhí)行和阻塞
正常情況下,ChannelPipeline的每一個ChannelHandler處理通過它的EventLoop(I/O線程)傳遞給它的事件。千萬不要去阻塞這個線程,否則它會對全部的I/O處理有負面影響。
有時候結合使用阻塞APIs的遺留代碼可能是必須的。對于這個實例,ChannelPipeline擁有add()方法來接收一個Event-ExecutorGroup。如果一個事件被傳遞到一個自定義的EventExecutorGroup,它將被包含此EventExecutorGroup的EventExecutors中的一個處理,而且從它自身Channel的EventLoop被移除。對于這個使用場景,Netty提供了一個稱為DefaultEventExecutorGroup的實現(xiàn)。

除了這些操作,也有通過類型或者名稱訪問ChannelHandlers的其他操作。它們?nèi)绫?.7所列:

表 6.7 訪問ChannelHandlers的ChannelPipeline操作

方法名 描述
get 通過類型或名稱返回一個ChannelHandler
context 返回綁定到ChannelHandler的ChannelHandlerContext
names 返回此ChannelPipeline中所有ChannelHandlers的名稱

6.2.2 觸發(fā)事件

ChannelPipeline API暴露其他的方法來調(diào)用入站和出站操作。表6.8列出了通知Channel-InboundHandlers發(fā)生在ChannelPipeline的事件的入站操作。

表6.8 ChannelPipeline入站操作

方法名 描述
fireChannelRegistered 在ChannelPipeline的下一個ChannelInboundHandler調(diào)用channelRegistered(ChannelHandlerContext)
fireChannelUnregistered 在ChannelPipeline的下一個ChannelInboundHandler調(diào)用channelUnRegistered(ChannelHandlerContext)
fireChannelActive 在ChannelPipeline的下一個ChannelInboundHandler調(diào)用channelInactive(ChannelHandlerContext)
fireExceptionCaught 在ChannelPipeline的下一個ChannelHandler調(diào)用exceptionCaught(ChannelHandlerContext,Throwable)
fireUserEventTriggered 在ChannelPipeline的下一個ChannelInboundHandler調(diào)用userEventTriggered(ChannelHandler-Context, Object)
fireChannelRead 在ChannelPipeline的下一個ChannelInboundHandler調(diào)用channelRead(ChannelHandlerContext,Object msg)
fireChannelReadComplete 在ChannelPipeline的下一個ChannelStateHandler調(diào)用channelReadComplete(ChannelHandler-Context)

在出站方面,處理事件將導致底層socket執(zhí)行某些操作。表6.9列出了ChannelPipeline API的出站操作。

表6.9 ChannelPipeline出站操作

方法名 描述
bind 綁定Channel到本地地址。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用bind(Channel-HandlerContext, SocketAddress, ChannelPromise)
connect 連接Channel到遠程地址。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用connect(ChannelHandlerContext, SocketAddress,ChannelPromise)
disconnect 斷開Channel。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用disconnect(Channel-HandlerContext,ChannelPromise)
close 關閉Channel。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用close(ChannelHandlerContext,ChannelPromise)
deregister 從之前分配的EventExecutor(EventLoop)注銷Channel。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用deregister(ChannelHandler-Context, ChannelPromise)。
flush 刷新Channel的所有掛起寫入。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用flush(Channel-HandlerContext)。
write 寫消息到Channel。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用write(Channel-HandlerContext, Object msg, ChannelPromise)。注意:此方法不會寫消息到底層socket,僅僅對它排隊。如果要將消息寫到socket,調(diào)用flush或者writeAndFlush()。
writeAndFlush 對于調(diào)用write()然后調(diào)用flush()來說,這是一個簡便的方法。
read 請求從Channel讀取更多的數(shù)據(jù)。此方法會在ChannelPipeline的下一個ChannelOutboundHandler調(diào)用read(ChannelHandlerContext)。

總而言之:

  • ChannelPipeline持有關聯(lián)Channel的ChannelHandlers。
  • ChannelPipeline可以通過按需增加和移除ChannelHandlers動態(tài)修改。
  • ChannelPipeline有一套豐富的API來調(diào)用操作應對入站和出站事件。

6.3 ChannelHandlerContext接口

ChannelHandlerContext代表ChannelHandler和ChannelPipeline之間的關聯(lián),且無論何時ChannelHandler被加入到Channel-Pipeline就創(chuàng)建。ChannelHandlerContext的主要功能是管理同一個ChannelPipeline中它的關聯(lián)ChannelHandler和其他ChannelHandler之間的相互作用。
ChannelHandlerContext有很多方法,一些方法也在它自身Channel和ChannelPipeline中出現(xiàn),但是有重大的不同之處。如果你在Channel和ChannelPipline實例上調(diào)用這些方法,他們一直在管道上傳播。同樣的方法在ChannelHandlerContext上調(diào)用,將在當前關聯(lián)的ChannelHandler開始并僅僅傳播到管道的下一個有能力處理此事件的ChannelHandler。
表6.10總結了ChannelHandlerContext API

表 6.10 ChannelHandlerContext API

方法名 描述
bind 綁定給予的SocketAddress并返回ChannelFuture
channel 返回綁定到此實例的Channel
close 關閉Channel并返回ChannelFuture
connect 連接到給予的SocketAddress并返回ChannelFuture
deregister 從先前分配的EventExecutor注銷并返回ChannelFuture
disconnect 斷開遠端連接并返回ChannelFuture
executor 返回分發(fā)事件的EventExecutor
fireChannelActive 觸發(fā)下一個ChannelInboundHandler調(diào)用channelActive()(已連接)
fireChannelInactive 觸發(fā)下一個ChannelInboundHandler調(diào)用channelInactive()(已連接)
fireChannelRead 觸發(fā)下一個ChannelInboundHandler調(diào)用channelRead()(已連接)
fireChannelReadComplete 觸發(fā)下一個ChannelInboundHandler的Channel可寫性變化的事件
handler 返回綁定該實例的ChannelHandler
isRemoved 如果關聯(lián)ChannelHandler從ChannelPipeline移除則返回true
name 返回該實例唯一的名稱
pipline 返回關聯(lián)ChannelPipeline
read 從Channel讀取數(shù)據(jù)到第一個入站緩存區(qū);如果成功的話,觸發(fā)channelRead事件,然后通知channelReadComplete攔截器
write 利用該實例通過管道寫消息

當使用ChannelHandlerContext API,請記住以下幾點:

  • 關聯(lián)ChannelHandler的ChannelHandlerContext從不會改變,所以緩存它的引用是安全的。
  • ChannelHandlerContext方法,正如我們在這章節(jié)開始陳述的一樣,相比在其他類上使用相同名稱的方法,包含更短的事件流。我們應盡可能利用這一點來提供最大性能。

6.3.1 使用ChannelHandlerContext

在這一章節(jié)我們將討論ChannelHandlerContext的用法,ChannelHandlerContext、Channel和ChannelPipeline上可用方法的行為。圖6.4展示了他們之間的關系。

圖6.4 Channel, ChannelPipeline,ChannelHandler和ChannelHandlerContext之間的關系
[圖片上傳失敗...(image-278974-1537327307594)]
以下代碼清單展示了你從ChannelHandlerContext獲取Channel引用。在Channel上調(diào)用write()會導致一個流通整個管道的寫事件。

碼單6.6 從ChannelHandlerContext訪問Channel

ChannelHandlerContext ctx = ..;
Channel channel = ctx.channel();
channel.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));

以下代碼清單展示了一個相似的例子,但是這次寫到ChannelPipeline。同樣,從ChannelHandlerContext檢索此引用。
碼單6.7 從ChannelHandlerContext訪問ChannelPipeline

ChannelHandlerContext ctx = ..;
ChannelPipeline pipeline = ctx.pipeline();
pipeline.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));

正如你在圖6.5所見,代碼清單6.6和6.7中的流程是相同的。特別需要注意的是,盡管write()在Channel或ChannelPipeline操作一直通過管道傳播事件上被調(diào)用,ChannelHandlerContext調(diào)用從一個攔截器到下一個ChannelHandler級別的攔截器的運動。

圖6.5 事件通過Channel或ChannelPipeline傳播
[圖片上傳失敗...(image-1d038b-1537327307594)]
為什么你想在ChannelPipeline中特定點傳播事件呢?

  • 為了減少傳遞事件通過對它不感興趣的ChannelHandlers
  • 為了防止通過對事件感興趣的攔截器處理此事件

為了調(diào)用處理以特殊的ChannelHandler開始,你必須參考在此ChannelHandler之前的ChannelHandler關聯(lián)的ChannelHandlerContext。此ChannelHandlerContext將調(diào)用跟隨與之相關的那個ChannelHandler。
以下代碼清單和圖6.6闡明這種用法。

Listing 6.8 調(diào)用 ChannelHandlerContext write()

ChannelHandlerContext ctx = ..;
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

正如圖6.6所示,消息通過始于下一個ChannelHandler的ChannelPipeline流通,繞過所有在前的ChannelHandler。
我們剛剛描述的用例是一個普遍用例,對于在特殊ChannelHandler實例上調(diào)用操作,它是特別有用的。

圖 6.6 事件流操作通過ChannelHandlerContext觸發(fā)
[圖片上傳失敗...(image-93636f-1537327307594)]

6.3.2 ChannelHandler 和ChannelHandlerContext的高級用法

正如你在代碼清單6.6所見,你可以通過調(diào)用ChannelHandlerContext的pipeline()方法獲取封閉的ChannelPipeline。這使得管道的ChannelHandler的運行時操作成為可能,并可以利用它來實現(xiàn)復雜的設計。舉個例子,你可以增加ChannelHandler到管道來支持動態(tài)協(xié)議變更。

通過緩存ChannelHandlerContext的引用以供后續(xù)使用可以支持其他高級用法,這可能替換外面任何ChannelHandler方法,且甚至可能發(fā)源于一個不同的線程。以下代碼清單展示了使用這種模式觸發(fā)一個事件。

碼單 6.9 緩存ChannelHandlerContext

public class WriteHandler extends ChannelHandlerAdapter {
    private ChannelHandlerContext ctx;
    //Stores reference to ChannelHandlerContext for later use
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        this.ctx = ctx;
    }
    //Sends message using previously stored ChannelHandlerContext
    public void send(String msg) {
        ctx.writeAndFlush(msg);
    }
}

因為一個ChannelHandler可以屬于多個ChannelPipeline,所以它可以綁定到多個ChannelHandlerContext實例。一個ChannelHandler用于此用法必須以@Sharable注解;否則,嘗試把它增加到多個ChannelPipeline將觸發(fā)異常。顯而易見,為了安全使用多并發(fā)通道(也就是說,連接),比如一個ChannelHandler必須是線程安全的。
以下代碼清單占了此模式的一種正確實現(xiàn)。

碼單 6.10 共享ChannelHandler

@Sharable
public class SharableHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("Channel read message: " + msg);
        ctx.fireChannelRead(msg);
    }
}

前述的ChannelHandler實現(xiàn)滿足包含多管道在內(nèi)的所有需求;也就是使用@Sharable注解且不持有任何狀態(tài)。反之,以下代碼清單中的代碼會導致問題。

碼單 6.11 @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();
    }
}

這段代碼的問題在于它持有狀態(tài);也就是說記錄方法調(diào)用次數(shù)的實例變量count。當并發(fā)通道訪問它時,增加這個類實例到ChannelPipeline將很有可能產(chǎn)生錯誤。(當然,這個簡單案例可以通過同步channelRead()方法來糾正。)
總而言之,當且僅當你確認你的ChannelHandler是線程安全時才能使用@Sharable。

為什么共享CHANNELHANDLER?
一個普遍的原因是,在多個ChannelPipelines中建立單個的ChannelHandler來收集多個渠道的統(tǒng)計數(shù)據(jù)。

關于ChannelHandlerContext及其與其他框架組件的關系的討論到此結束。下面我們將研究異常處理。

6.4 異常處理

異常處理是任何大型應用的重要部分,且有各種實現(xiàn)方法。相應地,Netty為處理入站或者出站處理期間拋出的異常提供了幾種選擇。這一章節(jié)將幫助你理解如何設計最適合你需求的方法。

6.4.1 處理入站異常

如果一個異常在處理入站事件期間被拋出,那么它將從ChannelInboundHandler中觸發(fā)它的位置開始流經(jīng)ChannelPipeline。為了處理這樣的一個異常,你需要在你ChannelInboundHandler實現(xiàn)中重寫以下方法。

public void exceptionCaught(
ChannelHandlerContext ctx, Throwable cause) throws Exception

以下大媽清單展示了一個關閉Channel并打印異常堆棧記錄的簡單例子。

碼單 6.12 基本的入站異常處理

public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
    Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

因為異常會繼續(xù)沿著入站方向流動(恰如所有入站事件),實現(xiàn)上述邏輯的ChannelInboundHandler經(jīng)常位于ChannelPipeline的最后。這確保了所有入站異常總會被處理,不管他們發(fā)生在ChannelPipeline的任何位置。
你應該如何對異常作出反應,這很可能與你的應用相關。你可能想去關閉Channel(和連接)或者你可能嘗試去恢復它。如果你沒有實現(xiàn)任何處理入站異常(或者沒有消費異常),Netty將記錄異常沒有被處理的事實。

來總結一下,

  • ChannelHandler.exceptionCaught()的默認實現(xiàn)轉發(fā)當前異常到管道的下一個攔截器。
  • 如果一個異常到達管道的末端,它會被記錄為未處理的。
  • 重寫exceptionCaught()來定義特定的處理。然后,你決定是否將異常傳播到該點以外。

6.4.2 處理出站異常

在出站操作中處理正常完成和異常的選項基于以下通知機制:

  • 每一個出站操作返回一個ChannelFuture。當操作完成時,使用ChannelFuture注冊的ChannelFutureListeners會接到成功或者錯誤的通知。
  • ChannelPromise實例幾乎在所有ChannelOutboundHandler方法中傳遞。作為ChannelFuture的子類,ChannelPromise也可以分配用于異步通知的監(jiān)聽器。但ChannelPromise還有提供實時通知的可寫方法:
ChannelPromise setSuccess();
ChannelPromise setFailure(Throwable cause);

增加ChannelFutureListener意味著在ChannelFuture實例上調(diào)用addListener(ChannelFutureListener),有兩種實現(xiàn)方案。最普遍使用的一種方案是在入站操作(比如write())返回的ChannelFuture上調(diào)用addListener()。

以下代碼清單使用這種方案來增加一個打印堆棧記錄然后關閉Channel的ChannelFutureListener。
碼單 6.13 給ChannelFuture增加ChannelFutureListener

ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture f) {
        if (!f.isSuccess()) {
            f.cause().printStackTrace();
            f.channel().close();
        }
    }
});

第二種方案是給作為ChannelOutboundHandler方法參數(shù)傳遞的ChannelPromise增加ChannelFutureListener。如下所示的代碼和前面的代碼清單有同樣的效果。

碼單 6.14 給ChannelPromise增加ChannelFutureListener

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();
                }
            }
        });
    }
}

ChannelPromise可寫方法
通過在ChannelPromise上調(diào)用setSuccess()和setFailure(),一旦ChannelHandler方法返回調(diào)用者,你就可以知道操作狀態(tài)。

為什么選擇第一種方案而不是第二種?對于詳細的異常處理,你可能會發(fā)現(xiàn),當調(diào)用出站操作時,增加ChannelFutureListener更加合適,正如代碼清單6.13所示。對于稍不專業(yè)的處理異常方案,你可能發(fā)現(xiàn)自定義的如代碼清單6.14所示的ChannelOutboundHandler實現(xiàn)會更加簡單。
如果你的ChannelOutboundHandler自身拋出異常會發(fā)生什么?在這種情況,Netty自身將通知任何用相應的ChannelPromise注冊的監(jiān)聽器。

6.5 總結

這一章我們仔細研究了Netty數(shù)據(jù)處理組件——ChannelHandler。我們討論ChannelHandlers如何一起聲明,他們?nèi)绾我訡hannelInboundHandlers和ChannelOutboundHandlers來與Channelpipeline相互作用。
下一章將聚焦在Netty的編解碼器抽象上,相比于直接使用底層的ChannelHandler實現(xiàn),它能使編寫協(xié)議加密和解密更加簡單。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容