Netty-Pipeline深度解析

賜我白日夢發表於2019-07-20

首先我們知道,在NIO網路程式設計模型中,IO操作直接和channel相關,比如客戶端的請求連線,或者向服務端傳送資料, 服務端都要從客戶端的channel獲取這個資料

那麼channelPipeline是什麼?

其實,這個channelPepiline是Netty增加給原生的channel的元件,在ChannelPipeline介面的上的註解闡述了channelPipeline的作用,這個channelPipeline是高階過濾器的實現,netty將chanenl中資料導向channelPipeline,進而給了使用者對channel中資料的百分百的控制權, 此外,channelPipeline資料結構是雙向連結串列,每一個節點都是channelContext,channelContext裡面維護了對應的handler和pipeline的引用, 大概總結一下: 通過chanelPipeline,使用者客戶輕鬆的往channel寫資料,從channel讀資料

建立pipeline

通過前面幾篇部落格的追蹤,我們知道無論我們是通過反射建立出服務端的channel也好,還是直接new建立客戶端的channel也好,隨著父類建構函式的逐層呼叫,最終我們都會在Channel體系的頂級抽象類AbstractChannel中,建立出Channel的一大元件 channelPipeline

於是我們程式的入口,AbstractChannelpipeline = newChannelPipeline(); ,跟進去,看到他的原始碼如下:

protected DefaultChannelPipeline newChannelPipeline() {
    // todo 跟進去
    return new DefaultChannelPipeline(this);
}

可以看到,它建立了一個DefaultChannelPipeline(thisChannel)
DefaultChannelPipeline是channelPipeline的預設實現,他有著舉足輕重的作用,我們看一下下面的 Channel ChannelContext ChannelPipeline的繼承體系圖,我們可以看到圖中兩個類,其實都很重要,

pipeline和context的關係圖

他們之間有什麼關係呢?

當我們看完了DefaultChannelPipeline()構造中做了什麼自然就知道了

// todo 來到這裡
protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    // todo 把當前的Channel 儲存起來
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

    // todo  這兩方法很重要
    // todo 設定尾
    tail = new TailContext(this);
    // todo 設定頭
    head = new HeadContext(this);

    // todo 雙向連結串列關聯
    head.next = tail;
    tail.prev = head;
}

主要做了如下幾件事:

  • 初始化succeededFuture
  • 初始化voidPromise
  • 建立尾節點
  • 建立頭節點
  • 關聯頭尾節點

其實,到現在為止,pipiline的初始化已經完成了,我們接著往下看

此外,我們看一下DefaultChannelPipeline的內部類和方法,如下圖()

DefaultChannelPipeline

我們關注我圈出來的幾部分

  • 兩個重要的內部類
    • 頭結點 HeaderContext
    • 尾節點 TailContext
    • PendingHandlerAddedTask 新增完handler之後處理的任務
    • PendingHandlerCallBack 新增完handler的回撥
    • PengdingHandlerRemovedTask 移除Handler之後的任務
  • 大量的addXXX方法,
 final AbstractChannelHandlerContext head;
 final AbstractChannelHandlerContext tail;

跟進它的封裝方法:

TailContext(DefaultChannelPipeline pipeline) {
        super(pipeline, null, TAIL_NAME, true, false);
        setAddComplete();
    }
// todo 來到這裡
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
                              boolean inbound, boolean outbound) {
    this.name = ObjectUtil.checkNotNull(name, "name");
    // todo 為ChannelContext的pipeline附上值了
    this.pipeline = pipeline;
    this.executor = executor;
    this.inbound = inbound;
    this.outbound = outbound;
    // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
    ordered = executor == null || executor instanceof OrderedEventExecutor;
}

我們可以看到,這個tail節點是inbound型別的處理器,一開始確實很納悶,難道header不應該是Inbound型別的嗎? 我也不買關子了,直接說為啥,

因為,對netty來說用傳送過來的資料,要就從header節點開始往後傳播,怎麼傳播呢? 因為是雙向連結串列,直接找後一個節點,什麼型別的節點呢? inbound型別的,於是資料msg就從header之後的第一個結點往後傳播,如果說,一直到最後,都只是傳播資料而沒有任何處理就會傳播到tail節點,因為tail也是inbound型別的, tail節點會替我們釋放掉這個msg,防止記憶體洩露,當然如果我們自己使用了msg,而沒往後傳播,也沒有釋放,記憶體洩露是早晚的時,這就是為啥tail是Inbound型別的, header節點和它相反,在下面說

