Netty 核心組件 Pipeline 源碼分析(一)之剖析 pipeline 三巨頭

目錄大綱:

  1. 前言
  2. ChannelPipeline | ChannelHandler | ChannelHandlerContext 三巨頭介紹
  3. 三巨頭編織過程(創(chuàng)建過程)
  4. ChannelPipeline 是如何調(diào)度 handler 的
  5. 總結(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ù)雜性和特征而異:

  1. 協(xié)議解碼器 - 將二進(jìn)制數(shù)據(jù)(例如 ByteBuf 在io.netty.buffer中的類))轉(zhuǎn)換為Java對象壳坪。
  2. 協(xié)議編碼器 - 將Java對象轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)舶得。
  3. 業(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 又有哪些方法呢曙求?

ChannelInboundInvoker 入站方法調(diào)用器
ChannelOutboundInvoker 出站方法調(diào)用器

可以看到,這兩個 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個步驟來看編織的過程:

  1. 任何一個 ChannelSocket 創(chuàng)建的同時都會創(chuàng)建 一個 pipeline椒楣。
  2. 當(dāng)用戶或系統(tǒng)內(nèi)部調(diào)用 pipeline 的 add*** 方法添加 handler 時佳遂,都會創(chuàng)建一個包裝這 handler 的 Context。
  3. 這些 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)建過程:

  1. 將 channel 賦值給 channel 字段拧抖,用于 pipeline 操作 channel。
  2. 創(chuàng)建一個 future 和 promise免绿,用于異步回調(diào)使用唧席。
  3. 創(chuàng)建一個 inbound 的 tailContext,創(chuàng)建一個既是 inbound 類型又是 outbound 類型的 headContext.
  4. 最后,將兩個 Context 互相連接淌哟,形成雙向鏈表迹卢。

注意: tailContext 和 HeadContext 非常的重要,所有 pipeline 中的事件都會流經(jīng)他們徒仓,所以我們重點(diǎn)關(guān)注 tailContext 和 headContext腐碱。

首先看看 TailContext 的設(shè)計:一個屬于 DefaultChannelPipeline 的內(nèi)部類。

UML 繼承圖如下:

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)致安全問題,同步了這段代碼咽白,步驟如下:

  1. 檢查這個 handler 實(shí)例是否是共享(Sharable 注解)的,如果不是鸟缕,并且已經(jīng)被別的 pipeline 使用了晶框,則拋出異常排抬。
  2. 調(diào)用 newContext(group, filterName(name, handler), handler) 方法,創(chuàng)建一個 Context授段。從這里可以看出來了蹲蒲,每次添加一個 handler 都會創(chuàng)建一個關(guān)聯(lián) Context。
  3. 調(diào)用 addLast 方法侵贵,將 Context 追加到鏈表中届搁。
  4. 如果這個通道還沒有注冊到 selecor 上,就將這個 Context 添加到這個 pipeline 的待辦任務(wù)中窍育。當(dāng)注冊好了以后卡睦,就會調(diào)用 callHandlerAdded0 方法(默認(rèn)是什么都不做,用戶可以實(shí)現(xiàn)這個方法)漱抓。

我們重點(diǎn)看看第 2 步和第 3 步:
newContext 方法代碼如下:

創(chuàng)建默認(rèn)的 DefaultChannelHandlerContext 實(shí)例
構(gòu)造方法

這里的 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扒帷!=锿隆搔涝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市和措,隨后出現(xiàn)的幾起案子庄呈,更是在濱河造成了極大的恐慌,老刑警劉巖派阱,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诬留,死亡現(xiàn)場離奇詭異,居然都是意外死亡贫母,警方通過查閱死者的電腦和手機(jī)文兑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颁独,“玉大人彩届,你說我怎么就攤上這事∈木疲” “怎么了樟蠕?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長靠柑。 經(jīng)常有香客問我寨辩,道長,這世上最難降的妖魔是什么歼冰? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任靡狞,我火速辦了婚禮,結(jié)果婚禮上隔嫡,老公的妹妹穿的比我還像新娘甸怕。我一直安慰自己,他們只是感情好腮恩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布梢杭。 她就那樣靜靜地躺著,像睡著了一般秸滴。 火紅的嫁衣襯著肌膚如雪武契。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機(jī)與錄音咒唆,去河邊找鬼届垫。 笑死,一個胖子當(dāng)著我的面吹牛全释,可吹牛的內(nèi)容都是我干的装处。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼浸船,長吁一口氣:“原來是場噩夢啊……” “哼符衔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起糟袁,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躺盛,沒想到半個月后项戴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡槽惫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年周叮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片界斜。...
    茶點(diǎn)故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡仿耽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出各薇,到底是詐尸還是另有隱情项贺,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布峭判,位于F島的核電站开缎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏林螃。R本人自食惡果不足惜奕删,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疗认。 院中可真熱鬧完残,春花似錦、人聲如沸横漏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绊茧。三九已至铝宵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹏秋。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工尊蚁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侣夷。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓横朋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親百拓。 傳聞我的和親對象是個殘疾皇子琴锭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評論 2 354

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