這一章涵蓋以下內(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é)議加密和解密更加簡單。