ok,現在知道了ChannelPipeline的建立了吧

Channelpipeline與ChannelHandler和ChannelHandlerContext之間的關係

它三者的關係也直接說了, 在上面pipeline的建立的過程中, DefaultChannelPipeline中的頭尾節點都是ChannelHandlerContext, 這就意味著, 在pipeline雙向連結串列的結構中,每一個節點都是一個ChannelHandlerContext, 而且每一個 ChannelHandlerContext維護一個handler,這一點不信可以看上圖,ChannelHandlerContext的實現類DefaultChannelHandlerContext的實現類, 原始碼如下:

final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
// todo  Context裡面有了 handler的引用
private final ChannelHandler handler;

// todo 建立預設的 ChannelHandlerContext,
DefaultChannelHandlerContext(
        DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
    super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    this.handler = handler;

ChannelHandlerContext 介面同時繼承ChannelOutBoundInvoker和ChannelInBoundInvoker使得他同時擁有了傳播入站事件和出站事件的能力, ChannelHandlerContext把事件傳播之後,是誰處理的呢? 當然是handler 下面給出ChannelHandler的繼承體系圖,可以看到針對入站出來和出站處理ChannelHandler有不同的繼承分支應對

channelHandler繼承體系

新增一個新的節點:

一般我們都是通過ChanelInitialezer動態的一次性新增多個handler, 下面就去看看,在服務端啟動過程中,ServerBootStrapinit(),如下原始碼:解析我寫在程式碼下面

// todo 這是ServerBootStrapt對 他父類初始化 channel的實現, 用於初始化 NioServerSocketChannel
@Override
void init(Channel channel) throws Exception {
// todo ChannelOption 是在配置 Channel 的 ChannelConfig 的資訊
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
    // todo 把 NioserverSocketChannel 和 options Map傳遞進去, 給Channel裡面的屬性賦值
    // todo 這些常量值全是關於和諸如TCP協議相關的資訊
    setChannelOptions(channel, options, logger);
}
    // todo 再次一波 給Channel裡面的屬性賦值  attrs0()是獲取到使用者自定義的業務邏輯屬性 --  AttributeKey
final Map<AttributeKey<?>, Object> attrs = attrs0();
// todo 這個map中維護的是 程式執行時的 動態的 業務資料 , 可以實現讓業務資料隨著netty的執行原來存進去的資料還能取出來
synchronized (attrs) {
    for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
        @SuppressWarnings("unchecked")
        AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
        channel.attr(key).set(e.getValue());
    }
}
// todo-------   options   attrs :   都可以在建立BootStrap時動態的傳遞進去
// todo ChannelPipeline   本身 就是一個重要的元件, 他裡面是一個一個的處理器, 說他是高階過濾器,互動的資料 會一層一層經過它
// todo 下面直接就呼叫了 p , 說明,在channel呼叫pipeline方法之前, pipeline已經被建立出來了!,
// todo 到底是什麼時候建立出來的 ?  其實是在建立NioServerSocketChannel這個通道物件時,在他的頂級抽象父類(AbstractChannel)中建立了一個預設的pipeline物件
/// todo 補充: ChannelHandlerContext 是 ChannelHandler和Pipeline 互動的橋樑
ChannelPipeline p = channel.pipeline();

// todo  workerGroup 處理IO執行緒
final EventLoopGroup currentChildGroup = childGroup;
// todo 我們自己新增的 Initializer
final ChannelHandler currentChildHandler = childHandler;

final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;


// todo 這裡是我們在Server類中新增的一些針對新連線channel的屬性設定, 這兩者屬性被acceptor使用到!!!
synchronized (childOptions) {
    currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
    currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}

