本文又是一篇原始碼分析文章,其實除了 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,也就是回聲,客戶端傳過去什麼值,服務端原樣返回什麼值。
左邊是服務端程式碼,右邊是客戶端程式碼。
上面的程式碼基本就是模板程式碼,每次使用都是這一個套路,唯一需要我們開發的部分是 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 同理,也是一對一的關係。
在 Bootstrap(客戶端) 和 ServerBootstrap(服務端) 的啟動過程中都會呼叫 channel(…) 方法:
下面,我們來看 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 到底是什麼:
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 例項:
說到這裡,我們順便再繼續往裡看一下 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 這個介面了:
爭取在看完本節後,讀者能搞清楚上面的這幾行是怎麼走的。
關於 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 介面。
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。
我把這幾個介面的主要方法列一下,這樣大家看得清晰些:
接下來,我們需要來一個實現類,這樣才能比較直觀地看出它們是怎麼使用的,因為上面的這些都是介面定義,具體還得看實現類是怎麼工作的。
下面,我們來介紹下 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)
這幾個方法:
看出 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 息息相關的,所以我們只不過要理解它就好了。
這節就說這麼多吧,我們回過頭來再看一下這張圖,看看大家是不是看懂了這節內容:
我們就說說上圖左邊的部分吧,雖然我們還不知道 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 組合來完成一些複雜的邏輯。
首先,我們看兩個重要的概念: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 了吧?下面我們來介紹它們的介面使用。
定義處理 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 連線起來:
注意,在不同的版本中,原始碼也略有差異,head 不一定是 in + out,大家知道這點就好了。
還有,從上面的 head 和 tail 我們也可以看到,其實 pipeline 中的每個元素是 ChannelHandlerContext 的例項,而不是 ChannelHandler 的例項,context 包裝了一下 handler,但是,後面我們都會用 handler 來描述一個 pipeline 上的節點,而不是使用 context,希望讀者知道這一點。
這裡只是構造了 pipeline,並且新增了兩個固定的 handler 到其中(head + tail),還不涉及到自定義的 handler 程式碼執行。我們回過頭來看下面這段程式碼:
我們說過 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 應該是這樣的:
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());
...
}
複製程式碼
它和服務端 ServerBootstrap 要新增 ServerBootstrapAcceptor 不一樣,它只需要將 EchoClient 類中的 ChannelInitializer 例項加進來就可以了,它的 ChannelInitializer 中新增了兩個 handler,LoggingHandler 和 EchoClientHandler:
很顯然,我們需要的是像 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 型別的:
Outbound 型別大家應該比較好認,注意 bind 也是 Outbound 型別的。
Netty 中的執行緒池 EventLoopGroup
接下來,我們來分析 Netty 中的執行緒池。Netty 中的執行緒池比較不好理解,因為它的類比較多,而且它們之間的關係錯綜複雜。看下圖,感受下 NioEventLoop 類和 NioEventLoopGroup 類的繼承結構:
這張圖我整理得有些亂,但是大家仔細看一下就會發現,涉及到的類確實挺多的。本節來給大家理理清楚這部分內容。
首先,我們說的 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 操作。