探究netty的觀察者設計模式

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

javadoc筆記點

觀察者的核心思想就是,在適當的時機回撥觀察者的指定動作函式

我們知道,在使用netty建立channel時,一般都是把這個channel設定成非阻塞的模式,這意味著什麼呢? 意味著所有io操作一經呼叫,即刻返回

這讓netty對io的吞吐量有了飛躍性的提升,但是非同步程式設計相對於傳統的序列化的程式設計模式來說,控制起來可太麻煩了

jdk提供了原生的Futrue介面,意為在未來任務,其實就是把任務封裝起來交給新的執行緒執行,在這個執行緒執行任務的期間,我們的主執行緒可以騰出時間去做別的事情

下面的netty給出的例項程式碼,我們可以看到,任務執行緒有返回一個Futrue物件,這個物件中封裝著任務執行的情況

 *  *   void showSearch(final String target)
 *  *       throws InterruptedException {
 *  *     Future<String> future
 *  *       = executor.submit(new Callable<String>() {
 *  *         public String call() {
 *  *             return searcher.search(target);
 *  *         }});
 *  *     displayOtherThings(); // do other things while searching
 *  *     try {
 *  *       displayText(future.get()); // use future
 *  *     } catch (ExecutionException ex) { cleanup(); return; }
 *  *   }
 *

雖然jdk原生Futrue可以實現非同步提交任務,並且返回了任務執行資訊的Futrue,但是有一個致命的缺點,從futrue獲取任務執行情況方法,是阻塞的,這是不被允許的,因為在netty中,一條channel可能關係著上千的客戶端的連結,其中一個客戶端的阻塞導致幾千的客戶端不可用是不被允許的,netty的Future設計成,繼承jdk原生的future,而且進行擴充套件如下

// todo 這個介面繼承了 java併發包總的Futrue  , 並在其基礎上增加了很多方法
// todo  Future 表示對未來任務的封裝
public interface Future<V> extends java.util.concurrent.Future<V> {

   // todo 判斷IO是否成功返回
   boolean isSuccess();

   // todo 判斷是否是 cancel()方法取消
   boolean isCancellable();

   // todo 返回IO 操作失敗的原因
   Throwable cause();
    
   /**
    *  todo 使用了觀察者設計模式, 給這個future新增監聽器, 一旦Future 完成, listenner 立即被通知
    */
   Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
   
   // todo 新增多個listenner
   Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);

   Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);

   // todo 移除多個 listenner
   Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
  
   // todo  sync(同步) 等待著 future 的完成, 並且,一旦future失敗了,就會丟擲 future 失敗的原因
   // todo bind()是個非同步操作,我們需要同步等待他執行成功
   Future<V> sync() throws InterruptedException;
 
   // todo  不會被中斷的 sync等待
   Future<V> syncUninterruptibly();

   // todo 等待
   Future<V> await() throws InterruptedException;

   Future<V> awaitUninterruptibly();

   // todo 無阻塞的返回Future物件, 如果沒有,返回null
   // todo  有時 future成功執行後返回值為null, 這是null就是成功的標識, 如 Runable就沒有返回值, 因此文件建議還要 通過isDone() 判斷一下真的完成了嗎

   V getNow();

    @Override
   boolean cancel(boolean mayInterruptIfRunning);
   ...

netty的觀察者模式

最常用的關於非同步執行的方法writeAndFlush()就是典型的觀察者的實現, 在netty中,當一個IO操作剛開始的時候,一個ChannelFutrue物件就會建立出來,此時,這個futrue物件既不是成功的,也不是失敗的,更不是被取消的,因為這個IO操作還沒有結束

如果我們想在IO操作結束後立刻執行其他的操作時,netty推薦我們使用addListenner()新增監聽者的方法而不是使用await()阻塞式等待,使用監聽者,我們就不用關係具體什麼時候IO操作結束,只需要提供回撥方法就可以,當IO操作結束後,方法會自動被回撥