// todo 預設 往NioServerSocketChannel的管道里面新增了一個 ChannelInitializer  ,
// todo  ( 後來我們自己新增的ChildHandler 就繼承了的這個ChannelInitializer , 而這個就繼承了的這個ChannelInitializer 實現了ChannelHandler)
p.addLast(new ChannelInitializer<Channel>() { // todo 進入addlast
    // todo  這個ChannelInitializer 方便我們一次性往pipeline中新增多個處理器
    @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            // todo  獲取bootStrap的handler 物件, 沒有返回空
            // todo  這個handler 針對bossgroup的Channel  , 給他新增上我們在server類中新增的handler()裡面新增處理器
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            // todo ServerBootstrapAcceptor 接收器, 是一個特殊的chanelHandler
             ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    // todo !!! --   這個很重要,在ServerBootStrap裡面,netty已經為我們生成了接收器  --!!!
                    // todo 專門處理新連線的接入, 把新連線的channel繫結在 workerGroup中的某一條執行緒上
                    // todo 用於處理使用者的請求, 但是還有沒搞明白它是怎麼觸發執行的
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            // todo 這些引數是使用者自定義的引數
                            // todo NioServerSocketChannel, worker執行緒組  處理器   關係的事件
                            ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
    }
});

這個函式真的是好長,但是我們的重點放在ChannelInitializer身上, 現在的階段, 當前的channel還沒有註冊上EventLoop上的Selector中

還有不是分析怎麼新增handler? 怎麼來這裡了? 其實下面的 ServerBootstrapAcceptor就是一個handler

我們看一下上面的程式碼做了啥

ch.eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        // todo !!! --   這個很重要,在ServerBootStrap裡面,netty已經為我們生成了接收器  --!!!
        // todo 專門處理新連線的接入, 把新連線的channel繫結在 workerGroup中的某一條執行緒上
        // todo 用於處理使用者的請求, 但是還有沒搞明白它是怎麼觸發執行的
        pipeline.addLast(new ServerBootstrapAcceptor(
                // todo 這些引數是使用者自定義的引數
                // todo NioServerSocketChannel, worker執行緒組  處理器   關係的事件
                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
    }
});

懵逼不? 當時真的是給我整蒙圈了, 還沒有關聯上 EventLoop呢!!! 哪來的ch.eventLoop()....

後來整明白了,這其實是一個回撥,netty提供給使用者在任意時刻都可以往pipeline中新增handler的實現手段

那麼在哪裡回撥呢? 其實是在 jdk原生的channel組冊進EventLoop中的Selector後緊接著回撥的,原始碼如下

private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // todo 進入這個方法doRegister()
        // todo 它把系統建立的ServerSocketChannel 註冊進了選擇器
        doRegister();
        neverRegistered = false;
        registered = true;
        
        // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
        // user may already fire events through the pipeline in the ChannelFutureListener.
        // todo 確保在 notify the promise前呼叫 handlerAdded(...)
        // todo 這是必需的,因為使用者可能已經通過ChannelFutureListener中的管道觸發了事件。
        // todo 如果需要的話,執行HandlerAdded()方法
        // todo 正是這個方法, 回撥了前面我們新增 Initializer 中新增 Accpter的重要方法
        pipeline.invokeHandlerAddedIfNeeded();

回撥函式在pipeline.invokeHandlerAddedIfNeeded();, 看它的命名, 如果需要的話,執行handler已經新增完成了操作 哈哈,我們現在當然需要,剛新增了個ServerBootstrapAcceptor

在跟進入看原始碼之間,注意,方法是pipeline呼叫的, 那個pipeline呢? 就是上面我們說的DefaultChannelPipeline, ok,跟進原始碼,進入 DefaultChannelPipeline

// todo 執行handler的新增,如果 需要的話
final void invokeHandlerAddedIfNeeded() {
    assert channel.eventLoop().inEventLoop();
    if (firstRegistration) {
        firstRegistration = false;
        // todo 現在我們的channel已經註冊在bossGroup中的eventLoop上了, 是時候回撥執行那些在註冊前新增的 handler了
        callHandlerAddedForAllHandlers();
    }
}

呼叫本類方法callHandlerAddedForAllHandlers(); 繼續跟進下


// todo 回撥原來在沒有註冊完成之前新增的handler
private void callHandlerAddedForAllHandlers() {
    final PendingHandlerCallback pendingHandlerCallbackHead;
    synchronized (this) {
        assert !registered;

        // This Channel itself was registered.
        registered = true;

        pendingHandlerCallbackHead = this.pendingHandlerCallbackHead;
        // Null out so it can be GC'ed.
        this.pendingHandlerCallbackHead = null;
    }
  PendingHandlerCallback task = pendingHandlerCallbackHead;
    while (task != null) {
        task.execute();
        task = task.next;
    }
}

我們它的動作task.execute();

