第 10 章 Netty 核心源碼剖析②

Pipeline Handler HandlerContext 創建源碼剖析
ChannelPipeline 調度 handler 的源碼剖析

源碼剖析目的
Netty 中的 ChannelPipeline 、 ChannelHandler 和 ChannelHandlerContext 是非常核心的組件, 我們從源碼來分析Netty 是如何設計這三個核心組件的,並分析是如何創建和協調工作的.

ChannelPipeline | ChannelHandler | ChannelHandlerContext 介紹

1.1 三者關係

  1. 每當 ServerSocket 創建一個新的連接,就會創建一個 Socket,對應的就是目標客戶端。

  2. 每一個新創建的 Socket 都將會分配一個全新的 ChannelPipeline(以下簡稱 pipeline)

  3. 每一個 ChannelPipeline 內部都含有多個 ChannelHandlerContext(以下簡稱 Context)

  4. 他們一起組成了雙向鏈表,這些 Context 用於包裝我們調用 addLast 方法時添加的 ChannelHandler(以下簡稱 handler)
    在這裏插入圖片描述

  • 上圖中:ChannelSocket 和 ChannelPipeline 是一對一的關聯關係,而 pipeline 內部的多個 Context 形成了鏈表,Context 只是對 Handler 的封裝。
  • 當一個請求進來的時候,會進入 Socket 對應的 pipeline,並經過 pipeline 所有的 handler,對,就是設計模式中的過濾器模式。

1.2 ChannelPipeline 作用及設計

1)pipeline 的接口設計
在這裏插入圖片描述
部分源碼
在這裏插入圖片描述

可以看到該接口繼承了 inBound,outBound,Iterable 接口,表示他可以調用數據出站的方法和入站的方法,同時也能遍歷內部的鏈表, 看看他的幾個代表性的方法,基本上都是針對 handler 鏈表的插入,追加,刪除,替換操作,類似是一個 LinkedList。同時,也能返回 channel(也就是 socket)
在這裏插入圖片描述
1)在 pipeline 的接口文檔上,提供了一幅圖
數據流向pipeline 是入棧,流出pipeline 是出棧
在這裏插入圖片描述

對上圖的解釋說明:

  • 這是一個 handler 的 list,handler 用於處理或攔截入站事件和出站事件,pipeline 實現了過濾器的高級形式,以便用戶控制事件如何處理以及 handler 在 pipeline 中如何交互。

  • 上圖描述了一個典型的 handler 在 pipeline 中處理 I/O 事件的方式,IO 事件由 inboundHandler 或者 outBoundHandler 處理,並通過調用ChannelHandlerContext.fireChannelRead 方法轉發給其最近的handler 處理程序 。
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 入站事件由入站處理程序以自下而上的方向處理,如圖所示。入站處理程序通常處理由圖底部的I / O線程生成入站數據。入站數據通常從如SocketChannel.read(ByteBuffer) 獲取。

  • 通常一個 pipeline 有多個 handler,例如,一個典型的服務器在每個通道的管道中都會有以下處理程序
    協議解碼器 - 將二進制數據轉換爲Java對象。
    協議編碼器 - 將Java對象轉換爲二進制數據。
    業務邏輯處理程序 - 執行實際業務邏輯(例如數據庫訪問)

  • 你的業務程序不能將線程阻塞,會影響 IO 的速度,進而影響整個 Netty 程序的性能。如果你的業務程序很快,就可以放在 IO 線程中,反之,你需要異步執行。或者在添加 handler 的時候添加一個線程池,
    例如:
    // 下面這個任務執行的時候,將不會阻塞 IO 線程,執行的線程來自 group 線程池
    pipeline.addLast(group,“handler”,new MyBusinessLogicHandler());
    或者放taskQueue或者scheduleTaskQueue中中

1.3 ChannelHandler 作用及設計

  1. 源碼
public interface ChannelHandler {