在netty中,一個IO操作是狀態分為如下幾種

 *                                      +---------------------------+
 *                                      | Completed successfully    |
 *                                      +---------------------------+
 *                                 +---->      isDone() = true      |
 * +--------- -----------------+    |    |   isSuccess() = true      |
 * |        Uncompleted       |    |    +===========================+
 * +--------------------------+    |    | Completed with failure    |
 * |      isDone() = false    |    |    +---------------------------+
 * |   isSuccess() = false    |----+---->      isDone() = true      |
 * | isCancelled() = false    |    |    |   cause() = non-null  非空|
 * |       cause() = null     |    |    +===========================+
 * +--------------------------+    |    | Completed by cancellation |
 *                                 |    +---------------------------+
 *                                 +---->      isDone() = true      |
 *                                      | isCancelled() = true      |
 *                                      +---------------------------+

原始碼追蹤

對writeAndFlush的使用

ChannelFuture channelFuture = ctx.writeAndFlush("from client : " + UUID.randomUUID());
channelFuture.addListener(future->{
    if(future.isSuccess()){
        todo
    }else{
        todo
    }
});

注意點: 我們使用writeAndFlush() 程式立即返回,隨後我們使用返回的物件新增監聽者,新增回撥,這個時writeAndFlush()有可能已經完成了,也有可能沒有完成,這是不確定的事

首先我們知道,writeAndFlush()是出站的動作,屬於channelOutboundHandler,而且他是從pipeline的尾部開始傳播的,原始碼如下:

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

尾節點資料AbstractChannelHandlerContext類, 繼續跟進檢視原始碼如下:

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

    @Override
    public ChannelPromise newPromise() {
        return new DefaultChannelPromise(channel(), executor());
    }

悄無聲息的做了一個很重要的事情,建立了Promise,這個DefaultChannelPromise就是被觀察者,過一會由它完成方法的回撥

繼續跟進writeAndFlush() ,原始碼如下, 我們可以看到promise被返回了, DefaultChannelPromiseChannelPromise的實現類,而ChannelPromise又繼承了ChannelFuture,這也是為什麼明明每次使用writeAndFlush()返回的都是ChannelFuture而我們這裡卻返回了DafaultChannelPromise

// todo 呼叫本類的 write(msg, true, promise)
@Override
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
    if (msg == null) {
        throw new NullPointerException("msg");
    }
    if (isNotValidPromise(promise, true)) {
        ReferenceCountUtil.release(msg);
        return promise;
    }
    write(msg, true, promise);
    return promise;

在去目標地之前,先看一下addListenner()幹了什麼,我們進入到DefaultChannelPromise 原始碼如下:

@Override
public ChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener) {
    super.addListener(listener);
    return this;
}

隨機進入它的父類 DefaultChannelPromise中

@Override
public Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {
    checkNotNull(listener, "listener");
    synchronized (this) {
        addListener0(listener);
    }
    if (isDone()) {
        notifyListeners();
    }
    return this;
}

這個函式分兩步進行

第一步: 為什麼新增監聽事件的方法需要同步?

在這種多執行緒併發執行的情況下,這個addListener0(listener);任意一個執行緒都能使用,存在同步新增的情況 這個動作不像將channel和EventLoop做的唯一繫結一樣,沒有任何必須使用inEventloop()去判斷在哪個執行緒中,直接使用同步

接著進入addListener0(listener)

private void addListener0(GenericFutureListener<? extends Future<? super V>> listener) {
    if (listeners == null) {
        listeners = listener; // todo 第一次新增直接在這裡賦值
    } else if (listeners instanceof DefaultFutureListeners) {
         // todo 第三次新增呼叫這裡
        ((DefaultFutureListeners) listeners).add(listener);
    } else {
        // todo 第二次新增來這裡複製, 由這個 DefaultFutureListeners 存放觀察者 
        listeners = new DefaultFutureListeners((GenericFutureListener<?>) listeners, listener);
    }
}