其中的task是誰? pendingHandlerCallbackHead 這是DefaultChannelPipeline的內部類, 它的作用就是輔助完成 新增handler之後的回撥, 原始碼如下:

private abstract static class PendingHandlerCallback implements Runnable {
    final AbstractChannelHandlerContext ctx;
    PendingHandlerCallback next;

    PendingHandlerCallback(AbstractChannelHandlerContext ctx) {
        this.ctx = ctx;
    }

    abstract void execute();
}

我們跟進上一步的task.execute()就會看到它的抽象方法,那麼是誰實現的呢? 實現類是PendingHandlerAddedTask同樣是DefaultChannelPipeline的內部類, 既然不是抽象類了, 就得同時實現他父類PendingHandlerCallback的抽象方法,其實有兩個一是個excute()另一個是run() --Runable

我們進入看它是如何實現excute,原始碼如下:

@Override
void execute() {
EventExecutor executor = ctx.executor();
if (executor.inEventLoop()) {
    callHandlerAdded0(ctx);
} else {
    try {
        executor.execute(this);
    } catch (RejectedExecutionException e) {
        if (logger.isWarnEnabled()) {
            logger.warn(
                    "Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.",
                    executor, ctx.name(), e);
        }
        remove0(ctx);
        ctx.setRemoved();
    }
}

HandlerAdded()的回撥時機

我們往下追蹤, 呼叫類本類方法callHandlerAdded0(ctx); 原始碼如下:

// todo 重點看看這個方法 , 入參是剛才新增的  Context
private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
try {
    // todo 在channel關聯上handler之後並且把Context新增到了 Pipeline之後進行呼叫!!!
    ctx.handler().handlerAdded(ctx); // todo 他是諸多的回撥方法中第一個被呼叫的
    ctx.setAddComplete();  // todo 修改狀態
}
...

繼續往下追蹤

  • ctx.handler() -- 獲取到了當前的channel
  • 呼叫channel的.handlerAdded(ctx);

這個handlerAdded()是定義在ChannelHandler中的回撥方法, 什麼時候回撥呢? 當handler新增後回撥, 因為我們知道,當服務端的channel在啟動時,會通過 channelInitializer 新增那個ServerBootstrapAcceptor,所以ServerBootstrapAcceptorhandlerAdded()的回撥時機就在上面程式碼中的ctx.handler().handlerAdded(ctx);

如果直接點選去這個函式,肯定就是ChannelHandler介面中去; 那麼 新的問題來了,誰是實現類? 答案是抽象類 ChannelInitializer`就在上面我們新增ServerBootstrapAcceptor就建立了一個ChannelInitializer`的匿名物件

它的繼承體系圖如下:

ChannelInitializer繼承圖

介紹這個ChannelInitializer 他是Netty提供的輔助類,用於提供針對channel的初始化工作,什麼工作呢? 批量初始化channel

這個中有三個重要方法,如下

  • 重寫的channel的handlerAdded(), 這其實也是handlerAdded()的回撥的體現
  • 自己的initChannel()
  • 自己的remove()

繼續跟進我們上面的handlerAdded(ChannelHandlerContext ctx) 原始碼如下:

   @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
                initChannel(ctx); // todo 這個方法在上面, 進入 可以在 finally中 找到移除Initializer的邏輯
            }
    }

呼叫本類的initChannel(ctx); 原始碼如下:

  private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
        if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
            try {
                initChannel((C) ctx.channel());
            } catch (Throwable cause) {
                // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
                // We do so to prevent multiple calls to initChannel(...).
                exceptionCaught(ctx, cause);
            } finally {
                // todo    remove(ctx);  刪除 ChannelInitializer
                remove(ctx);
            }
            return true;
        }
        return false;
    }

兩個點

  • 第一: 繼續呼叫本類的抽象方法initChannel((C) ctx.channel());
  • 第二: 移除了remove(ctx);

分開進行第一步

