認真的 Netty 源碼解析(一)

本文又是一篇源碼分析文章,其實除了 Doug Lea 的併發包源碼,我是不太愛寫源碼分析的。

本文將介紹 Netty,Java 平臺上使用最廣泛的 NIO 包,它是對 JDK 中的 NIO 實現的一層封裝,讓我們能更方便地開發 NIO 程序。其實,Netty 不僅僅是 NIO 吧,但是,基本上大家都衝着 NIO 來的。

個人感覺國內對於 Netty 的吹噓是有點過了,主要是很多人靠它吃飯,要麼是搞培訓的,要麼是出書的,恨不得把 Netty 吹上天去,這樣讀者就願意掏錢了,這種現象也是挺不好的,反而使得初學者覺得 Netty 是什麼高深的技術一樣。

Netty 的源碼不是很簡單,因爲它比較多,而且各個類之間的關係錯綜複雜,很多人說它的源碼很好,這點我覺得一般,真要說好代碼,還得 Doug Lea 的併發源碼比較漂亮,一行行都是精華,不過它們是不同類型的,也沒什麼好對比的。Netty 源碼好就好在它的接口使用比較靈活,往往接口好用的框架,源碼都不會太簡單。

本來,我只是想和之前一樣,寫一篇文章搞定的,不過按照以前的文章的反饋來看,很多人不是很喜歡這種風格,閱讀體驗不是很好。所以,純粹爲了迎合大家吧,本來我也不想的,但是既然是分享內容,就偶爾迎合下讀者吧。

注意:

  • 本文只介紹 TCP 相關的內容,Netty 對於其他協議的支持,不在本文的討論範圍內。

  • 和併發包的源碼分析不一樣,我不可能一行一行源碼說,所以有些異常分支是會直接略過,除非我覺得需要介紹。

  • Netty 源碼一直在更新,各版本之間有些差異,我是按照 2018-09-06 的最新版本 4.1.25.Final 來進行介紹的。

建議初學者在看完本文以後,可以去翻翻《Netty In Action》,網上可以找到中文文字版的(當我沒說)。

按照我的時間投入計算的話,這也算是篇值錢的文章了,所以各位給個面子好好閱讀,歡迎大家提出不滿意的地方。

Echo 例子

Netty 作爲 NIO 的庫,自然既可以作爲服務端接受請求,也可以作爲客戶端發起請求。使用 Netty 開發客戶端或服務端都是非常簡單的,Netty 做了很好的封裝,我們通常只要開發一個或多個 handler 用來處理我們的自定義邏輯就可以了。

下面,我們來看一個經常會見到的例子,它叫 Echo,也就是回聲,客戶端傳過去什麼值,服務端原樣返回什麼值。

5

左邊是服務端代碼,右邊是客戶端代碼。

上面的代碼基本就是模板代碼,每次使用都是這一個套路,唯一需要我們開發的部分是 handler(…) 和 childHandler(…) 方法中指定的各個 handler,如 EchoServerHandler 和 EchoClientHandler,當然 Netty 源碼也給我們提供了很多的 handler,比如上面的 LoggingHandler,它就是 Netty 源碼中爲我們提供的,需要的時候直接拿過來用就好了。

我們先來看一下上述代碼中涉及到的一些內容:

  • ServerBootstrap 類用於創建服務端實例,Bootstrap 用於創建客戶端實例。

  • 兩個 EventLoopGroup:bossGroup 和 workerGroup,它們涉及的是 Netty 的線程模型,可以看到服務端有兩個 group,而客戶端只有一個,它們就是 Netty 中的線程池。

  • Netty 中的 Channel,沒有直接使用 Java 原生的 ServerSocketChannel 和 SocketChannel,而是包裝了 NioServerSocketChannel 和 NioSocketChannel 與之對應。

    當然,也有對其他協議的支持,如支持 UDP 協議的 NioDatagramChannel,本文只關心 TCP 相關的。

  • 左邊 handler(…) 方法指定了一個 handler(LoggingHandler),這個 handler 是給服務端收到新的請求的時候處理用的。右邊 handler(...) 方法指定了客戶端處理請求過程中需要使用的 handlers。

    如果你想在 EchoServer 中也指定多個 handler,也可以像右邊的 EchoClient 一樣使用 ChannelInitializer

  • 左邊 childHandler(…) 指定了 childHandler,這邊的 handlers 是給新創建的連接用的,我們知道服務端 ServerSocketChannel 在 accept 一個連接以後,需要創建 SocketChannel 的實例,childHandler(…) 中設置的 handler 就是用於處理新創建的 SocketChannel 的,而不是用來處理 ServerSocketChannel 實例的。

  • pipeline:handler 可以指定多個(需要上面的 ChannelInitializer 類幫助),它們會組成了一個 pipeline,它們其實就類似攔截器的概念,現在只要記住一點,每個 NioSocketChannel 或 NioServerSocketChannel 實例內部都會有一個一個 pipeline 實例。pipeline 中還涉及到 handler 的執行順序。

  • ChannelFuture:這個涉及到 Netty 中的異步編程,和 JDK 中的 Future 接口類似。

對於不瞭解 Netty 的讀者,也不要有什麼壓力,我會一一介紹它們,本文主要面向新手,我覺得比較難理解或比較重要的部分,會花比較大的篇幅來介紹清楚。

上面的源碼中沒有展示消息發送和消息接收的處理,此部分我會在介紹完上面的這些內容以後再進行介紹。

下面,將分塊來介紹這些內容。由於我也沒有那麼強大的組織能力,所以希望讀者一節一節往下看,對於自己熟悉的內容可以適當看快一些。

Netty 中的 Channel

這節我們來看看 NioSocketChannel 是怎麼和 JDK 底層的 SocketChannel 聯繫在一起的,它們是一對一的關係。NioServerSocketChannel 和 ServerSocketChannel 同理,也是一對一的關係。

3

在 Bootstrap(客戶端) 和 ServerBootstrap(服務端) 的啓動過程中都會調用 channel(…) 方法:

10

下面,我們來看 channel(…) 方法的源碼:

// AbstractBootstrap public B channel(Class<? extends C> channelClass) {     if (channelClass == null) {         throw new NullPointerException("channelClass");     }     return channelFactory(new ReflectiveChannelFactory<C>(channelClass)); }

我們可以看到,這個方法只是設置了 channelFactory 爲 ReflectiveChannelFactory 的一個實例,然後我們看下這裏的 ReflectiveChannelFactory 到底是什麼:

1