第二步: 為什麼接著判斷isDone()

writeAndFlush()是非同步執行的,而且在我們新增監聽者的操作之前已經開始執行了,所以在新增完監聽者之後,立即驗證一把,有沒有成功

思考一波:

回顧writeAndFlush()的呼叫順序,從tail開始傳播兩波事件,第一波write,緊接著第二波flush,一直傳播到header,進入unsafe類中,由他完成把據寫入jdk原生ByteBuffer的操作, 所以按理說,我們新增是listenner的回撥就是在header的unsafe中完成的,這是我們的目標地

任何方法的回撥都是提前設計好了的,就像pipeline中的handler中的方法的回撥,就是通過遍歷pipeline內部的連結串列實現的,這裡的通知觀察者,其實也是呼叫觀察者的方法,而且他使用的一定是觀察的父類及以上的引用實現的方法回撥

回到我們的writeAndFlush()這個方法,在第二波事務傳遞完成,將資料真正寫入jdk原生的ByteBuffer之前,只有進行的所有回撥都是設定失敗的狀態,直到把資料安全發出後才可能是 回撥成功的操作

此外,想要進行回撥的操作,就得有被觀察的物件的引用,所以一會我就回看到,Promise 一路被傳遞下去

我們進入的unsafe的write()就可以看到與回撥相關的操作safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);,原始碼如下

@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) { // todo 快取 寫進來的 buffer

    safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
    
    ReferenceCountUtil.release(msg);
    return;
}

我們繼續跟進本類方法safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);, 原始碼如下:

protected final void safeSetFailure(ChannelPromise promise, Throwable cause) {
    if (!(promise instanceof VoidChannelPromise) && !promise.tryFailure(cause)) {
        logger.warn("Failed to mark a promise as failure because it's done already: {}", promise, cause);
    }
}

其中重要的方法,就是回撥 被觀察者的 tryFailure(cause), 這個被觀察者的型別是ChannelPromise, 我們去看它的實現,原始碼如下

@Override
public boolean tryFailure(Throwable cause) {
    if (setFailure0(cause)) {
        notifyListeners();
        return true;
    }
    return false;
}

呼叫本類方法notifyListeners()

繼續跟進本類方法notifyListenersNow();

接著跟進本類方法notifyListener0(this, (GenericFutureListener<?>) listeners);

繼續l.operationComplete(future); 終於看到了呼叫了監聽者的完成操作,實際上就是回撥使用者的方法,雖然是完成的,但是失敗了


下面我們去flush()中去檢視通知成功的回撥過程, 方法的呼叫順序如下

flush();
flush0();
doWrite(outboundBuffer);

在doWrite()方法中,就會使用自旋的方式往嘗試把資料寫出去, 資料被寫出去後,有一個標識 done=true, 證明是成功寫出了, 緊接著就是把當前的盛放ByteBuf的entry從連結串列上移除,原始碼出下

if (done) {
    // todo 跟進去
    in.remove();
} else {

我們繼續跟進remove(), 終於我們找到了成功回撥的標誌,在remove()的底端safeSuccess(promise);, 下一步就是用回撥使用者新增的監聽者操作完成了,並且完成的狀態是Success成功的

public boolean remove() {
// todo 獲取當前的 Entry
Entry e = flushedEntry;
if (e == null) {
    clearNioBuffers();
    return false;
}
Object msg = e.msg;

ChannelPromise promise = e.promise;
int size = e.pendingSize;

// todo 將當前的Entry進行移除
removeEntry(e);

if (!e.cancelled) {
    // only release message, notify and decrement if it was not canceled before.
    ReferenceCountUtil.safeRelease(msg);
    safeSuccess(promise);
    decrementPendingOutboundBytes(size, false, true);
}

相關文章