initChannel((C) ctx.channel()); 初始化channel,這個函式被設計成了抽象的, 問題來了, 實現類是誰? 實現類其實剛才說了,就是netty在新增ServerBootStrapAcceptor時建立的那個匿名內部類,我們跟進去看他的實現: 原始碼如下:

 @Override
    public void initChannel(final Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();
        // todo  獲取bootStrap的handler 物件, 沒有返回空
        // todo  這個handler 針對bossgroup的Channel  , 給他新增上我們在server類中新增的handler()裡面新增處理器
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }

        // todo ServerBootstrapAcceptor 接收器, 是一個特殊的chanelHandler
         ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                // todo !!! --   這個很重要,在ServerBootStrap裡面,netty已經為我們生成了接收器  --!!!
                // todo 專門處理新連線的接入, 把新連線的channel繫結在 workerGroup中的某一條執行緒上
                // todo 用於處理使用者的請求, 但是還有沒搞明白它是怎麼觸發執行的
                pipeline.addLast(new ServerBootstrapAcceptor(
                        // todo 這些引數是使用者自定義的引數
                        // todo NioServerSocketChannel, worker執行緒組  處理器   關係的事件
                        ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
}

實際上就是完成了一次方法的回撥,成功新增了ServerBootstrapAcceptor處理器

刪除一個節點

回來看第二步

remove(ctx); 刪除一個節點, 把Initializer刪除了? 是的, 把這個初始化器刪除了, 為啥要把它刪除呢, 說了好多次, 其實他是一個輔助類, 目的就是通過他往channel中一次性新增多個handler, 現在handler也新增完成了, 留著他也沒啥用,直接移除了

我們接著看它的原始碼

 // todo 刪除當前ctx 節點
    private void remove(ChannelHandlerContext ctx) {
        try {
            ChannelPipeline pipeline = ctx.pipeline();
            if (pipeline.context(this) != null) {
                pipeline.remove(this);
            }
        } finally {
            initMap.remove(ctx);
        }
    }

從pipeline中移除, 一路看過去,就會發現地城刪除連結串列節點的操作

private static void remove0(AbstractChannelHandlerContext ctx) {
    AbstractChannelHandlerContext prev = ctx.prev;
    AbstractChannelHandlerContext next = ctx.next;
    prev.next = next;
    next.prev = prev;
}

inbound事件的傳播

什麼是inbound事件

inbound事件其實就是客戶端主動發起事件,比如說客戶端請求連線,連線後客戶端有主動的給服務端傳送需要處理的有效資料等,只要是客戶端主動發起的事件,都算是Inbound事件,特徵就是事件觸發型別,當channel處於某個節點,觸發服務端傳播哪些動作

netty如何對待inbound

netty為了更好的處理channel中的資料,給jdk原生的channel新增了pipeline元件,netty會把原生jdk的channel中的資料導向這個pipeline,從pipeline中的header開始 往下傳播, 使用者對這個過程擁有百分百的控制權,可以把資料拿出來處理, 也可以往下傳播,一直傳播到tail節點,tail節點會進行回收,如果在傳播的過程中,最終沒到尾節點,自己也沒回收,就會面臨記憶體洩露的問題

一句話總結,面對Inbound的資料, 被動傳播

netty知道客戶端傳送過來的資料是啥型別嗎?

比如一個聊天程式,客戶端可能傳送的是心跳包,也可能傳送的是聊天的內容,netty又不是人,他是不知道資料是啥的,他只知道來了資料,需要進一步處理,怎麼處理呢? 把資料導向使用者指定的handler鏈條

開始讀原始碼

這裡書接上一篇部落格的尾部,事件的傳播
重點步驟如下

第一步: 等待服務端啟動完成

第二步: 使用telnet模擬傳送請求 --- > 新連線的接入邏輯

第三步: register0(ChannelPromise promise)方法中會傳播channel啟用事件 --> 目的是二次註冊埠,

第三個也是我們程式的入手點: fireChannelActive() 原始碼如下:

@Override
public final ChannelPipeline fireChannelActive() {
    // todo ChannelActive從head開始傳播
    AbstractChannelHandlerContext.invokeChannelActive(head);
    return this;
}

呼叫了AbstractChannelHandlerContextinvokeChannelActive方法

在這裡,我覺得特別有必須有必要告訴自己 AbstractChannelHandlerContext的重要性,現在的DefaultChannelPipeline中的每一個節點,包括header,tail,以及我們自己的新增的,都是AbstractChannelHandlerContext型別,事件的傳播圍繞著AbstractChannelHandlerContext的方法開始,溫習它的繼承體系如下圖

pipeline和context的關係圖

接著回到AbstractChannelHandlerContext.invokeChannelActive(head); , 很顯然,這是個靜態方法, 跟進去,原始碼如下:

// todo 來這
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
    next.invokeChannelActive();
...
}
  • 第一點: inbound型別的事件是從header開始傳播的 , next --> HeaderContext
  • 第二點: HeaderContext其實就是 AbstractChannelHandlerContext型別的,所以 invokeChannelActive()其實是當前類的方法