 //當把 ChannelHandler 添加到 pipeline 時被調用
 void handlerAdded(ChannelHandlerContext ctx) throws Exception;
 
//當從 pipeline 中移除時調用
 void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
 
// 當處理過程中在 pipeline 發生異常時調用
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

ChannelHandler 的作用就是處理 IO 事件或攔截 IO 事件,並將其轉發給下一個處理程序 ChannelHandler。
Handler 處理事件時分入站和出站的,兩個方向的操作都是不同的,因此,Netty 定義了兩個子接口繼承 ChannelHandler

2)ChannelInboundHandler 入站事件接口
在這裏插入圖片描述

  • channelActive 用於當 Channel 處於活動狀態時被調用;

  • channelRead 當從Channel 讀取數據時被調用等等方法。

  • 程序員需要重寫一些方法,當發生關注的事件,需要在方法中實現我們的業務邏輯,因爲當事件發生時,Netty 會回調對應的方法。

3)ChannelOutboundHandler 出站事件接口
在這裏插入圖片描述

  • bind 方法,當請求將 Channel 綁定到本地地址時調用
  • close 方法,當請求關閉 Channel 時調用等等
  • 出站操作都是一些連接和寫出數據類似的方法。

4)ChannelDuplexHandler 處理出站和入站事件
在這裏插入圖片描述

  • ChannelDuplexHandler 間接實現了入站接口並直接實現了出站接口。
  • 是一個通用的能夠同時處理入站事件和出站事件的類。

1.4 ChannelHandlerContext 作用及設計

  1. ChannelHandlerContext UML圖
    在這裏插入圖片描述
    ChannelHandlerContext 繼承了出站方法調用接口和入站方法調用接口

1)ChannelOutboundInvokerChannelInboundInvoker 部分源碼
在這裏插入圖片描述
在這裏插入圖片描述

  • 這兩個 invoker 就是針對入站或出站方法來的,就是在 入站或出站 handler 的外層再包裝一層,達到在方法前後攔截並做一些特定操作的目的

2)ChannelHandlerContext部分源碼
在這裏插入圖片描述

  • ChannelHandlerContext 不僅僅時繼承了他們兩個的方法,同時也定義了一些自己的方法
  • 這些方法能夠獲取 Context 上下文環境中對應的比如 channel,executor,handler ,pipeline,內存分配器,關聯的 handler 是否被刪除。
  • Context 就是包裝了 handler 相關的一切,以方便 Context 可以在 pipeline 方便的操作 handler

ChannelPipeline | ChannelHandler | ChannelHandlerContext 創建過程

分爲3個步驟來看創建的過程:

  • 任何一個 ChannelSocket 創建的同時都會創建 一個 pipeline。

  • 當用戶或系統內部調用 pipeline 的 add*** 方法添加 handler 時,都會創建一個包裝這 handler 的 Context。

  • 這些 Context 在 pipeline 中組成了雙向鏈表。

2.1 Socket 創建的時候創建 pipeline;在 SocketChannel 的抽象父類 AbstractChannel 的構造方法中

 protected AbstractChannel(Channel parent) {
        this.parent = parent; //斷點測試
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline(); 
    }

Debug 一下, 可以看到代碼會執行到這裏, 然後繼續追蹤到

 protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

說明:
1) 將 channel 賦值給 channel 字段,用於 pipeline 操作 channel。
2) 創建一個 future 和 promise,用於異步回調使用。
3) 創建一個 inbound 的 tailContext,創建一個既是 inbound 類型又是 outbound 類型的 headContext.在這裏插入圖片描述
在這裏插入圖片描述
4) 最後,將兩個 Context 互相連接,形成雙向鏈表。
5) tailContext 和 HeadContext 非常的重要,所有 pipeline 中的事件都會流經他們,

2.2 在 add** 添加 Handler 處理器的時候創建 Context** 看下 DefaultChannelPipeline 的 addLast 方法如何創建的 Context,代碼如下

@Override
    public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
        if (handlers == null) { //斷點
            throw new NullPointerException("handlers");
        }

        for (ChannelHandler h: handlers) {
            if (h == null) {
                break;
            }
            addLast(executor, null, h);
        }

        return this;
    }

繼續Debug

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() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        callHandlerAdded0(newCtx);
        return this;
    }