newChannel() 方法是 ChannelFactory 接口中的唯一方法,工廠模式大家都很熟悉。我們可以看到,ReflectiveChannelFactory#newChannel() 方法中使用了反射調用 Channel 的無參構造方法來創建 Channel,我們只要知道,ChannelFactory 的 newChannel() 方法什麼時候會被調用就可以了。

  • 對於 NioSocketChannel,由於它充當客戶端的功能,它的創建時機在 connect(…) 的時候;

  • 對於 NioServerSocketChannel 來說,它充當服務端功能,它的創建時機在綁定端口 bind(…) 的時候。

接下來,我們來簡單追蹤下充當客戶端的 Bootstrap 中 NioSocketChannel 的創建過程,看看 NioSocketChannel 是怎麼和 JDK 中的 SocketChannel 關聯在一起的:

// Bootstrap public ChannelFuture connect(String inetHost, int inetPort) {     return connect(InetSocketAddress.createUnresolved(inetHost, inetPort)); }

然後再往裏看,到這個方法:

public ChannelFuture connect(SocketAddress remoteAddress) {     if (remoteAddress == null) {         throw new NullPointerException("remoteAddress");     // validate 只是校驗一下各個參數是不是正確設置了     validate();     return doResolveAndConnect(remoteAddress, config.localAddress()); }

繼續:

// 再往裏就到這裏了 private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {     // 我們要說的部分在這裏     final ChannelFuture regFuture = initAndRegister();     final Channel channel = regFuture.channel();     ...... }

然後,我們看 initAndRegister() 方法:

final ChannelFuture initAndRegister() {     Channel channel = null;     try {         // 前面我們說過,這裏會進行 Channel 的實例化         channel = channelFactory.newChannel();         init(channel);     } catch (Throwable t) {         ...     }     ...     return regFuture; }

我們找到了 channel = channelFactory.newChannel() 這行代碼,根據前面說的,這裏會調用相應 Channel 的無參構造方法。

然後我們就可以去看 NioSocketChannel 的構造方法了:

public NioSocketChannel() {     // SelectorProvider 實例用於創建 JDK 的 SocketChannel 實例     this(DEFAULT_SELECTOR_PROVIDER); } public NioSocketChannel(SelectorProvider provider) {     // 看這裏,newSocket(provider) 方法會創建 JDK 的 SocketChannel     this(newSocket(provider)); }

我們可以看到,在調用 newSocket(provider) 的時候,會創建 JDK NIO 的一個 SocketChannel 實例:

private static SocketChannel newSocket(SelectorProvider provider) {     try {         // 創建 SocketChannel 實例         return provider.openSocketChannel();     } catch (IOException e) {         throw new ChannelException("Failed to open a socket.", e);     } }

NioServerSocketChannel 同理,也非常簡單,從 ServerBootstrap#bind(...) 方法一路點進去就清楚了。

所以我們知道了,NioSocketChannel 在實例化過程中,會先實例化 JDK 底層的 SocketChannel,NioServerSocketChannel 也一樣,會先實例化 ServerSocketChannel 實例:

18

說到這裏,我們順便再繼續往裏看一下 NioSocketChannel 的構造方法:

public NioSocketChannel(SelectorProvider provider) {     this(newSocket(provider)); }

剛纔我們看到這裏,newSocket(provider) 創建了底層的 SocketChannel 實例,我們繼續往下看構造方法:

public NioSocketChannel(Channel parent, SocketChannel socket) {     super(parent, socket);     config = new NioSocketChannelConfig(this, socket.socket()); }

上面有兩行代碼,第二行代碼很簡單,實例化了內部的 NioSocketChannelConfig 實例,它用於保存 channel 的配置信息,這裏沒有我們現在需要關心的內容,直接跳過。

第一行調用父類構造器,除了設置屬性外,還設置了 SocketChannel 的非阻塞模式:

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {     // 毫無疑問,客戶端關心的是 OP_READ 事件,等待讀取服務端返回數據     super(parent, ch, SelectionKey.OP_READ); } // 然後是到這裏 protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {     super(parent);     this.ch = ch;     // 我們看到這裏只是保存了 SelectionKey.OP_READ 這個信息,在後面的時候會用到     this.readInterestOp = readInterestOp;     try {         // ******設置 channel 的非阻塞模式******         ch.configureBlocking(false);     } catch (IOException e) {         ......     } }

NioServerSocketChannel 的構造方法類似,也設置了非阻塞,然後設置服務端關心的 SelectionKey.OP_ACCEPT 事件:

public NioServerSocketChannel(ServerSocketChannel channel) {     // 對於服務端來說,關心的是 SelectionKey.OP_ACCEPT 事件,等待客戶端連接     super(null, channel, SelectionKey.OP_ACCEPT);     config = new NioServerSocketChannelConfig(this, javaChannel().socket()); }

這節關於 Channel 的內容我們先介紹這麼多,主要就是實例化了 JDK 層的 SocketChannel 或 ServerSocketChannel,然後設置了非阻塞模式,我們後面再繼續深入下去。

Netty 中的 Future、Promise

Netty 中非常多的異步調用,所以在介紹更多 NIO 相關的內容之前,我們來看看它的異步接口是怎麼使用的。

前面我們在介紹 Echo 例子的時候,已經用過了 ChannelFuture 這個接口了:

6

爭取在看完本節後,讀者能搞清楚上面的這幾行是怎麼走的。

關於 Future 接口,我想大家應該都很熟悉,用得最多的就是在使用 Java 的線程池 ThreadPoolExecutor 的時候了。在 submit 一個任務到線程池中的時候,返回的就是一個 Future 實例,通過它來獲取提交的任務的執行狀態和最終的執行結果,我們最常用它的 isDone() 和 get() 方法。

下面是 JDK 中的 Future 接口 java.util.concurrent.Future:

public interface Future<V> {     // 取消該任務     boolean cancel(boolean mayInterruptIfRunning);     // 任務是否已取消     boolean isCancelled();     // 任務是否已完成     boolean isDone();     // 阻塞獲取任務執行結果     V get() throws InterruptedException, ExecutionException;     // 帶超時參數的獲取任務執行結果     V get(long timeout, TimeUnit unit)         throws InterruptedException, ExecutionException, TimeoutException; }

Netty 中的 Future 接口繼承了 JDK 中的 Future 接口,然後添加了一些方法:

// io.netty.util.concurrent.Future

public interface Future<V> extends java.util.concurrent.Future<V> {     // 是否成功     boolean isSuccess();     // 是否可取消     boolean isCancellable();     // 如果任務執行失敗,這個方法返回異常信息     Throwable cause();     // 添加 Listener 來進行回調     Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);     Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);     Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);     Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);     // 阻塞等待任務結束,如果任務失敗,將“導致失敗的異常”重新拋出來     Future<V> sync() throws InterruptedException;     // 不響應中斷的 sync(),這個大家應該都很熟了     Future<V> syncUninterruptibly();     // 阻塞等待任務結束,和 sync() 功能是一樣的,不過如果任務失敗,它不會拋出執行過程中的異常     Future<V> await() throws InterruptedException;     Future<V> awaitUninterruptibly();     boolean await(long timeout, TimeUnit unit) throws InterruptedException;     boolean await(long timeoutMillis) throws InterruptedException;     boolean awaitUninterruptibly(long timeout, TimeUnit unit);     boolean awaitUninterruptibly(long timeoutMillis);     // 獲取執行結果,不阻塞。我們都知道 java.util.concurrent.Future 中的 get() 是阻塞的     V getNow();     // 取消任務執行,如果取消成功,任務會因爲 CancellationException 異常而導致失敗     //      也就是 isSuccess()==false,同時上面的 cause() 方法返回 CancellationException 的實例。     // mayInterruptIfRunning 說的是:是否對正在執行該任務的線程進行中斷(這樣才能停止該任務的執行),     //       似乎 Netty 中 Future 接口的各個實現類,都沒有使用這個參數     @Override     boolean cancel(boolean mayInterruptIfRunning); }

看完上面的 Netty 的 Future 接口,我們可以發現,它加了 sync() 和 await() 用於阻塞等待,還加了 Listeners,只要任務結束去回調 Listener 們就可以了,那麼我們就不一定要主動調用 isDone() 來獲取狀態,或通過 get() 阻塞方法來獲取值。

順便說下 sync() 和 await() 的區別:sync() 內部會先調用 await() 方法,等 await() 方法返回後,會檢查下這個任務是否失敗,如果失敗,重新將導致失敗的異常拋出來。也就是說,如果使用 await(),任務拋出異常後,await() 方法會返回,但是不會拋出異常,而 sync() 方法返回的同時會拋出異常。

我們也可以看到,Future 接口沒有和 IO 操作關聯在一起,還是比較

純淨的接口。


接下來,我們來看 Future 接口的子接口 ChannelFuture,這個接口用得最多,它將和 IO 操作中的 Channel 關聯在一起了,用於異步處理 Channel 中的事件。

public interface ChannelFuture extends Future<Void> {     // ChannelFuture 關聯的 Channel     Channel channel();     // 覆寫以下幾個方法,使得它們返回值爲 ChannelFuture 類型      @Override     ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> listener);     @Override     ChannelFuture addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);     @Override     ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> listener);     @Override     ChannelFuture removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);     @Override     ChannelFuture sync() throws InterruptedException;     @Override     ChannelFuture syncUninterruptibly();     @Override     ChannelFuture await() throws InterruptedException;     @Override     ChannelFuture awaitUninterruptibly();     // 用來標記該 future 是 void 的,     // 這樣就不允許使用 addListener(...), sync(), await() 以及它們的幾個重載方法     boolean isVoid(); }

我們看到,ChannelFuture 接口相對於 Future 接口,除了將 channel 關聯進來,沒有增加什麼東西。還有個 isVoid() 方法算是不那麼重要的存在吧。其他幾個都是方法覆寫,爲了讓返回值類型變爲 ChannelFuture,而不是 Future。

這裏有點跳,我們來介紹下 Promise 接口,它和 ChannelFuture 接口無關,而是和前面的 Future 接口相關,Promise 這個接口非常重要。

Promise 接口和 ChannelFuture 一樣,也繼承了 Netty 的 Future 接口,然後加了一些 Promise 的內容:

public interface Promise<V> extends Future<V> {     // 標記該 future 成功及設置其執行結果,並且會通知所有的 listeners。     // 如果該操作失敗,將拋出異常(失敗指的是該 future 已經有了結果了,成功的結果,或者失敗的結果)     Promise<V> setSuccess(V result);     // 和 setSuccess 方法一樣,只不過如果失敗,它不拋異常,返回 false     boolean trySuccess(V result);     // 標記該 future 失敗,及其失敗原因。     // 如果失敗,將拋出異常(失敗指的是已經有了結果了)     Promise<V> setFailure(Throwable cause);     // 標記該 future 失敗,及其失敗原因。     // 如果已經有結果,返回 false,不拋出異常     boolean tryFailure(Throwable cause);     // 標記該 future 不可以被取消     boolean setUncancellable();     // 這裏和 ChannelFuture 一樣,對這幾個方法進行覆寫,目的是爲了返回 Promise 類型的實例     @Override     Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);     @Override     Promise<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);     @Override     Promise<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);     @Override     Promise<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);     @Override     Promise<V> await() throws InterruptedException;     @Override     Promise<V> awaitUninterruptibly();     @Override     Promise<V> sync() throws InterruptedException;     @Override     Promise<V> syncUninterruptibly(); }

可能有些讀者對 Promise 的概念不是很熟悉,這裏簡單說兩句。

我覺得只要明白一點,Promise 實例內部是一個任務,任務的執行往往是異步的,通常是一個線程池來處理任務。Promise 提供的 setSuccess(V result) 或 setFailure(Throwable t) 將來會被某個執行任務的線程在執行完成以後調用,同時那個線程在調用 setSuccess(result) 或 setFailure(t) 後會回調 listeners 的回調函數(當然,回調的具體內容不一定要由執行任務的線程自己來執行,它可以創建新的線程來執行,也可以將回調任務提交到某個線程池來執行)。而且,一旦 setSuccess(...) 或 setFailure(...) 後,那些 await() 或 sync() 的線程就會從等待中返回。

所以這裏就有兩種編程方式,一種是用 await(),等 await() 方法返回後,得到 promise 的執行結果,然後處理它;另一種就是提供 Listener 實例,我們不太關心任務什麼時候會執行完,只要它執行完了以後會去執行 listener 中的處理方法就行。

接下來,我們再來看下 ChannelPromise,它繼承了前面介紹的 ChannelFuture 和 Promise 接口。

4

ChannelPromise 接口在 Netty 中使用得比較多,因爲它綜合了 ChannelFuture 和 Promise 兩個接口:

/**  * Special {@link ChannelFuture} which is writable.  */ public interface ChannelPromise extends ChannelFuture, Promise<Void> {     // 覆寫 ChannelFuture 中的 channel() 方法,其實這個方法一點沒變     @Override     Channel channel();     // 下面幾個方法是覆寫 Promise 中的接口,爲了返回值類型是 ChannelPromise     @Override     ChannelPromise setSuccess(Void result);     ChannelPromise setSuccess();     boolean trySuccess();     @Override     ChannelPromise setFailure(Throwable cause);     // 到這裏大家應該都熟悉了,下面幾個方法的覆寫也是爲了得到 ChannelPromise 類型的實例     @Override     ChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener);     @Override     ChannelPromise addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);     @Override     ChannelPromise removeListener(GenericFutureListener<? extends Future<? super Void>> listener);     @Override     ChannelPromise removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);     @Override     ChannelPromise sync() throws InterruptedException;     @Override     ChannelPromise syncUninterruptibly();     @Override     ChannelPromise await() throws InterruptedException;     @Override     ChannelPromise awaitUninterruptibly();     /**      * Returns a new {@link ChannelPromise} if {@link #isVoid()} returns {@code true} otherwise itself.      */     // 我們忽略這個方法吧。     ChannelPromise unvoid(); }

我們可以看到,它綜合了 ChannelFuture 和 Promise 中的方法,只不過通過覆寫將返回值都變爲 ChannelPromise 了而已,沒有增加什麼新的功能。

小結一下,我們上面介紹了幾個接口,Future 以及它的子接口 ChannelFuture 和 Promise,然後是 ChannelPromise 接口同時繼承了 ChannelFuture 和 Promise。

我把這幾個接口的主要方法列一下,這樣大家看得清晰些:

4

接下來,我們需要來一個實現類,這樣才能比較直觀地看出它們是怎麼使用的,因爲上面的這些都是接口定義,具體還得看實現類是怎麼工作的。

下面,我們來介紹下 DefaultPromise 這個實現類,這個類很常用,它的源碼也不短,我們介紹幾個關鍵的內容。

首先,我們看下它有哪些屬性:

public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {     // 保存執行結果     private volatile Object result;     // 執行任務的線程池,promise 持有 executor 的引用,這個其實有點奇怪了     private final EventExecutor executor;     // 監聽者,回調函數,任務結束後(正常或異常結束)執行     private Object listeners;     // 等待這個 promise 的線程數(調用sync()/await()進行等待的線程數量)     private short waiters;     // 是否正在喚醒等待線程,用於防止重複執行喚醒,不然會重複執行 listeners 的回調方法     private boolean notifyingListeners;     ...... }

可以看出,此類實現了 Promise,但是沒有實現 ChannelFuture,所以它和 Channel 聯繫不起來。

別急,我們後面會碰到另一個類 DefaultChannelPromise 的使用,這個類是綜合了 ChannelFuture 和 Promise 的,但是它的實現其實大部分都是繼承自這裏的 DefaultPromise 類的。

說完上面的屬性以後,大家可以看下 setSuccess(V result) 、trySuccess(V result) 和 setFailure(Throwable cause) 、 tryFailure(Throwable cause) 這幾個方法:

8

看出 setSuccess(result) 和 trySuccess(result) 的區別了嗎?

上面幾個方法都非常簡單,先設置好值,然後執行監聽者們的回調方法。notifyListeners() 方法感興趣的讀者也可以看一看,不過它還涉及到 Netty 線程池的一些內容,我們還沒有介紹到線程池,這裏就不展開了。上面的代碼,在 setSuccess0 或 setFailure0 方法中都會喚醒阻塞在 sync() 或 await() 的線程

另外,就是可以看下 sync() 和 await() 的區別,其他的我覺得隨便看看就好了。

@Override public Promise<V> sync() throws InterruptedException {     await();     // 如果任務是失敗的,重新拋出相應的異常     rethrowIfFailed();     return this; }

接下來,我們來寫個實例代碼吧:

    public static void main(String[] args) {         // 構造線程池         EventExecutor executor = new DefaultEventExecutor();         // 創建 DefaultPromise 實例         Promise promise = new DefaultPromise(executor);         // 下面給這個 promise 添加兩個 listener         promise.addListener(new GenericFutureListener<Future<Integer>>() {             @Override             public void operationComplete(Future future) throws Exception {                 if (future.isSuccess()) {                     System.out.println("任務結束,結果:" + future.get());                 } else {                     System.out.println("任務失敗,異常:" + future.cause());                 }             }         }).addListener(new GenericFutureListener<Future<Integer>>() {             @Override             public void operationComplete(Future future) throws Exception {                 System.out.println("任務結束,balabala...");             }         });         // 提交任務到線程池,五秒後執行結束,設置執行 promise 的結果         executor.submit(new Runnable() {             @Override             public void run() {                 try {                     Thread.sleep(5000);                 } catch (InterruptedException e) {                 }                 // 設置 promise 的結果                 // promise.setFailure(new RuntimeException());                 promise.setSuccess(123456);             }         });         // main 線程阻塞等待執行結果         try {             promise.sync();         } catch (InterruptedException e) {         }     }

運行代碼,兩個 listener 將在 5 秒後將輸出:

任務結束,結果:123456 任務結束,balabala...

讀者這裏可以試一下 sync() 和 await() 的區別,在任務中調用 promise.setFailure(new RuntimeException()) 試試看。

上面的代碼中,大家可能會對線程池 executor 和 promise 之間的關係感到有點迷惑。讀者應該也要清楚,具體的任務不一定就要在這個 executor 中被執行。任務結束以後,需要調用 promise.setSuccess(result) 作爲通知。

通常來說,promise 代表的 future 是不需要和線程池攪在一起的,future 只關心任務是否結束以及任務的執行結果,至於是哪個線程或哪個線程池執行的任務,future 其實是不關心的。

不過 Netty 畢竟不是要創建一個通用的線程池實現,而是和它要處理的 IO 息息相關的,所以我們只不過要理解它就好了。

這節就說這麼多吧,我們回過頭來再看一下這張圖,看看大家是不是看懂了這節內容:

6

我們就說說上圖左邊的部分吧,雖然我們還不知道 bind() 操作中具體會做什麼工作,但是我們應該可以猜出一二。

顯然,main 線程調用 b.bind(port) 這個方法會返回一個 ChannelFuture,bind() 是一個異步方法,當某個執行線程執行了真正的綁定操作後,那個執行線程一定會標記這個 future 爲成功(我們假定 bind 會成功),然後這裏的 sync() 方法就會返回了。

如果 bind(port) 失敗,我們知道,sync() 方法會將異常拋出來,然後就會執行到 finally 塊了。

一旦綁定端口成功,進入下面一行,f.channel() 方法會返回該 future 關聯的 channel。channel.closeFuture() 也會返回一個 ChannelFuture,然後調用了 sync() 方法,這個 sync() 方法返回的條件是:有其他的線程關閉了 NioServerSocketChannel,往往是因爲需要停掉服務了,然後那個線程會設置 future 的狀態( setSuccess(result) 或 setFailure(cause) ),這個 sync() 方法纔會返回。

這節就到這裏,希望大家對 Netty 中的異步編程有些瞭解以後,後續碰到源碼的時候能知道是怎麼使用的。

ChannelPipeline,和 Inbound、Outbound

我想很多讀者應該或多或少都有 Netty 中 pipeline 的概念。前面我們說了,使用 Netty 的時候,我們通常就只要寫一些自定義的 handler 就可以了,我們定義的這些 handler 會組成一個 pipeline,用於處理 IO 事件,這個和我們平時接觸的 Filter 或 Interceptor 表達的差不多是一個意思。

每個 Channel 內部都有一個 pipeline,pipeline 由多個 handler 組成,handler 之間的順序是很重要的,因爲 IO 事件將按照順序順次經過 pipeline 上的 handler,這樣每個 handler 可以專注於做一點點小事,由多個 handler 組合來完成一些複雜的邏輯。

11

首先,我們看兩個重要的概念:Inbound 和 Outbound。在 Netty 中,IO 事件被分爲 Inbound 事件和 Outbound 事件。

Outbound 的 out 指的是 出去,有哪些 IO 事件屬於此類呢?比如 connect、write、flush 這些 IO 操作是往外部方向進行的,它們就屬於 Outbound 事件。

其他的,諸如 accept、read 這種就屬於 Inbound 事件。

比如客戶端在發起請求的時候,需要 1️⃣connect 到服務器,然後 2️⃣write 數據傳到服務器,再然後 3️⃣read 服務器返回的數據,前面的 connect 和 write 就是 out 事件,後面的 read 就是 in 事件。

比如很多初學者看不懂下面的這段代碼,這段代碼用於服務端的 childHandler 中:

1. pipeline.addLast(new StringDecoder()); 2. pipeline.addLast(new StringEncoder()); 3. pipeline.addLast(new BizHandler());

初學者肯定都納悶,以爲這個順序寫錯了,應該是先 decode 客戶端過來的數據,然後用 BizHandler 處理業務邏輯,最後再 encode 數據然後返回給客戶端,所以添加的順序應該是 1 -> 3 -> 2 纔對。

其實這裏的三個 handler 是分組的,分爲 Inbound(1 和 3) 和 Outbound(2):

  • 客戶端連接進來的時候,讀取(read)客戶端請求數據的操作是 Inbound 的,所以會先使用 1,然後是 3 對處理進行處理;

  • 處理完數據後,返回給客戶端數據的 write 操作是 Outbound 的,此時使用的是 2。

所以雖然添加順序有點怪,但是執行順序其實是按照 1 -> 3 -> 2 進行的。

如果我們在上面的基礎上,加上下面的第四行,這是一個 OutboundHandler:

4. pipeline.addLast(new OutboundHandlerA());

那麼執行順序是不是就是 1 -> 3 -> 2 -> 4 呢?答案是:不是的。

對於 Inbound 操作,按照添加順序執行每個 Inbound 類型的 handler;而對於 Outbound 操作,是反着來的,從後往前,順次執行 Outbound 類型的 handler。

所以,上面的順序應該是先 1 後 3,它們是 Inbound 的,然後是 4,最後纔是 2,它們兩個是 Outbound 的。

到這裏,我想大家應該都知道 Inbound 和 Outbound 了吧?下面我們來介紹它們的接口使用。

9

定義處理 Inbound 事件的 handler 需要實現 ChannelInboundHandler,定義處理 Outbound 事件的 handler 需要實現 ChannelOutboundHandler。最下面的三個類,是 Netty 提供的適配器,特別的,如果我們希望定義一個 handler 能同時處理 Inbound 和 Outbound 事件,可以通過繼承中間的 ChannelDuplexHandler 的方式。

有了 Inbound 和 Outbound 的概念以後,我們來開始介紹 Pipeline 的源碼。

我們說過,一個 Channel 關聯一個 pipeline,NioSocketChannel 和 NioServerSocketChannel 在執行構造方法的時候,都會走到它們的父類 AbstractChannel 的構造方法中:

protected AbstractChannel(Channel parent) {     this.parent = parent;     // 給每個 channel 分配一個唯一 id     id = newId();     // 每個 channel 內部需要一個 Unsafe 的實例     unsafe = newUnsafe();     // 每個 channel 內部都會創建一個 pipeline     pipeline = newChannelPipeline(); }

上面的三行代碼中,id 比較不重要,Netty 中的 Unsafe 實例其實挺重要的,這裏簡單介紹一下。

在 JDK 的源碼中,sun.misc.Unsafe 類提供了一些底層操作的能力,它設計出來是給 JDK 中的源碼使用的,比如 AQS、ConcurrentHashMap 等,我們在之前的併發包的源碼分析中也看到了很多它們使用 Unsafe 的場景,這個 Unsafe 類不是給我們的代碼使用的(需要的話,我們也是可以獲取它的實例的)。

Unsafe 類的構造方法是 private 的,但是它提供了 getUnsafe() 這個靜態方法:

Unsafe unsafe = Unsafe.getUnsafe();

大家可以試一下,上面這行代碼編譯沒有問題,但是執行的時候會拋 java.lang.SecurityException 異常,因爲它就不是給我們的代碼用的。

但是如果你就是想獲取 Unsafe 的實例,可以通過下面這個代碼獲取到:

Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null);

Netty 中的 Unsafe 也是同樣的意思,它封裝了 Netty 中會使用到的 JDK 提供的 NIO 接口,比如將 channel 註冊到 selector 上,比如 bind 操作,比如 connect 操作等,這些操作都是稍微偏底層一些。Netty 同樣也是不希望我們的業務代碼使用 Unsafe 的實例,它是提供給 Netty 中的源碼使用的。

不過,對於我們源碼分析來說,我們還是會有很多時候需要分析 Unsafe 中的源碼的

關於 Unsafe,我們後面用到了再說,這裏只要知道,它封裝了大部分需要訪問 JDK 的 NIO 接口的操作就好了。這裏我們繼續將焦點放在 pipeline 上:

protected DefaultChannelPipeline newChannelPipeline() {     return new DefaultChannelPipeline(this); }

這裏開始調用 DefaultChannelPipeline 的構造方法,並把當前 channel 的引用傳入:

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; }

這裏實例化了 tail 和 head 這兩個 handler。tail 實現了 ChannelInboundHandler 接口,而 head 實現了 ChannelOutboundHandler 和 ChannelInboundHandler 兩個接口,並且最後兩行代碼將 tail 和 head 連接起來:

12

注意,在不同的版本中,源碼也略有差異,head 不一定是 in + out,大家知道這點就好了。

還有,從上面的 head 和 tail 我們也可以看到,其實 pipeline 中的每個元素是 ChannelHandlerContext 的實例,而不是 ChannelHandler 的實例,context 包裝了一下 handler,但是,後面我們都會用 handler 來描述一個 pipeline 上的節點,而不是使用 context,希望讀者知道這一點。

這裏只是構造了 pipeline,並且添加了兩個固定的 handler 到其中(head + tail),還不涉及到自定義的 handler 代碼執行。我們回過頭來看下面這段代碼:

13

我們說過 childHandler 中指定的 handler 不是給 NioServerSocketChannel 使用的,是給 NioSocketChannel 使用的,所以這裏我們不看它。

這裏調用 handler(…) 方法指定了一個 LoggingHandler 的實例,然後我們再進去下面的 bind(…) 方法中看看這個 LoggingHandler 實例是怎麼進入到我們之前構造的 pipeline 內的。

順着 bind() 一直往前走,bind() -> doBind() -> initAndRegister():

final ChannelFuture initAndRegister() {     Channel channel = null;     try {         // 1. 構造 channel 實例,同時會構造 pipeline 實例,         // 現在 pipeline 中有 head 和 tail 兩個 handler 了         channel = channelFactory.newChannel();         // 2. 看這裏         init(channel);     } catch (Throwable t) {     ...... }

上面的兩行代碼,第一行實現了構造 channel 和 channel 內部的 pipeline,我們來看第二行 init 代碼:

// ServerBootstrap:

@Override void init(Channel channel) throws Exception {     ......     // 拿到剛剛創建的 channel 內部的 pipeline 實例     ChannelPipeline p = channel.pipeline();     ...     // 開始往 pipeline 中添加一個 handler,這個 handler 是 ChannelInitializer 的實例     p.addLast(new ChannelInitializer<Channel>() {         // 我們以後會看到,下面這個 initChannel 方法何時會被調用         @Override         public void initChannel(final Channel ch) throws Exception {             final ChannelPipeline pipeline = ch.pipeline();             // 這個方法返回我們最開始指定的 LoggingHandler 實例             ChannelHandler handler = config.handler();             if (handler != null) {                 // 添加 LoggingHandler                 pipeline.addLast(handler);             }             // 先不用管這裏的 eventLoop             ch.eventLoop().execute(new Runnable() {                 @Override                 public void run() {                     // 添加一個 handler 到 pipeline 中:ServerBootstrapAcceptor                     // 從名字可以看到,這個 handler 的目的是用於接收客戶端請求                     pipeline.addLast(new ServerBootstrapAcceptor(                             ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));                 }             });         }     }); }

這裏涉及到 pipeline 中的輔助類 ChannelInitializer,我們看到,它本身是一個 handler(Inbound 類型),但是它的作用和普通 handler 有點不一樣,它純碎是用來將其他的 handler 加入到 pipeline 中的。

此時的 pipeline 應該是這樣的:

14

ChannelInitializer 的 initChannel(channel) 方法被調用的時候,會往 pipeline 中添加我們最開始指定的 LoggingHandler 和添加一個 ServerBootstrapAcceptor。但是我們現在還不知道這個 initChannel 方法何時會被調用。

上面我們說的是作爲服務端的 NioServerSocketChannel 的 pipeline,NioSocketChannel 也是差不多的,我們可以看一下 Bootstrap 類的 init(channel) 方法:

void init(Channel channel) throws Exception {     ChannelPipeline p = channel.pipeline();     p.addLast(config.handler());     ... }

23

它和服務端 ServerBootstrap 要添加 ServerBootstrapAcceptor 不一樣,它只需要將 EchoClient 類中的 ChannelInitializer 實例加進來就可以了,它的 ChannelInitializer 中添加了兩個 handler,LoggingHandler 和 EchoClientHandler:

16

很顯然,我們需要的是像 LoggingHandler 和 EchoClientHandler 這樣的 handler,但是,它們現在還不在 pipeline 中,那麼它們什麼時候會真正進入到 pipeline 中呢?以後我們再揭曉。

還有,爲什麼 Server 端我們指定的是一個 handler 實例,而 Client 指定的是一個 ChannelInitializer 實例?其實它們是可以隨意搭配使用的,你甚至可以在 ChannelInitializer 實例中添加 ChannelInitializer 的實例。

非常抱歉,這裏又要斷了,下面要先介紹線程池了,大家要記住 pipeline 現在的樣子,head + channelInitializer + tail

本節沒有介紹 handler 的向後傳播,就是一個 handler 處理完了以後,怎麼傳遞給下一個 handler 來處理?比如我們熟悉的 JavaEE 中的 Filter 是採用在一個 Filter 實例中調用 chain.doFilter(request, response) 來傳遞給下一個 Filter 這種方式的。

我們用下面這張圖結束本節。下圖展示了傳播的方法,但我其實是更想讓大家看一下,哪些事件是 Inbound 類型的,哪些是 Outbound 類型的:

19

Outbound 類型大家應該比較好認,注意 bind 也是 Outbound 類型的。

Netty 中的線程池 EventLoopGroup

接下來,我們來分析 Netty 中的線程池。Netty 中的線程池比較不好理解,因爲它的類比較多,而且它們之間的關係錯綜複雜。看下圖,感受下 NioEventLoop 類和 NioEventLoopGroup 類的繼承結構:

2

這張圖我整理得有些亂,但是大家仔細看一下就會發現,涉及到的類確實挺多的。本節來給大家理理清楚這部分內容。

首先,我們說的 Netty 的線程池,指的就是 NioEventLoopGroup 的實例;線程池中的單個線程,指的是右邊 NioEventLoop 的實例。

我們第一節介紹的 Echo 例子,客戶端和服務端的啓動代碼中,最開始我們總是先實例化 NioEventLoopGroup:

// EchoClient 代碼最開始: EventLoopGroup group = new NioEventLoopGroup(); // EchoServer 代碼最開始: EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup();

下面,我們就從 NioEventLoopGroup 的源碼開始進行分析。

我們打開 NioEventLoopGroup 的源碼,可以看到,NioEventLoopGroup 有多個構造方法用於參數設置,最簡單地,我們採用無參構造函數,或僅僅設置線程數量就可以了,其他的參數採用默認值。

public NioEventLoopGroup() {     this(0); } public NioEventLoopGroup(int nThreads) {     this(nThreads, (Executor) null); } // 參數最全的構造方法 public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,                          final SelectorProvider selectorProvider,                          final SelectStrategyFactory selectStrategyFactory,                          final RejectedExecutionHandler rejectedExecutionHandler) {     // 調用父類的構造方法     super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler); }

我們來稍微看一下構造方法中的各個參數:

  • nThreads:這個最簡單,就是線程池中的線程數,也就是 NioEventLoop 的實例數量。

  • executor:我們知道,我們本身就是要構造一個線程池(Executor),爲什麼這裏傳一個 executor 實例呢?它其實不是給線程池用的,而是給 NioEventLoop 用的。

  • chooserFactory:當我們提交一個任務到線程池的時候,線程池需要選擇(choose)其中的一個線程來執行這個任務,這個就是用來實現選擇策略的。

  • selectorProvider:這個簡單,我們需要通過它來實例化 Selector,可以看到每個線程池都持有一個 selectorProvider 實例。

  • selectStrategyFactory:這個涉及到的是線程池中線程的工作流程,在介紹 NioEventLoop 的時候會說。

  • rejectedExecutionHandler:這個也是線程池的好朋友了,用於處理線程池中沒有可用的線程來執行任務的情況。在 Netty 中稍微有一點點不一樣,這個是給 NioEventLoop 實例用的,以後我們再詳細介紹。

這裏介紹這些參數是希望大家有個印象,這樣可能會對接下來的源碼更有感覺一些,我們接下來就追着一條線走下去看看。

我們就看無參構造方法:

public NioEventLoopGroup() {     this(0); }

然後一步步走下去,到這個構造方法:

public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory, final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) {     super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); }

大家自己要去跟一下源碼,這樣才知道設置了哪些默認值,下面這幾個參數都被設置了默認值:

  • selectorProvider = SelectorProvider.provider()

    這個沒什麼好說的,調用了 JDK 提供的方法

  • selectStrategyFactory = DefaultSelectStrategyFactory.INSTANCE

    這個涉及到的是線程在做 select 操作和執行任務過程中的策略選擇問題,在介紹 NioEventLoop 的時候會用到。

  • rejectedExecutionHandler = RejectedExecutionHandlers.reject()

    也就是說,默認拒絕策略是:拋出異常

跟着源碼走,我們會來到父類 MultithreadEventLoopGroup 的構造方法中:

protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {     super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args); }

這裏我們發現,如果採用無參構造函數,那麼到這裏的時候,默認地 nThreads 會被設置爲 CPU 核心數 *2。大家可以看下 DEFAULT_EVENT_LOOP_THREADS 的默認值,以及 static 代碼塊的設值邏輯。

我們繼續往下走:

protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {     this(nThreads, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args); }

到這一步的時候,new ThreadPerTaskExecutor(threadFactory) 會構造一個 executor。

我們現在還不知道這個 executor 怎麼用,我們看下它的源碼:

public final class ThreadPerTaskExecutor implements Executor {     private final ThreadFactory threadFactory;     public ThreadPerTaskExecutor(ThreadFactory threadFactory) {         if (threadFactory == null) {             throw new NullPointerException("threadFactory");         }         this.threadFactory = threadFactory;     }     @Override     public void execute(Runnable command) {         // 爲每個任務新建一個線程         threadFactory.newThread(command).start();     } }

Executor 作爲線程池的最頂層接口, 我們知道,它只有一個 execute(runnable) 方法,從上面我們可以看到,實現類 ThreadPerTaskExecutor 的邏輯就是每來一個任務,新建一個線程

我們先記住這個,前面也說了,它是給 NioEventLoop 用的,不是給 NioEventLoopGroup 用的。

上一步設置完了 executor,我們繼續往下看:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {     this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args); }

這一步設置了 chooserFactory,用來實現從線程池中選擇一個線程的選擇策略。

ChooserFactory 的邏輯比較簡單,我們看下 DefaultEventExecutorChooserFactory 的實現:

@Override public EventExecutorChooser newChooser(EventExecutor[] executors) {     if (isPowerOfTwo(executors.length)) {         return new PowerOfTwoEventExecutorChooser(executors);     } else {         return new GenericEventExecutorChooser(executors);     } }

這裏設置的策略也很簡單:

1、如果線程池的線程數量是 2^n,採用下面的方式會高效一些:

@Override public EventExecutor next() {     return executors[idx.getAndIncrement() & executors.length - 1]; }

2、如果不是,用取模的方式:

@Override public EventExecutor next() {     return executors[Math.abs(idx.getAndIncrement() % executors.length)]; }

走了這麼久,我們終於到了一個幹實事的構造方法中了:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,                                         EventExecutorChooserFactory chooserFactory, Object... args) {     if (nThreads <= 0) {         throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));     }     // executor 如果是 null,做一次和前面一樣的默認設置。     if (executor == null) {         executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());     }     // 這裏的 children 數組非常重要,它就是線程池中的線程數組,這麼說不太嚴謹,但是就大概這個意思     children = new EventExecutor[nThreads];     // 下面這個 for 循環將實例化 children 數組中的每一個元素     for (int i = 0; i < nThreads; i ++) {         boolean success = false;         try {             // 實例化!!!!!!             children[i] = newChild(executor, args);             success = true;         } catch (Exception e) {             // TODO: Think about if this is a good exception type             throw new IllegalStateException("failed to create a child event loop", e);         } finally {             // 如果有一個 child 實例化失敗,那麼 success 就會爲 false,然後進入下面的失敗處理邏輯             if (!success) {                 // 把已經成功實例化的“線程” shutdown,shutdown 是異步操作                 for (int j = 0; j < i; j ++) {                     children[j].shutdownGracefully();                 }                 // 等待這些線程成功 shutdown                 for (int j = 0; j < i; j ++) {                     EventExecutor e = children[j];                     try {                         while (!e.isTerminated()) {                             e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);                         }                     } catch (InterruptedException interrupted) {                         // 把中斷狀態設置回去,交給關心的線程來處理.                         Thread.currentThread().interrupt();                         break;                     }                 }             }         }     }     // ================================================     // === 到這裏,就是代表上面的實例化所有線程已經成功結束 ===     // ================================================     // 通過之前設置的 chooserFactory 來實例化 Chooser,把線程池數組傳進去,這就不必再說了吧     chooser = chooserFactory.newChooser(children);     // 設置一個 Listener 用來監聽該線程池的 termination 事件     // 下面的代碼邏輯是:給池中每一個線程都設置這個 listener,當監聽到所有線程都 terminate 以後,這個線程池就算真正的 terminate 了。     final FutureListener<Object> terminationListener = new FutureListener<Object>() {         @Override         public void operationComplete(Future<Object> future) throws Exception {             if (terminatedChildren.incrementAndGet() == children.length) {                 terminationFuture.setSuccess(null);             }         }     };     for (EventExecutor e: children) {         e.terminationFuture().addListener(terminationListener);     }     // 設置 readonlyChildren,它是隻讀集合,以後用到再說     Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);     Collections.addAll(childrenSet, children);     readonlyChildren = Collections.unmodifiableSet(childrenSet); }

上面的代碼非常簡單,沒有什麼需要特別說的,接下來,我們來看看 newChild() 這個方法,這個方法非常重要,它將創建線程池中的線程。

我上面已經用過很多次"線程"這個詞了,它可不是 Thread 的意思,而是指池中的個體,後面我們會看到每個"線程"在什麼時候會真正創建 Thread 實例。反正每個 NioEventLoop 實例內部都會有一個自己的 Thread 實例,所以把這兩個概念混在一起也無所謂吧。

newChild(…) 方法在 NioEventLoopGroup 中覆寫了,上面說的"線程"其實就是 NioEventLoop:

@Override protected EventLoop newChild(Executor executor, Object... args) throws Exception {     return new NioEventLoop(this, executor, (SelectorProvider) args[0],         ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); }

它調用了 NioEventLoop 的構造方法:

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,              SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {     super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);     if (selectorProvider == null) {         throw new NullPointerException("selectorProvider");     }     if (strategy == null) {         throw new NullPointerException("selectStrategy");     }     provider = selectorProvider;     // 開啓 NIO 中最重要的組件:Selector     final SelectorTuple selectorTuple = openSelector();     selector = selectorTuple.selector;     unwrappedSelector = selectorTuple.unwrappedSelector;     selectStrategy = strategy; }

我們先粗略觀察一下,然後再往下看:

  • 在 Netty 中,NioEventLoopGroup 代表線程池,NioEventLoop 就是其中的線程。

  • 線程池 NioEventLoopGroup 是池中的線程 NioEventLoop 的 parent,從上面的代碼中的取名可以看出。

  • 每個 NioEventLoop 都有自己的 Selector,上面的代碼也反應了這一點,這和 Tomcat 中的 NIO 模型有點區別。

  • executor、selectStrategy 和 rejectedExecutionHandler 從 NioEventLoopGroup 中一路傳到了 NioEventLoop 中。

這個時候,我們來看一下 NioEventLoop 的屬性都有哪些,我們先忽略它的父類的屬性,單單看它自己的:

private Selector selector; private Selector unwrappedSelector; private SelectedSelectionKeySet selectedKeys; private final SelectorProvider provider; private final AtomicBoolean wakenUp = new AtomicBoolean(); private final SelectStrategy selectStrategy; private volatile int ioRatio = 50; private int cancelledKeys; private boolean needsToSelectAgain;

結合它的構造方法我們來總結一下:

  • provider:它由 NioEventLoopGroup 傳進來,前面我們說了一個線程池有一個 selectorProvider,用於創建 Selector 實例

  • selector:雖然我們還沒看創建 selector 的代碼,但我們已經知道,在 Netty 中 Selector 是跟着線程池中的線程走的。

  • selectStrategy:select 操作的策略,這個不急。

  • ioRatio:這是 IO 任務的執行時間比例,因爲每個線程既有 IO 任務執行,也有非 IO 任務需要執行,所以該參數爲了保證有足夠時間是給 IO 的。這裏也不需要急着去理解什麼 IO 任務、什麼非 IO 任務。

然後我們繼續走它的構造方法,我們看到上面的構造方法調用了父類的構造器,它的父類是 SingleThreadEventLoop。

protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,                                 boolean addTaskWakesUp, int maxPendingTasks,                                 RejectedExecutionHandler rejectedExecutionHandler) {     super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);     // 我們可以直接忽略這個東西,以後我們也不會再介紹它     tailTasks = newTaskQueue(maxPendingTasks); }

SingleThreadEventLoop 這個名字很詭異有沒有?然後它的構造方法又調用了父類 SingleThreadEventExecutor 的構造方法:

protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,                                     boolean addTaskWakesUp, int maxPendingTasks,                                     RejectedExecutionHandler rejectedHandler) {     super(parent);     this.addTaskWakesUp = addTaskWakesUp;     this.maxPendingTasks = Math.max(16, maxPendingTasks);     this.executor = ObjectUtil.checkNotNull(executor, "executor");     // taskQueue,這個東西很重要,提交給 NioEventLoop 的任務都會進入到這個 taskQueue 中等待被執行     // 這個 queue 的默認容量是 16     taskQueue = newTaskQueue(this.maxPendingTasks);     rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler"); }

到這裏就更加詭異了,NioEventLoop 的父類是 SingleThreadEventLoop,而 SingleThreadEventLoop 的父類是 SingleThreadEventExecutor,它的名字告訴我們,它是一個 Executor,是一個線程池,而且是 Single Thread 單線程的。

也就是說,線程池 NioEventLoopGroup 中的每一個線程 NioEventLoop 也可以當做一個線程池來用,只不過池中只有一個線程。這種設計雖然看上去很巧妙,不過有點反人類的樣子。

上面這個構造函數比較簡單:

  • 設置了 parent,也就是之前創建的線程池 NioEventLoopGroup 實例

  • executor:它是我們之前實例化的 ThreadPerTaskExecutor,我們說過,這個東西在線程池中沒有用,它是給 NioEventLoop 用的,馬上我們就要看到它了。提前透露一下,它用來開啓 NioEventLoop 中的線程(Thread 實例)。

  • taskQueue:這算是該構造方法中新的東西,它是任務隊列。我們前面說過,NioEventLoop 需要負責 IO 事件和非 IO 事件,通常它都在執行 selector 的 select 方法或者正在處理 selectedKeys,如果我們要 submit 一個任務給它,任務就會被放到 taskQueue 中,等它來輪詢。該隊列是線程安全的 LinkedBlockingQueue,默認容量爲 16。

  • rejectedExecutionHandler:taskQueue 的默認容量是 16,所以,如果 submit 的任務堆積了到了 16,再往裏面提交任務會觸發 rejectedExecutionHandler 的執行策略。

    還記得默認策略嗎:拋出RejectedExecutionException 異常。

    在 NioEventLoopGroup 的默認構造中,它的實現是這樣的:

       private static final RejectedExecutionHandler REJECT = new RejectedExecutionHandler() {        @Override        public void rejected(Runnable task, SingleThreadEventExecutor executor) {            throw new RejectedExecutionException();        }    };

然後,我們再回到 NioEventLoop 的構造方法:

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,              SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {     // 我們剛剛說完了這個     super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);     if (selectorProvider == null) {         throw new NullPointerException("selectorProvider");     }     if (strategy == null) {         throw new NullPointerException("selectStrategy");     }     provider = selectorProvider;     // 創建 selector 實例     final SelectorTuple selectorTuple = openSelector();     selector = selectorTuple.selector;     unwrappedSelector = selectorTuple.unwrappedSelector;     selectStrategy = strategy; }

可以看到,最重要的方法其實就是 openSelector() 方法,它將創建 NIO 中最重要的一個組件 Selector。在這個方法中,Netty 也做了一些優化,這部分我們就不去分析它了。

到這裏,我們的線程池 NioEventLoopGroup 創建完成了,並且實例化了池中的所有 NioEventLoop 實例。

同時,大家應該已經看到,上面並沒有真正創建 NioEventLoop 中的線程(沒有創建 Thread 實例)。

提前透露一下,創建線程的時機在第一個任務提交過來的時候,那麼第一個任務是什麼呢?是我們馬上要說的 channel 的 register 操作。


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