ok,跟進入看看他幹啥了,原始碼:

// todo 使Channel活躍
private void invokeChannelActive() {
// todo 繼續進去
((ChannelInboundHandler) handler()).channelActive(this);
}

我們看, 上面的程式碼做了如下幾件事

  • handler() -- 返回當前的 handler, 就是從HandlerContext中拿出handler
  • 強轉成ChannelInboundHandler型別的,因為他是InBound型別的處理器

如果我們用滑鼠點選channelActive(this), 毫無疑問會進入ChannelInboundHandler,看到的是抽象方法

那麼問題來了, 誰實現的它?

其實是headerContext 頭結點做的, 之前說過,Inbound事件,是從header開始傳播的,繼續跟進去, 看原始碼:

// todo 來到這裡, 分兩步, 1. 把ChannelActive事件繼續往下傳播, 傳播結束之後,做第二件事
// todo                  2.     readIfIsAutoRead();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// todo  fireChannelActive是在做了實際的埠繫結之後才觸發回撥
ctx.fireChannelActive();

// todo 預設方式註冊一個read事件
// todo 跟進去, readIfIsAutoRead 作用是 把已經註冊進selector上的事件, 重新註冊繫結上在初始化NioServerSocketChannel時新增的Accept事件
// todo 目的是 新連線到來時, selector輪詢到accept事件, 使得netty可以進一步的處理這個事件
readIfIsAutoRead();
}

其實這裡有兩種重要的事情 , 上面我們也看到了:

  • 向下傳播channelActive() 目的是讓header後面的使用者新增的handler中的channelActive()被回撥
  • readIfIsAutoRead(); 就是去註冊Netty能看懂的感興趣的事件

下面我們看它的事件往下傳播, 於是重新回到了AbstractChannelHandlerContext, 原始碼如下:

public ChannelHandlerContext fireChannelActive() {
        invokeChannelActive(findContextInbound());
        return this;
    }
  • findContextInbound()找出下一個Inbound型別的處理器, 我們去看他的實現,原始碼如下:
 private AbstractChannelHandlerContext findContextInbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.next;
    } while (!ctx.inbound);
    return ctx;
}

是不是明明白白的? 從當前節點開始,往後變數整個連結串列, 下一個節點是誰呢? 在新連結接入的邏輯中,呼叫的ChannelInitializer我手動 批量新增了三個InboundHandler,按照我新增的順序,他們會依次被找到

繼續跟進本類方法invokeChannelActive(findContextInbound()),原始碼如下

  // todo 來這
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelActive();
...

一開始的next--> HeaderContext
現在的 next就是header之後,我手動新增的Inbound的handler

同樣是呼叫本類方法invokeChannelActive(),原始碼如下:

// todo 使Channel活躍
private void invokeChannelActive() {
    // todo 繼續進去
    ((ChannelInboundHandler) handler()).channelActive(this);

再次看到,回撥, 我新增的handler.channelActive(this); ,進入檢視

public class MyServerHandlerA extends ChannelInboundHandlerAdapter {
// todo  當服務端的channel繫結上埠之後,就是 傳播 channelActive 事件
// todo   事件傳播到下面後,我們手動傳播一個 channelRead事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ctx.channel().pipeline().fireChannelRead("hello MyServerHandlerA");
}

在我處理器中,繼續往下傳播手動新增的資料"hello MyServerHandlerA"

同樣她會按找上面的順序依次傳播下去

最終她會來到tail , 在tail做了如下的工作, 原始碼如下

 @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // todo channelRead
    onUnhandledInboundMessage(msg);
}

protected void onUnhandledInboundException(Throwable cause) {
try {
    logger.warn(
            "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
                    "It usually means the last handler in the pipeline did not handle the exception.",
            cause);
} finally {
    ReferenceCountUtil.release(cause);
}
}  

為什麼Tail節點是 Inbound 型別的處理器?

上一步就說明了為什麼Tail為什麼設計成Inbound, channel中的資料,無論服務端有沒有使用,最終都要被釋放掉,tail可以做到收尾的工作, 清理記憶體


outbound事件的傳播

什麼是outBound事件

