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
被返回了, DefaultChannelPromise
是ChannelPromise
的實現類,而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);
}