目錄大綱:
- 前言
- ChannelPipeline | ChannelHandler | ChannelHandlerContext 三巨頭介紹
- 三巨頭編織過程(創(chuàng)建過程)
- ChannelPipeline 是如何調(diào)度 handler 的
- 總結(jié)
前言
相信對 Netty 熟悉的同學(xué)對 pipeline 都非常的熟悉丑婿,肯定也有不熟悉的语泽,不管怎樣鹦肿,樓主今天的目的就是將 pipeline 從頭擼到尾紊撕,徹徹底底的理解 pipeline 的每一步操作钱骂。
當(dāng)然夺艰,文章還是一如既往的長。請非戰(zhàn)斗人員盡快撤離8皇佟2橇ァB嗉小!
讓我們開始吧苏潜!
1. ChannelPipeline | ChannelHandler | ChannelHandlerContext 三巨頭介紹
如果把 Netty 比作一個人類的話银萍,那么 EventLoop 就是這個人的大腦,負(fù)責(zé)這個人的所有操作恤左。而 pipeline 就是這個的腸道贴唇,負(fù)責(zé)將這個人吃進(jìn)去的東西進(jìn)行消化然后處理。這個比喻可能不是很恰當(dāng)飞袋,當(dāng)然這也是為了加深理解戳气。
當(dāng)然,我說的 pipelie 是一個廣義的概念授嘀,pipeline 包括很多東西物咳,就像我們標(biāo)題說的三巨頭,下面我們就來好好說說他們的關(guān)系蹄皱。
1.0三者關(guān)系
我們在之前的文章中知道,每當(dāng) ServerSocket 創(chuàng)建一個新的連接芯肤,就會創(chuàng)建一個 Socket巷折,對應(yīng)的就是目標(biāo)客戶端。而每一個新創(chuàng)建的 Socket 都將會分配一個全新的 ChannelPipeline(以下簡稱 pipeline)崖咨,他們的關(guān)系是永久不變的锻拘;而每一個 ChannelPipeline 內(nèi)部都含有多個 ChannelHandlerContext(以下簡稱 Context),他們一起組成了雙向鏈表击蹲,這些 Context 用于包裝我們調(diào)用 addLast 方法時添加的 ChannelHandler(以下簡稱 handler)署拟。
所以說,他們的關(guān)系是這樣的:
上圖中:ChannelSocket 和 ChannelPipeline 是一對一的關(guān)聯(lián)關(guān)系歌豺,而 pipeline 內(nèi)部的多個 Context 形成了鏈表推穷,Context 只是對 Handler 的封裝。
為什么需要對 Handler 進(jìn)行封裝呢类咧?想象一下:當(dāng)你 A handler 要調(diào) B handler 方法的時候馒铃,如果沒有 Context,那么就直接調(diào)用了痕惋,如果有一些需要在調(diào)用前后通用的邏輯就需要在每個 handler 地方都寫区宇,這樣會導(dǎo)致代碼重復(fù),而且緊耦合值戳,不符合設(shè)計原則议谷。
總的來說,當(dāng)一個請求進(jìn)來的時候堕虹,會進(jìn)入 Socket 對應(yīng)的 pipeline卧晓,并流經(jīng) pipeline 所有的 handler芬首,對,就是設(shè)計模式中的過濾器模式禀崖,可以說是最佳實(shí)踐衩辟。用過濾器處理網(wǎng)絡(luò)數(shù)據(jù)的不止 netty,還有 tomcat波附,相信大家對 tomcat 的 filter(應(yīng)該是 servlet 的 filter) 都非常的熟悉吧艺晴。
知道了他們的概念,我們繼續(xù)深入看看他們的設(shè)計掸屡。
1.1 ChannelPipeline 作用及設(shè)計
首先看 pipeline 的接口設(shè)計:
public interface ChannelPipeline
extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable<Entry<String, ChannelHandler>> {
ChannelPipeline addFirst(String name, ChannelHandler handler);
ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler);
ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler);
ChannelPipeline addLast(ChannelHandler... handlers);
Channel channel();
ChannelHandlerContext context(ChannelHandler handler);
ChannelPipeline remove(ChannelHandler handler);
ChannelPipeline replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler);
}
通過 UML 圖封寞,可以看到該接口繼承了 inBound,outBound仅财,Iterable 接口狈究,表示他可以調(diào)用當(dāng)數(shù)據(jù)出站的方法和入站的方法,同時也能遍歷內(nèi)部的鏈表盏求。
再看看他的幾個具有代表性的方法抖锥,基本上都是針對 handler 鏈表的插入,追加碎罚,刪除磅废,替換操作,甚至荆烈,我們可以想象他就是一個 LinkedList拯勉。同時,他也能返回 channel(也就是 socket)憔购。
在 pipeline 的接口文檔上宫峦,作者寫了很多注釋并且畫了一幅圖:
文檔大致意思是:
這是一個 handler 的 list,handler 用于處理或攔截入站事件和出站事件玫鸟,pipeline 實(shí)現(xiàn)了過濾器的高級形式导绷,以便用戶完全控制事件如何處理以及 handler 在 pipeline 中如何交互。
上圖描述了一個典型的 handler 在 pipeline 中處理 I/O 事件的方式鞋邑,IO 事件由 inboundHandler 或者 outBoundHandler 處理诵次,并通過調(diào)用 ChannelHandlerContext.fireChannelRead 方法轉(zhuǎn)發(fā)給其最近的處理程序 。
入站事件由入站處理程序以自下而上的方向處理枚碗,如圖所示逾一。入站處理程序通常處理由圖底部的I / O線程生成入站數(shù)據(jù)。入站數(shù)據(jù)通常從如 SocketChannel.read(ByteBuffer) 獲取肮雨。如果入站事件超出頂層入站處理程序遵堵,它將被靜默放棄,或者在需要您關(guān)注時進(jìn)行記錄。
通常一個 pipeline 有多個 handler陌宿,例如锡足,一個典型的服務(wù)器在每個通道的管道中都會有以下處理程序,但是您的里程可能會因協(xié)議和業(yè)務(wù)邏輯的復(fù)雜性和特征而異:
- 協(xié)議解碼器 - 將二進(jìn)制數(shù)據(jù)(例如 ByteBuf 在io.netty.buffer中的類))轉(zhuǎn)換為Java對象壳坪。
- 協(xié)議編碼器 - 將Java對象轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)舶得。
- 業(yè)務(wù)邏輯處理程序 - 執(zhí)行實(shí)際業(yè)務(wù)邏輯(例如數(shù)據(jù)庫訪問)。
注意:你的業(yè)務(wù)程序不能將線程阻塞爽蝴,他將會影響 IO 的速度沐批,進(jìn)而影響整個 Netty 程序的性能。如果你的業(yè)務(wù)程序很快蝎亚,就可以放在 IO 線程中九孩,反之,你需要異步執(zhí)行发框√杀颍或者在添加 handler 的時候添加一個線程池,例如:
// 下面這個任務(wù)執(zhí)行的時候梅惯,將不會阻塞 IO 線程宪拥,執(zhí)行的線程來自 group 線程池
pipeline.addLast(group,“handler”铣减,new MyBusinessLogicHandler());
好江解,關(guān)于 pipeline 的設(shè)計就介紹到這里。我們再看看我們常見的 ChannelHandler徙歼。
1.2 ChannelHandler 作用及設(shè)計
關(guān)于 ChannelHanderl 我們都非常的熟悉吧,在每個最初認(rèn)識 Netty 的人都知道他的 demo 程序中會添加 handler 并自己實(shí)現(xiàn) handler鳖枕,通常魄梯,我們說 handler 指的就是 ChannelHandler。
ChannelHandler 是一個頂級接口宾符,沒有繼承任何接口:
定義了 3 個方法:
public interface ChannelHandler {
// 當(dāng)把 ChannelHandler 添加到 pipeline 時被調(diào)用
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
// 當(dāng)從 pipeline 中移除時調(diào)用
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
// 當(dāng)處理過程中在 pipeline 發(fā)生異常時調(diào)用
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
總的來說酿秸,ChannelHandler 的作用就是處理 IO 事件或攔截 IO 事件,并將其轉(zhuǎn)發(fā)給下一個處理程序 ChannelHandler魏烫。
從上面的代碼中辣苏,可以看到,ChannelHandler 并沒有提供很多的方法哄褒,因?yàn)?Handler 處理事件時分入站和出站的稀蟋,兩個方向的操作都是不同的,因此呐赡,Netty 定義了兩個子接口繼承 ChannelHandler退客。
1. ChannelInboundHandler 入站事件接口
public interface ChannelInboundHandler extends ChannelHandler {
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
void channelActive(ChannelHandlerContext ctx) throws Exception;
void channelInactive(ChannelHandlerContext ctx) throws Exception;
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
如果你經(jīng)常使用 Netty 程序,你會非常的熟悉這些方法,比如 channelActive 用于當(dāng) Channel 處于活動狀態(tài)時被調(diào)用萌狂;channelRead ------ 當(dāng)從Channel 讀取數(shù)據(jù)時被調(diào)用等等方法档玻。通常我們需要重寫一些方法,當(dāng)發(fā)生關(guān)注的事件茫藏,我們需要在方法中實(shí)現(xiàn)我們的業(yè)務(wù)邏輯误趴,因?yàn)楫?dāng)事件發(fā)生時,Netty 會回調(diào)對應(yīng)的方法务傲。
注意:當(dāng)你重寫了上面的 channelRead 方法時凉当,你需要顯示的釋放與池化的 ByteBuf 實(shí)例相關(guān)的內(nèi)存。Netty 為此提供了了一個使用方法 ReferenceCountUtil.release().
2. ChannelOutboundHandler 出站事件接口
ChannelOutboundHandler 負(fù)責(zé)出站操作和處理出站數(shù)據(jù)树灶。接口方法如下:
public interface ChannelOutboundHandler extends ChannelHandler {
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception;
void connect(
ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) throws Exception;
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void read(ChannelHandlerContext ctx) throws Exception;
void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception;
void flush(ChannelHandlerContext ctx) throws Exception;
}
大家可以熟悉熟悉這個接口纤怒,比如 bind 方法,當(dāng)請求將 Channel 綁定到本地地址時調(diào)用天通,close 方法泊窘,當(dāng)請求關(guān)閉 Channel 時調(diào)用等等,總的來說像寒,出站操作都是一些連接和寫出數(shù)據(jù)類似的方法烘豹。和入站操作有很大的不同。
總之诺祸,我們要區(qū)別入站方法和出站方法携悯,這在 pipeline 中將會起很大的作用。
3. ChannelDuplexHandler 處理出站和入站事件
public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implements ChannelOutboundHandler {
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
throws Exception {
ctx.disconnect(promise);
}
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.close(promise);
}
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
從上面的代碼中可以看出 ChannelDuplexHandler 間接實(shí)現(xiàn)了入站接口并直接實(shí)現(xiàn)了出站接口筷笨。是一個通用的能夠同時處理入站事件和出站事件的類憔鬼。
介紹了完了 ChannelHandler 的設(shè)計,我們再來看看 ChannelHandlerContext 胃夏。
1.3 ChannelHandlerContext 作用及設(shè)計
實(shí)際上轴或,從上面的代碼中,我們已經(jīng)看到了 Context 的用處仰禀,在 ChannelDuplexHandler 中照雁,cxt 無處不在。事實(shí)上答恶,以read 方法為例:調(diào)用 handler 的 read 方法饺蚊,如果你不處理,就會調(diào)用 context 的 read 方法悬嗓,context 再調(diào)用下一個 context 的 handler 的 read 方法污呼。
我們看看 ChannelHandlerContext 的接口 UML :
ChannelHandlerContext 繼承了出站方法調(diào)用接口和入站方法調(diào)用接口。那么烫扼, ChannelInboundInvoker 和 ChannelOutboundInvoker 又有哪些方法呢曙求?
可以看到,這兩個 invoker 就是針對入站或出站方法來的,就是再 入站或出站 handler 的外層再包裝一層悟狱,達(dá)到在方法前后攔截并做一些特定操作的目的静浴。
而 ChannelHandlerContext 不僅僅時繼承了他們兩個的方法,同時也定義了一些自己的方法:
public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker {
Channel channel();
EventExecutor executor();
String name();
ChannelHandler handler();
boolean isRemoved();
ChannelPipeline pipeline();
ByteBufAllocator alloc();
}
這些方法能夠獲取 Context 上下文環(huán)境中對應(yīng)的比如 channel挤渐,executor苹享,handler ,pipeline浴麻,內(nèi)存分配器得问,關(guān)聯(lián)的 handler 是否被刪除。
我們可以認(rèn)為软免,Context 就是包裝了 handler 相關(guān)的一切宫纬,以方便 Context 可以在 pipeline 方便的操作 handler 相關(guān)的資源和行為。
2. 三巨頭編織過程(創(chuàng)建過程)
介紹完了 "三巨頭" 的接口設(shè)計和一些方法膏萧,那么我們就看看漓骚,他們是如何編制在一起的。
在文章前面榛泛,我們說:
每當(dāng) ServerSocket 創(chuàng)建一個新的連接蝌蹂,就會創(chuàng)建一個 Socket,對應(yīng)的就是目標(biāo)客戶端曹锨。而每一個新創(chuàng)建的 Socket 都將會分配一個全新的 ChannelPipeline(以下簡稱 pipeline)孤个,他們的關(guān)系是永久不變的;而每一個 ChannelPipeline 內(nèi)部都含有多個 ChannelHandlerContext(以下簡稱 Context)沛简,他們一起組成了雙向鏈表齐鲤,這些 Context 用于包裝我們調(diào)用 addLast 方法時添加的 ChannelHandler(以下簡稱 handler)。
我們可以分為3個步驟來看編織的過程:
- 任何一個 ChannelSocket 創(chuàng)建的同時都會創(chuàng)建 一個 pipeline椒楣。
- 當(dāng)用戶或系統(tǒng)內(nèi)部調(diào)用 pipeline 的 add*** 方法添加 handler 時佳遂,都會創(chuàng)建一個包裝這 handler 的 Context。
- 這些 Context 在 pipeline 中組成了雙向鏈表撒顿。
讓我們從代碼層面看看他們的編織過程。
1. Socket 創(chuàng)建的時候創(chuàng)建 pipeline:
在 SocketChannel 的抽象父類 AbstractChannel 的構(gòu)造方法中:
從 newChannelPipeline 方法中獲取一個 pipeline荚板,這個方法的標(biāo)準(zhǔn)實(shí)現(xiàn)如下:
創(chuàng)建一個 DefaultChannelPipeline 對象凤壁,并傳入 channel 對象。這個 DefaultChannelPipeline 是 ChannelPipeline 接口的標(biāo)準(zhǔn)實(shí)現(xiàn)跪另。
我們看看他的創(chuàng)建過程:
- 將 channel 賦值給 channel 字段拧抖,用于 pipeline 操作 channel。
- 創(chuàng)建一個 future 和 promise免绿,用于異步回調(diào)使用唧席。
- 創(chuàng)建一個 inbound 的 tailContext,創(chuàng)建一個既是 inbound 類型又是 outbound 類型的 headContext.
- 最后,將兩個 Context 互相連接淌哟,形成雙向鏈表迹卢。
注意: tailContext 和 HeadContext 非常的重要,所有 pipeline 中的事件都會流經(jīng)他們徒仓,所以我們重點(diǎn)關(guān)注 tailContext 和 headContext腐碱。
首先看看 TailContext 的設(shè)計:一個屬于 DefaultChannelPipeline 的內(nèi)部類。
UML 繼承圖如下:
從上圖中可以看出掉弛, TailContext 是一個處理入站事件的 handler症见。
構(gòu)造方法如下:
private static final String TAIL_NAME = generateName0(TailContext.class);
TailContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, TAIL_NAME, true, false);
setAddComplete();
}
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
boolean inbound, boolean outbound) {
this.name = ObjectUtil.checkNotNull(name, "name");
this.pipeline = pipeline;
this.executor = executor;
this.inbound = inbound;
this.outbound = outbound;
ordered = executor == null || executor instanceof OrderedEventExecutor;
}
從上面的構(gòu)造方法中可以看出來,Context 果然就是 Context 殃饿,囊括了 Channel 所包含的一切谋作,這里說一下 name 是 簡單類名+#0 的形式。pipeline 就是當(dāng)前的 pipeline乎芳,executor 是 null遵蚜,inbound 屬性是 true,outbound 屬性是 fasle秒咐。說明他是一個入站處理器谬晕。當(dāng)有入站事件時,會調(diào)用 tailContext携取。
說完 TailContext 攒钳,再看看 HeadContext。
HeadContext 同樣時 DefaultChannelPipeline 的內(nèi)部類雷滋,UML 繼承圖如下:
從上圖中不撑,可以看出來 HeadContext 非常的全能,既是入站處理器也是出站處理器晤斩,任何事件都逃不過他的眼睛焕檬。
他的構(gòu)造方法和 tail 有些許的不同:
HeadContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, HEAD_NAME, false, true);
unsafe = pipeline.channel().unsafe();
setAddComplete();
}
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
boolean inbound, boolean outbound) {
this.name = ObjectUtil.checkNotNull(name, "name");
this.pipeline = pipeline;
this.executor = executor;
this.inbound = inbound;
this.outbound = outbound;
ordered = executor == null || executor instanceof OrderedEventExecutor;
}
從構(gòu)造方法上看,唯一的區(qū)別就是比 tailContext 多了一個屬性 unsafe澳泵,而這個屬性來自于 pipeline 所屬的 channel 的 unsafe实愚,如果大家有印象的話,會記得 channel 初始化的時候兔辅,也會初始化一個 unsafe腊敲,這個我們今天先不細(xì)說,只需要知道他是一個 Netty 中一個直接處理的類维苔,每個類型的 Socket 都有不同的實(shí)現(xiàn)碰辅。而為什么 head 需要這樣一個屬性呢?因?yàn)?head 需要處理出站數(shù)據(jù)介时,還記得出站接口時怎么定義的嗎没宾?
出站接口中都是針對數(shù)據(jù)的操作凌彬,比如 read,write循衰,flush 等操作铲敛,所以需要 unsafe 這個能夠處理數(shù)據(jù)的工具實(shí)例。
為什么 tail 不需要呢羹蚣?我想你應(yīng)該知道了原探,tail 雖然是入站 handler,入站 handler 定義的方法沒有需要直接處理數(shù)據(jù)的顽素,比如 read咽弦,write,flush等:
理解這兩個處理器的定義很重要胁出,因?yàn)槊糠N類型的處理器定義的的任務(wù)都是不同的型型。
2. 在 add**** 添加處理器的時候創(chuàng)建 Context
我們看看 DefaultChannelPipeline 的 addLast 方法如何創(chuàng)建的 Context,代碼如下:
@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
for (ChannelHandler h: handlers) {
if (h == null) {
break;
}
addLast(executor, null, h);
}
return this;
}
注意全蝶,addLast 是個重載方法闹蒜,你可以選擇傳入一個線程池,作用是什么呢抑淫?當(dāng)你的業(yè)務(wù) handler 非常耗時绷落,甚至阻塞線程,那么 Netty 建議你異步執(zhí)行該任務(wù)始苇,否則將會影響 Netty 的性能砌烁。而這個線程池就是用來執(zhí)行這個 handler 的耗時任務(wù)的。
什么時候會返回這個線程池呢催式?
當(dāng)你調(diào)用類似 ChannelActive 方法的時候函喉,會需要 Cotext 的 executor,方法如下:
如果你沒有定義 handler 自己的 executor荣月,那么就使用 channel 的 線程管呵,也就是 IO 線程。你需要十分確定你的業(yè)務(wù)不會阻塞線程哺窄。
再看看 addLast 方法:
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
addLast0(newCtx);
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
public void run() {
callHandlerAdded0(newCtx);
}
});
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
向 pipeline 添加 handler捐下,參數(shù)是線程池,name 是null萌业, handler 是我們或者系統(tǒng)傳入的handler蔑担。Netty 為了防止多個線程導(dǎo)致安全問題,同步了這段代碼咽白,步驟如下:
- 檢查這個 handler 實(shí)例是否是共享(Sharable 注解)的,如果不是鸟缕,并且已經(jīng)被別的 pipeline 使用了晶框,則拋出異常排抬。
- 調(diào)用 newContext(group, filterName(name, handler), handler) 方法,創(chuàng)建一個 Context授段。從這里可以看出來了蹲蒲,每次添加一個 handler 都會創(chuàng)建一個關(guān)聯(lián) Context。
- 調(diào)用 addLast 方法侵贵,將 Context 追加到鏈表中届搁。
- 如果這個通道還沒有注冊到 selecor 上,就將這個 Context 添加到這個 pipeline 的待辦任務(wù)中窍育。當(dāng)注冊好了以后卡睦,就會調(diào)用 callHandlerAdded0 方法(默認(rèn)是什么都不做,用戶可以實(shí)現(xiàn)這個方法)漱抓。
我們重點(diǎn)看看第 2 步和第 3 步:
newContext 方法代碼如下:
這里的 super 構(gòu)造方法和 head tail 一樣表锻,沒什么不同,有 2 個方法需要注意一下 isInbound 和 isOutbound 方法乞娄。這兩個方法是辨別這個 handler 是 inbound 還是 outbound 瞬逊。如果是你,你怎么寫仪或?我們還是看看 Netty 是怎么寫的吧:
private static boolean isInbound(ChannelHandler handler) {
return handler instanceof ChannelInboundHandler;
}
private static boolean isOutbound(ChannelHandler handler) {
return handler instanceof ChannelOutboundHandler;
}
很簡單确镊,通過 instanceof 關(guān)鍵字判斷。哈哈范删。
再看看第 3 步蕾域,如何將這個新創(chuàng)建的 Context 插入到鏈表中:
也很簡單,一個標(biāo)準(zhǔn)的雙向鏈表實(shí)現(xiàn)瓶逃。將新的 Context 的 prev 指向 tail 之前的 prev束铭,將新的 Context 的 next 指向 tail,將 tail 之前的 prev 的 next 指向新的 Context厢绝, 將 tail 現(xiàn)在的 prev 指向新的 Context契沫。成功插入到 tail 的前面,所以昔汉,這里的 addLast 不是真正的 last懈万,而是除了 tail 的 last,因?yàn)?tail 是系統(tǒng)的節(jié)點(diǎn)靶病,需要做一些系統(tǒng)工作会通。
好了,到這里娄周,針對三巨頭的創(chuàng)建過程涕侈,我們就了解的差不多了,就和我們最初說的一樣煤辨,每當(dāng)創(chuàng)建 ChannelSocket 的時候都會創(chuàng)建一個綁定的 pipeline裳涛,一對一的關(guān)系木张,同時也創(chuàng)建一個 pipeline,創(chuàng)建 pipeline 的時候也會創(chuàng)建 tail 節(jié)點(diǎn)和 head 節(jié)點(diǎn)端三,形成最初的鏈表舷礼。tail 是入站 inbound 類型的 handler, head 既是 inbound 也是 outbound 類型的 handler郊闯。在調(diào)用 pipeline 的 addLast 方法的時候妻献,會根據(jù)給定的 handler 創(chuàng)建一個 Context,然后团赁,將這個 Context 插入到鏈表的尾端(tail 前面)育拨。這樣,整個三巨頭就連接起來了然痊,就能為后面的請求進(jìn)行流式處理了至朗。
3. ChannelPipeline 是如何調(diào)度 handler 的
說了這么多,那么當(dāng)一個請求進(jìn)來的時候剧浸,ChannelPipeline 是如何調(diào)用內(nèi)部的這些 handler 的呢锹引?我們一起來看看。
首先唆香,當(dāng)一個請求進(jìn)來的時候嫌变,會第一個調(diào)用 pipeline 的 相關(guān)方法,如果是入站事件躬它,這些方法由 fire 開頭腾啥,表示開始管道的流動。讓后面的 handler 繼續(xù)處理冯吓。
我們看看 DefaultChannelPipeline 是如何實(shí)現(xiàn)這些 fire 方法的倘待。
從上圖中可以看出來,這些方法都是 inbound 的方法组贺,也就是入站事件凸舵,調(diào)用靜態(tài)方法傳入的也是 inbound 的類型 head handler。這些靜態(tài)方法則會調(diào)用 head 的 ChannelInboundInvoker 接口的方法失尖,再然后調(diào)用 handler 的真正方法啊奄。
再看看 piepline 的 outbound 的 fire 方法實(shí)現(xiàn):
和 inbound 類似,這些都是出站的實(shí)現(xiàn)掀潮,但是調(diào)用的是 outbound 類型的 tail handler 來進(jìn)行處理菇夸,因?yàn)檫@些都是 outbound 事件。
為什么出站是 tail 開始仪吧,入站從 head 開始呢庄新?因?yàn)槌稣臼菑膬?nèi)部外面寫,從tail 開始薯鼠,能夠讓前面的 handler 進(jìn)行處理择诈,防止由 handler 被遺漏凡蚜,比如編碼。反之吭从,入站當(dāng)然是從 head 往內(nèi)部輸入,讓后面的 handler 能夠處理這些輸入的數(shù)據(jù)恶迈。比如解碼涩金。
這也解釋了雖然 head 也實(shí)現(xiàn)了 outbound 接口跪妥,但不是從 head 開始執(zhí)行出站任務(wù)生音。
關(guān)于如何調(diào)度,請讓我用一張圖來表示:
pipeline 首先會調(diào)用 Context 的靜態(tài)方法 fireXXX酣胀,并傳入 Context奈附,然后全度,靜態(tài)方法調(diào)用 Context 的 invoker 方法,而 invoker 方法內(nèi)部會調(diào)用該 Context 所包含的 Handler 的真正的 XXX 方法斥滤,調(diào)用結(jié)束后将鸵,如果還需要繼續(xù)向后傳遞,就調(diào)用 Context 的 fireXXX2 方法佑颇,循環(huán)往復(fù)顶掉。
我們將在下一篇文章中詳細(xì)的解析一個請求在 pipeline 中的流動過程。這幅圖僅作拋磚引玉挑胸。
好痒筒,到這里,關(guān)于這三巨頭的介紹就差不多了茬贵,下面簿透,外面來做一下總結(jié)。
4. 總結(jié)
這是我們 Netty 系列關(guān)于 pipeline 的第一篇文章解藻,講述了關(guān)于 pipeline 老充,Context,Handler 錯綜復(fù)雜的關(guān)系舆逃,實(shí)際上蚂维,還是很清晰的。Context 包裝 handler路狮,多個 Context 在 pipeline 中形成了雙向鏈表虫啥,入站方向叫 inbound,由 head 節(jié)點(diǎn)開始奄妨,出站方法叫 outbound 涂籽,由 tail 節(jié)點(diǎn)開始。而節(jié)點(diǎn)中間的傳遞通過 AbstractChannelHandlerContext 類內(nèi)部的 fire 系列方法砸抛,找到當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn)不斷的循環(huán)傳播评雌。是一個完美的過濾器高級形式树枫。
下一篇,將和大家一起在 pipeline 的管道中游走一趟景东。
good luckI扒帷!=锿隆搔涝!