建立的outbound事件如: connect,disconnect,bind,write,flush,read,close,register,deregister, outbound型別事件更多的是服務端主動發起的事件,如給主動channel繫結上埠,主動往channel寫資料,主動關閉使用者的的連線

開始讀原始碼

最典型的outbound事件,就是服務端往客戶端寫資料,準備測試用例如下:

public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerB");
        ctx.write(ctx, promise);
    }
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        ctx.executor().schedule(()->{
            // todo 模擬給 客戶端一個響應
            ctx.channel().write("Hello World");
            // 寫法二 :  ctx.write("Hello World");
        },3, TimeUnit.SECONDS);
    }
}
public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter {
    // todo  當服務端的channel繫結上埠之後,就是 傳播 channelActive 事件
    // todo   事件傳播到下面後,我們手動傳播一個 channelRead事件
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerA");
        ctx.write(ctx, promise);
    }
}
public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerC");
        ctx.write(ctx, promise);
    }
}

下面我們把斷點除錯,把斷點打在OutBoundHandlerBhandlerAdded上, 模擬向客戶端傳送資料, 啟動程式,大概的流程如下

  • 等待服務端的啟動
  • 服務端Selector輪詢服務端channel可能發生的感興趣的事件
  • 使用telnet向服務端傳送請求
  • 服務端建立客戶端的channel,在給客戶端的原生的chanenl註冊到 Selector上
  • 通過invokeChannelAddedIfNeeded()將我們新增在Initializer中的handler新增到pipeline中
    • 挨個回撥這些handler中的channelAdded()方法
      • 和我們新增進去的順序相反
      • C --> B --->A
    • 這些childHandler,會新增在每一條客戶端的channel的pipeline
  • 傳播channel註冊完成事件
  • 傳播channelActive事件
    • readIfAutoRead() 完成二次註冊netty可以處理的感興趣的事件

此外,我們上面的write以定時任務的形式提交,當用ctx中的唯一的執行緒執行器三秒後去執行任務,所以程式會繼續下去繫結埠, 過了三秒後把定時任務聚合到普通任務佇列中,那時才會執行我們OutBoundHandlerB中的ctx.channel().write("Hello World");

outBound型別的handler新增順序和執行順序有什麼關係

因為Outbound型別的事件是從連結串列的tail開始傳播的,所以執行的順序和我們的新增進去的順序相反

篇幅太長了,重寫補一張圖

channelHandler繼承體系

ctx.channel().write("Hello World");開始跟原始碼, 滑鼠直接跟進去,進入的是ChannelOutboundInvoker, 往channel中寫,我們進入DefaultChannelPipeline的實現,原始碼如下

@Override
public final ChannelFuture write(Object msg) {
    return tail.write(msg);
}

再一次的驗證了,出站的事件是從尾部往前傳遞的, 我們知道,tail節點是DefaultChannelHandlerContext型別的,所以我們看它的write()方法是如何實現的

@Override
public ChannelFuture write(Object msg) {
    return write(msg, newPromise());
}

其中msg-->我們要寫會客戶端的內容, newPromise()預設的promise()
,繼續跟進本類方法write(msg, newPromise()),原始碼如下:

@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
    if (msg == null) {
        throw new NullPointerException("msg");
    }

    try {
        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return promise;
        }
    } catch (RuntimeException e) {
        ReferenceCountUtil.release(msg);
        throw e;
    }
    write(msg, false, promise);

    return promise;
}

上面做了很多判斷,其中我們只關心write(msg, false, promise); 原始碼如下:

private void write(Object msg, boolean flush, ChannelPromise promise) {
    AbstractChannelHandlerContext next = findContextOutbound();
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }

我們可以看到,重要的邏輯findContextOutbound(); 它的原始碼如下, 從尾節點開始遍歷連結串列,找到前一個outbound型別的handler

private AbstractChannelHandlerContext findContextOutbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.prev;
    } while (!ctx.outbound);
    return ctx;
}

找到後,因為我們使用函式是write而不是writeAndFlush所以進入上面的else程式碼塊invokeWrite

private void invokeWrite(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
    } else {
        write(msg, promise);
    }
}

繼續跟進invokeWrite0(msg, promise); 終於看到了handler的write邏輯

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}

其中:

  • (ChannelOutboundHandler) handler() -- 是tail前面的節點
  • 呼叫當前節點的write函式

實際上就是回撥我們自己的新增的handler的write函式,我們跟進去,原始碼如下:

