Netty那點事(3)Channel與Pipeline
來自: http://www.importnew.com/17676.html
本系列:
Channel是理解和使用 Netty 的核心。Channel的涉及內容較多,這里我使用由淺入深的介紹方法。在這篇文章中,我們主要介紹Channel部分中Pipeline實現機制。為了避免枯燥,借用一下《盜夢空間》的“夢境”概念,希望大家喜歡。
一層夢境:Channel實現概覽
在Netty里, Channel 是通訊的載體,而 ChannelHandler 負責Channel中的邏輯處理。
那么 ChannelPipeline 是什么呢?我覺得可以理解為ChannelHandler的容器:一個Channel包含一個ChannelPipeline,所有ChannelHandler都會注冊到ChannelPipeline中,并按順序組織起來。
在Netty中, ChannelEvent 是數據或者狀態的載體,例如傳輸的數據對應 MessageEvent ,狀態的改變對應 ChannelStateEvent 。當對Channel進行操作時,會產生一個ChannelEvent,并發送到 ChannelPipeline 。ChannelPipeline會選擇一個ChannelHandler進行處理。這個ChannelHandler處理之后,可能會產生新的ChannelEvent,并流轉到下一個ChannelHandler。
例如,一個數據最開始是一個 MessageEvent ,它附帶了一個未解碼的原始二進制消息 ChannelBuffer ,然后某個Handler將其解碼成了一個數據對象,并生成了一個新的 MessageEvent ,并傳遞給下一步進行處理。
到了這里,可以看到,其實Channel的核心流程位于 ChannelPipeline 中。于是我們進入ChannelPipeline的深層夢境里,來看看它具體的實現。
二層夢境:ChannelPipeline的主流程
Netty的ChannelPipeline包含兩條線路:Upstream和Downstream。Upstream對應上行,接收到的消息、被動的狀態改變,都屬于Upstream。Downstream則對應下行,發送的消息、主動的狀態改變,都屬于Downstream。 ChannelPipeline 接口包含了兩個重要的方法: sendUpstream(ChannelEvent e) 和 sendDownstream(ChannelEvent e) ,就分別對應了Upstream和Downstream。
對應的,ChannelPipeline里包含的ChannelHandler也包含兩類: ChannelUpstreamHandler 和 ChannelDownstreamHandler 。每條線路的Handler是互相獨立的。它們都很簡單的只包含一個方法: ChannelUpstreamHandler.handleUpstream 和 ChannelDownstreamHandler.handleDownstream 。
Netty官方的javadoc里有一張圖( ChannelPipeline 接口里),非常形象的說明了這個機制(我對原圖進行了一點修改,加上了 ChannelSink ,因為我覺得這部分對理解代碼流程會有些幫助):
什么叫 ChannelSink 呢?ChannelSink包含一個重要方法 ChannelSink.eventSunk ,可以接受任意ChannelEvent。“sink”的意思是”下沉”,那么”ChannelSink”好像可以理解為”Channel下沉的地方”?實際上,它的作用確實是這樣,也可以換個說法:“處于末尾的萬能Handler”。最初讀到這里,也有些困惑,這么理解之后,就感覺簡單許多。 只有Downstream包含 ChannelSink ,這里會做一些建立連接、綁定端口等重要操作。為什么UploadStream沒有ChannelSink呢?我只能認為,一方面,不符合”sink”的意義,另一方面,也沒有什么處理好做的吧!
這里有個值得注意的地方:在一條“流”里,一個 ChannelEvent 并不會主動的”流”經所有的Handler,而是由 上一個Handler顯式的調用 ChannelPipeline.sendUp(Down)stream 產生,并交給下一個Handler處理 。也就是說,每個Handler接收到一個ChannelEvent,并處理結束后,如果需要繼續處理,那么它需要調用 sendUp(Down)stream 新發起一個事件。如果它不再發起事件,那么處理就到此結束,即使它后面仍然有Handler沒有執行。這個機制可以保證最大的靈活性,當然對Handler的先后順序也有了更嚴格的要求。
順便說一句,在Netty 3.x里,這個機制會導致大量的ChannelEvent對象創建,因此Netty 4.x版本對此進行了改進。推ter的 finagle 框架實踐中,就提到從Netty 3.x升級到Netty 4.x,可以大大降低GC開銷。有興趣的可以看看這篇文章: https://blog.推ter.com/2013/netty-4-at-推ter-reduced-gc-overhead
下面我們從代碼層面來對這里面發生的事情進行深入分析,這部分涉及到一些細節,需要打開項目源碼,對照來看,會比較有收獲。
三層夢境:深入ChannelPipeline內部
DefaultChannelPipeline的內部結構
ChannelPipeline 的主要的實現代碼在 DefaultChannelPipeline 類里。列一下DefaultChannelPipeline的主要字段:
public class DefaultChannelPipeline implements ChannelPipeline {private volatile Channel channel; private volatile ChannelSink sink; private volatile DefaultChannelHandlerContext head; private volatile DefaultChannelHandlerContext tail; private final Map<String, DefaultChannelHandlerContext> name2ctx = new HashMap<String, DefaultChannelHandlerContext>(4); }</pre>
這里需要介紹一下 ChannelHandlerContext 這個接口。顧名思義,ChannelHandlerContext保存了Netty與Handler相關的的上下文信息。而咱們這里的 DefaultChannelHandlerContext ,則是對 ChannelHandler 的一個包裝。一個 DefaultChannelHandlerContext 內部,除了包含一個 ChannelHandler ,還保存了”next”和”prev”兩個指針,從而形成一個雙向鏈表。
因此,在 DefaultChannelPipeline 中,我們看到的是對 DefaultChannelHandlerContext 的引用,而不是對 ChannelHandler 的直接引用。這里包含”head”和”tail”兩個引用,分別指向鏈表的頭和尾。而name2ctx則是一個按名字索引DefaultChannelHandlerContext用戶的一個map,主要在按照名稱刪除或者添加ChannelHandler時使用。
sendUpstream和sendDownstream
前面提到了, ChannelPipeline 接口的兩個重要的方法: sendUpstream(ChannelEvent e) 和 sendDownstream(ChannelEvent e) 。 所有事件 的發起都是基于這兩個方法進行的。 Channels 類有一系列 fireChannelBound 之類的 fireXXXX 方法,其實都是對這兩個方法的facade包裝。
下面來看一下這兩個方法的實現(對代碼做了一些簡化,保留主邏輯):
public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext head = getActualUpstreamContext(this.head); head.getHandler().handleUpstream(head, e); }private DefaultChannelHandlerContext getActualUpstreamContext(DefaultChannelHandlerContext ctx) { DefaultChannelHandlerContext realCtx = ctx; while (!realCtx.canHandleUpstream()) { realCtx = realCtx.next; if (realCtx == null) { return null; } } return realCtx; }</pre>
這里最終調用了 ChannelUpstreamHandler.handleUpstream 來處理這個ChannelEvent。有意思的是,這里我們看不到任何”將Handler向后移一位”的操作,但是我們總不能每次都用同一個Handler來進行處理啊?實際上,我們更為常用的是 ChannelHandlerContext.handleUpstream 方法(實現是 DefaultChannelHandlerContext.sendUpstream 方法):
public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext next = getActualUpstreamContext(this.next); DefaultChannelPipeline.this.sendUpstream(next, e); }可以看到,這里最終仍然調用了 ChannelPipeline.sendUpstream 方法,但是 它會將Handler指針后移 。
我們接下來看看 DefaultChannelHandlerContext.sendDownstream :
public void sendDownstream(ChannelEvent e) { DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev); if (prev == null) { try { getSink().eventSunk(DefaultChannelPipeline.this, e); } catch (Throwable t) { notifyHandlerException(e, t); } } else { DefaultChannelPipeline.this.sendDownstream(prev, e); } }與sendUpstream好像不大相同哦?這里有兩點:一是到達末尾時,就如夢境二所說,會調用ChannelSink進行處理;二是這里指針是 往前移 的,所以我們知道了:
UpstreamHandler是從前往后執行的,DownstreamHandler是從后往前執行的。在ChannelPipeline里添加時需要注意順序了!
DefaultChannelPipeline里還有些機制,像添加/刪除/替換Handler,以及 ChannelPipelineFactory 等,比較好理解,就不細說了。
回到現實:Pipeline解決的問題
好了,深入分析完代碼,有點頭暈了,我們回到最開始的地方,來想一想,Netty的Pipeline機制解決了什么問題?
我認為至少有兩點:
一是提供了ChannelHandler的編程模型,基于ChannelHandler開發業務邏輯,基本不需要關心網絡通訊方面的事情,專注于編碼/解碼/邏輯處理就可以了。Handler也是比較方便的開發模式,在很多框架中都有用到。
二是實現了所謂的”Universal Asynchronous API”。這也是Netty官方標榜的一個功能。用過OIO和NIO的都知道,這兩套API風格相差極大,要從一個遷移到另一個成本是很大的。即使是NIO,異步和同步編程差距也很大。而Netty屏蔽了OIO和NIO的API差異,通過Channel提供對外接口,并通過ChannelPipeline將其連接起來,因此替換起來非常簡單。
![]()
理清了ChannelPipeline的主流程,我們對Channel部分的大致結構算是弄清楚了。可是到了這里,我們依然對一個連接具體怎么處理沒有什么概念,下篇文章,我們會分析一下,在Netty中,捷徑如何處理連接的建立、數據的傳輸這些事情。
PS: Pipeline這部分拖了兩個月,終于寫完了。中間寫的實在緩慢,寫個高質量(至少是自認為吧!)的文章不容易,但是仍不忍心這部分就此爛尾。中間參考了一些優秀的文章,還自己使用netty開發了一些應用。以后這類文章,還是要集中時間來寫完好了。
參考資料:
- Sink http://en.wikipedia.org/wiki/Sink_(computing)
</ul> </div>