說明

  1. pipeline 添加 handler,參數是線程池,name 是null, handler 是我們或者系統傳入的handler。Netty 爲了防止多個線程導致安全問題,同步了這段代碼,步驟如下:
  2. 檢查這個 handler 實例是否是共享的,如果不是,並且已經被別的 pipeline 使用了,則拋出異常。
  3. 調用 newContext(group, filterName(name, handler), handler) 方法,創建一個 Context。從這裏可以看出來了,每次添加一個 handler 都會創建一個關聯 Context。
  4. 調用 addLast 方法,將 Context 追加到鏈表中。
  5. 如果這個通道還沒有註冊到 selecor 上,就將這個 Context 添加到這個 pipeline 的待辦任務中。當註冊好了以後,就會調用 callHandlerAdded0 方法(默認是什麼都不做,用戶可以實現這個方法)。
  6. 到這裏,針對三對象創建過程,瞭解的差不多了,和最初說的一樣,每當創建 ChannelSocket 的時候都會創建一個綁定的 pipeline,一對一的關係,創建 pipeline 的時候也會創建 tail 節點和 head 節點,形成最初的鏈表。tail 是入站 inbound 類型的 handler, head 既是 inbound 也是 outbound 類型的 handler。在調用 pipeline 的 addLast 方法的時候,會根據給定的 handler 創建一個 Context,然後,將這個 Context 插入到鏈表的尾端(tail 前面)。

在這裏插入圖片描述

ChannelPipeline 是如何調度 handler 的源碼剖析

在這裏插入圖片描述
在這裏插入圖片描述
DefaultChannelPipeline 是如何實現這些 fire 方法的

1.1 DefaultChannelPipeline 源碼

public class DefaultChannelPipeline implements ChannelPipeline {
@Override
    public final ChannelPipeline fireChannelActive() {
        AbstractChannelHandlerContext.invokeChannelActive(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelInactive() {
        AbstractChannelHandlerContext.invokeChannelInactive(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireExceptionCaught(Throwable cause) {
        AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
        return this;
    }

    @Override
    public final ChannelPipeline fireUserEventTriggered(Object event) {
        AbstractChannelHandlerContext.invokeUserEventTriggered(head, event);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelRead(Object msg) {
        AbstractChannelHandlerContext.invokeChannelRead(head, msg);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelReadComplete() {
        AbstractChannelHandlerContext.invokeChannelReadComplete(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelWritabilityChanged() {
        AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);
        return this;
    }
}

說明:
可以看出來,這些方法都是 inbound 的方法,也就是入站事件,調用靜態方法傳入的也是 inbound 的類型 head handler。這些靜態方法則會調用 head 的 ChannelInboundInvoker 接口的方法,再然後調用 handler 的真正方法
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

1.2 再看下piepline 的 outbound 的 fire 方法實現源碼

public class DefaultChannelPipeline implements ChannelPipeline {
 @Override
    public final ChannelFuture bind(SocketAddress localAddress) {
        return tail.bind(localAddress);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress) {
        return tail.connect(remoteAddress);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
        return tail.connect(remoteAddress, localAddress);
    }

    @Override
    public final ChannelFuture disconnect() {
        return tail.disconnect();
    }

    @Override
    public final ChannelFuture close() {
        return tail.close();
    }

    @Override
    public final ChannelFuture deregister() {
        return tail.deregister();
    }

    @Override
    public final ChannelPipeline flush() {
        tail.flush();
        return this;
    }

    @Override
    public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
        return tail.bind(localAddress, promise);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }

    @Override
    public final ChannelFuture connect(
            SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, localAddress, promise);
    }

    @Override
    public final ChannelFuture disconnect(ChannelPromise promise) {
        return tail.disconnect(promise);
    }
}

說明:

  1. 這些都是出站的實現,但是調用的是 outbound 類型的 tail handler 來進行處理,因爲這些都是 outbound 事件。
  2. 出站是 tail 開始,入站從 head 開始。因爲出站是從內部外面寫,從tail 開始,能夠讓前面的 handler 進行處理,防止由 handler 被遺漏,比如編碼。反之,入站當然是從 head 往內部輸入,讓後面的 handler 能夠處理這些輸入的數據。比如解碼。因此雖然 head 也實現了 outbound 接口,但不是從 head 開始執行出站任務
    在這裏插入圖片描述

2.關於如何調度,用一張圖來表示:
在這裏插入圖片描述

說明:

  1. pipeline 首先會調用 Context 的靜態方法 fireXXX,並傳入 Context
  2. 然後,靜態方法調用 Context 的 invoker 方法,而 invoker 方法內部會調用該 Context 所包含的 Handler 的真正的 XXX 方法,調用結束後,如果還需要繼續向後傳遞,就調用 Context 的 fireXXX2 方法,循環往復。責任鏈模式

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章