public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerC");
        ctx.write(msg, promise);
    }
}

我們繼續呼叫write, 按照相同的邏輯,msg會繼續往前傳遞

一直傳遞到HeadContext節點, 因為這個節點也是Outbound型別的, 這就是Outbound事件的傳播,我們直接看HeaderContext是如何收尾的, 原始碼如下:

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    unsafe.write(msg, promise);
}

Header使用了unsafe類,這沒毛病,和資料讀寫有關的操作,最終都離不開unsafe

為什麼Header節點是outBound型別的處理器?

拿上面的write事件來說,msg經過這麼多handler的加工,最終的目的是傳遞到客戶端,所以netty把header設計為outBound型別的節點,由他完成往客戶端的寫

context.write()與context.channel().write()的區別

  • context.write(),會從當前的節點開始往前傳播
  • context.channel().write() 從尾節點開始依次往前傳播

異常的傳播

netty中如果發生了異常的話,異常事件的傳播和當前的節點是 入站和出站處理器是沒關係的,一直往下一個節點傳播,如果一直沒有handler處理異常,最終由tail節點處理

最佳的異常處理解決方法

既然異常的傳播和入站和出站型別的處理器沒關係,那麼我們就在pipeline的最後,也就是tail之前,新增我們的統一異常處理器就好了, 就像下面:

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
      // todo 異常處理的最佳實踐, 最pipeline的最後新增異常處理handler
      channelPipeline.addLast(new myExceptionCaughtHandler());
    }
}

public class myExceptionCaughtHandler extends ChannelInboundHandlerAdapter {
// 最終全部的異常都會來到這裡
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    if (cause instanceof 自定義異常1){
    }else if(cause instanceof  自定義異常2){
    }
    // todo 下面不要往下傳播了
     // super.exceptionCaught(ctx, cause);
}
}

SimpleChannelInboundHandler 的特點

通過前面的分析,我們知道如果客戶端的msg一味的往後傳播,最終會傳播到tail節點,由tail節點處理釋放,從而避免了記憶體的洩露

如果我們的handler使用了msg之後沒有往後傳遞就要倒黴了,時間久了就會出現記憶體洩露的問題

netty人性化的為我們提供的指定泛型的 SimpleChannelInboundHandler<T> ,可以為我們自動的釋放記憶體,我們看他是如何做到的

/ todo 直接繼承於ChanelInboundHandlerAdapter的實現 抽象類
// todo 我們自己的處理器, 同樣可以繼承SimpleChannelInboundHandler介面卡,達到相同的效果
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {

    private final TypeParameterMatcher matcher;
    private final boolean autoRelease;
    protected SimpleChannelInboundHandler() {
        this(true);
    }
    protected SimpleChannelInboundHandler(boolean autoRelease) {
        matcher = TypeParameterMatcher.find(this, SimpleChannelInboundHandler.class, "I");
        this.autoRelease = autoRelease;
    }

    protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType) {
        this(inboundMessageType, true);
    }

    protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType, boolean autoRelease) {
        matcher = TypeParameterMatcher.get(inboundMessageType);
        this.autoRelease = autoRelease;
    }

    public boolean acceptInboundMessage(Object msg) throws Exception {
        return matcher.match(msg);
    }

    // todo  channelRead 完全被改寫了
    // todo 這其實又是一種設計模式 ,   模板方法設計模式
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                // todo 把訊息進行了強轉
                I imsg = (I) msg;
                //  todo channelRead0()在他的父類中是抽象的,因此我們自己寫handler時,需要重寫它的這個抽象的 方法 , 在下面
                // todo 這其實又是一種設計模式 ,   模板方法設計模式
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {// todo 對msg的計數減一, 表示對訊息的引用減一. 也就意味著我們不要在任何
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }
    protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
}
  • 它本身是抽象類,抽象方法是channelRead0,意味著我們需要重寫這個方法
  • 他繼承了ChannelInboundHandlerAdapter 這是個介面卡類,使他可以僅實現部分自己需要的方法就ok

我們看它實現的channelRead, 模板方法設計模式 主要做了如下三件事

  • 將msg 強轉成特定的泛型型別的資料
  • 將ctx和msg傳遞給自己的chanenlRead0使用msg和ctx(ctx,msg)
    • chanenlRead0使用msg和ctx
  • 在finally程式碼塊中,將msg釋放

相關文章