本系列Netty原始碼解析文章基於 4.1.56.Final版本
在上篇文章《聊聊Netty那些事兒之從核心角度看IO模型》中我們花了大量的篇幅來從核心角度詳細講述了五種IO模型
的演進過程以及ReactorIO執行緒模型
的底層基石IO多路複用技術在核心中的實現原理。
最後我們引出了netty中使用的主從Reactor IO執行緒模型。
通過上篇文章的介紹,我們已經清楚了在IO呼叫的過程中核心幫我們搞了哪些事情,那麼俗話說的好核心領進門,修行在netty
,netty在使用者空間又幫我們搞了哪些事情?
那麼從本文開始,筆者將從原始碼角度來帶大家看下上圖中的Reactor IO執行緒模型
在Netty中是如何實現的。
本文作為Reactor在Netty中實現系列文章中的開篇文章,筆者先來為大家介紹Reactor的骨架是如何建立出來的。
在上篇文章中我們提到Netty採用的是主從Reactor多執行緒
的模型,但是它在實現上又與Doug Lea在Scalable IO in Java論文中提到的經典主從Reactor多執行緒模型
有所差異。
Netty中的Reactor
是以Group
的形式出現的,主從Reactor
在Netty中就是主從Reactor組
,每個Reactor Group
中會有多個Reactor
用來執行具體的IO任務
。當然在netty中Reactor
不只用來執行IO任務
,這個我們後面再說。
Main Reactor Group
中的Reactor
數量取決於服務端要監聽的埠個數,通常我們的服務端程式只會監聽一個埠,所以Main Reactor Group
只會有一個Main Reactor
執行緒來處理最重要的事情:繫結埠地址
,接收客戶端連線
,為客戶端建立對應的SocketChannel
,將客戶端SocketChannel分配給一個固定的Sub Reactor
。也就是上篇文章筆者為大家舉的例子,飯店最重要的工作就是先把客人迎接進來。“我家大門常開啟,開放懷抱等你,擁抱過就有了默契你會愛上這裡......”
Sub Reactor Group
裡有多個Reactor
執行緒,Reactor
執行緒的個數可以通過系統引數-D io.netty.eventLoopThreads
指定。預設的Reactor
的個數為CPU核數 * 2
。Sub Reactor
執行緒主要用來輪詢客戶端SocketChannel上的IO就緒事件
,處理IO就緒事件
,執行非同步任務
。Sub Reactor Group
做的事情就是上篇飯店例子中服務員的工作,客人進來了要為客人分配座位,端茶送水,做菜上菜。“不管遠近都是客人,請不用客氣,相約好了在一起,我們歡迎您......”
一個
客戶端SocketChannel
只能分配給一個固定的Sub Reactor
。一個Sub Reactor
負責處理多個客戶端SocketChannel
,這樣可以將服務端承載的全量客戶端連線
分攤到多個Sub Reactor
中處理,同時也能保證客戶端SocketChannel上的IO處理的執行緒安全性
。
由於文章篇幅的關係,作為Reactor在netty中實現的第一篇我們主要來介紹主從Reactor Group
的建立流程,骨架脈絡先搭好。
下面我們來看一段Netty服務端程式碼的編寫模板,從程式碼模板的流程中我們來解析下主從Reactor的建立流程以及在這個過程中所涉及到的Netty核心類。
Netty服務端程式碼模板
/**
* Echoes back any received data from a client.
*/
public final class EchoServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// Configure the server.
//建立主從Reactor執行緒組
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//配置主從Reactor
.channel(NioServerSocketChannel.class)//配置主Reactor中的channel型別
.option(ChannelOption.SO_BACKLOG, 100)//設定主Reactor中channel的option選項
.handler(new LoggingHandler(LogLevel.INFO))//設定主Reactor中Channel->pipline->handler
.childHandler(new ChannelInitializer<SocketChannel>() {//設定從Reactor中註冊channel的pipeline
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});
// Start the server. 繫結埠啟動服務,開始監聽accept事件
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
- 首先我們要建立Netty最核心的部分 ->
建立主從Reactor Group
,在Netty中EventLoopGroup
就是Reactor Group
的實現類。對應的EventLoop
就是Reactor
的實現類。
//建立主從Reactor執行緒組
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
- 建立用於
IO處理
的ChannelHandler
,實現相應IO事件
的回撥函式,編寫對應的IO處理
邏輯。注意這裡只是簡單示例哈,詳細的IO事件處理,筆者會單獨開一篇文章專門講述。
final EchoServerHandler serverHandler = new EchoServerHandler();
/**
* Handler implementation for the echo server.
*/
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
................省略IO處理邏輯................
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
-
建立
ServerBootstrap
Netty服務端啟動類,並在啟動類中配置啟動Netty服務端所需要的一些必備資訊。-
通過
serverBootstrap.group(bossGroup, workerGroup)
為Netty服務端配置主從Reactor Group
例項。 -
通過
serverBootstrap.channel(NioServerSocketChannel.class)
配置Netty服務端的ServerSocketChannel
用於繫結埠地址
以及建立客戶端SocketChannel
。Netty中的NioServerSocketChannel.class
就是對JDK NIO中ServerSocketChannel
的封裝。而用於表示客戶端連線
的NioSocketChannel
是對JDK NIOSocketChannel
封裝。
在上篇文章介紹
Socket核心結構
小節中我們提到,在編寫服務端網路程式時,我們首先要建立一個Socket
用於listen和bind
埠地址,我們把這個叫做監聽Socket
,這裡對應的就是NioServerSocketChannel.class
。當客戶端連線完成三次握手,系統呼叫accept
函式會基於監聽Socket
建立出來一個新的Socket
專門用於與客戶端之間的網路通訊我們稱為客戶端連線Socket
,這裡對應的就是NioSocketChannel.class
-
serverBootstrap.option(ChannelOption.SO_BACKLOG, 100)
設定服務端ServerSocketChannel
中的SocketOption
。關於SocketOption
的選項我們後邊的文章再聊,本文主要聚焦在NettyMain Reactor Group
的建立及工作流程。 -
serverBootstrap.handler(....)
設定服務端NioServerSocketChannel
中對應Pipieline
中的ChannelHandler
。
netty有兩種
Channel型別
:一種是服務端用於監聽繫結埠地址的NioServerSocketChannel
,一種是用於客戶端通訊的NioSocketChannel
。每種Channel型別例項
都會對應一個PipeLine
用於編排對應channel例項
上的IO事件處理邏輯。PipeLine
中組織的就是ChannelHandler
用於編寫特定的IO處理邏輯。注意
serverBootstrap.handler
設定的是服務端NioServerSocketChannel PipeLine
中的ChannelHandler
。serverBootstrap.childHandler(ChannelHandler childHandler)
用於設定客戶端NioSocketChannel
中對應Pipieline
中的ChannelHandler
。我們通常配置的編碼解碼器就是在這裡。
ServerBootstrap
啟動類方法帶有child
字首的均是設定客戶端NioSocketChannel
屬性的。ChannelInitializer
是用於當SocketChannel
成功註冊到繫結的Reactor
上後,用於初始化該SocketChannel
的Pipeline
。它的initChannel
方法會在註冊成功後執行。這裡只是捎帶提一下,讓大家有個初步印象,後面我會專門介紹。 -
-
ChannelFuture f = serverBootstrap.bind(PORT).sync()
這一步會是下篇文章要重點分析的主題Main Reactor Group
的啟動,繫結埠地址,開始監聽客戶端連線事件(OP_ACCEPT
)。本文我們只關注建立流程。 -
f.channel().closeFuture().sync()
等待服務端NioServerSocketChannel
關閉。Netty服務端到這裡正式啟動,並準備好接受客戶端連線的準備。 -
shutdownGracefully
優雅關閉主從Reactor執行緒組
裡的所有Reactor執行緒
。
Netty對IO模型的支援
在上篇文章中我們介紹了五種IO模型
,Netty中支援BIO
,NIO
,AIO
以及多種作業系統下的IO多路複用技術
實現。
在Netty中切換這幾種IO模型
也是非常的方便,下面我們來看下Netty如何對這幾種IO模型進行支援的。
首先我們介紹下幾個與IO模型
相關的重要介面:
EventLoop
EventLoop
就是Netty中的Reactor
,可以說它就是Netty的引擎,負責Channel上IO就緒事件的監聽
,IO就緒事件的處理
,非同步任務的執行
驅動著整個Netty的運轉。
不同IO模型
下,EventLoop
有著不同的實現,我們只需要切換不同的實現類就可以完成對NettyIO模型
的切換。
BIO | NIO | AIO |
---|---|---|
ThreadPerChannelEventLoop | NioEventLoop | AioEventLoop |
在NIO模型
下Netty會自動
根據作業系統以及版本的不同選擇對應的IO多路複用技術實現
。比如Linux 2.6版本以上用的是Epoll
,2.6版本以下用的是Poll
,Mac下采用的是Kqueue
。
其中Linux kernel 在5.1版本引入的非同步IO庫io_uring正在netty中孵化。
EventLoopGroup
Netty中的Reactor
是以Group
的形式出現的,EventLoopGroup
正是Reactor組
的介面定義,負責管理Reactor
,Netty中的Channel
就是通過EventLoopGroup
註冊到具體的Reactor
上的。
Netty的IO執行緒模型是主從Reactor多執行緒模型
,主從Reactor執行緒組
在Netty原始碼中對應的其實就是兩個EventLoopGroup
例項。
不同的IO模型
也有對應的實現:
BIO | NIO | AIO |
---|---|---|
ThreadPerChannelEventLoopGroup | NioEventLoopGroup | AioEventLoopGroup |
ServerSocketChannel
用於Netty服務端使用的ServerSocketChannel
,對應於上篇文章提到的監聽Socket
,負責繫結監聽埠地址,接收客戶端連線並建立用於與客戶端通訊的SocketChannel
。
不同的IO模型
下的實現:
BIO | NIO | AIO |
---|---|---|
OioServerSocketChannel | NioServerSocketChannel | AioServerSocketChannel |
SocketChannel
用於與客戶端通訊的SocketChannel
,對應於上篇文章提到的客戶端連線Socket
,當客戶端完成三次握手後,由系統呼叫accept
函式根據監聽Socket
建立。
不同的IO模型
下的實現:
BIO | NIO | AIO |
---|---|---|
OioSocketChannel | NioSocketChannel | AioSocketChannel |
我們看到在不同IO模型
的實現中,Netty這些圍繞IO模型
的核心類只是字首的不同:
- BIO對應的字首為
Oio
表示old io
,現在已經廢棄不推薦使用。 - NIO對應的字首為
Nio
,正是Netty推薦也是我們常用的非阻塞IO模型
。 - AIO對應的字首為
Aio
,由於Linux下的非同步IO
機制實現的並不成熟,效能提升表現上也不明顯,現已被刪除。
我們只需要將IO模型
的這些核心介面對應的實現類字首
改為對應IO模型
的字首,就可以輕鬆在Netty中完成對IO模型
的切換。
多種NIO的實現
Common | Linux | Mac |
---|---|---|
NioEventLoopGroup | EpollEventLoopGroup | KQueueEventLoopGroup |
NioEventLoop | EpollEventLoop | KQueueEventLoop |
NioServerSocketChannel | EpollServerSocketChannel | KQueueServerSocketChannel |
NioSocketChannel | EpollSocketChannel | KQueueSocketChannel |
我們通常在使用NIO模型
的時候會使用Common列
下的這些IO模型
核心類,Common類
也會根據作業系統的不同自動選擇JDK
在對應平臺下的IO多路複用技術
的實現。
而Netty自身也根據作業系統的不同提供了自己對IO多路複用技術
的實現,比JDK
的實現效能更優。比如:
JDK
的 NIO預設
實現是水平觸發
,Netty 是邊緣觸發(預設)
和水平觸發可切換。。- Netty 實現的垃圾回收更少、效能更好。
我們編寫Netty服務端程式的時候也可以根據作業系統的不同,採用Netty自身的實現來進一步優化程式。做法也很簡單,直接將上圖中紅框裡的實現類替換成Netty的自身實現類即可完成切換。
經過以上對Netty服務端程式碼編寫模板以及IO模型
相關核心類的簡單介紹,我們對Netty的建立流程有了一個簡單粗略的總體認識,下面我們來深入剖析下建立流程過程中的每一個步驟以及這個過程中涉及到的核心類實現。
以下原始碼解析部分我們均採用Common列
下NIO
相關的實現進行解析。
建立主從Reactor執行緒組
在Netty服務端程式編寫模板的開始,我們首先會建立兩個Reactor執行緒組:
-
一個是主Reactor執行緒組
bossGroup
用於監聽客戶端連線,建立客戶端連線NioSocketChannel
,並將建立好的客戶端連線NioSocketChannel
註冊到從Reactor執行緒組中一個固定的Reactor
上。 -
一個是從Reactor執行緒組
workerGroup
,workerGroup
中的Reactor
負責監聽繫結在其上的客戶端連線NioSocketChannel
上的IO就緒事件
,並處理IO就緒事件
,執行非同步任務
。
//建立主從Reactor執行緒組
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
Netty中Reactor執行緒組的實現類為NioEventLoopGroup
,在建立bossGroup
和workerGroup
的時候用到了NioEventLoopGroup
的兩個建構函式:
- 帶
nThreads
引數的建構函式public NioEventLoopGroup(int nThreads)
。 - 不帶
nThreads
引數的預設
建構函式public NioEventLoopGroup()
public class NioEventLoopGroup extends MultithreadEventLoopGroup {
/**
* Create a new instance using the default number of threads, the default {@link ThreadFactory} and
* the {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
*/
public NioEventLoopGroup() {
this(0);
}
/**
* Create a new instance using the specified number of threads, {@link ThreadFactory} and the
* {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
*/
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
......................省略...........................
}
nThreads
參數列示當前要建立的Reactor執行緒組
內包含多少個Reactor執行緒
。不指定nThreads
引數的話採用預設的Reactor執行緒
個數,用0
表示。
最終會呼叫到建構函式
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
final SelectStrategyFactory selectStrategyFactory) {
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
下面簡單介紹下建構函式中這幾個引數的作用,後面我們在講解本文主線的過程中還會提及這幾個引數,到時在詳細介紹,這裡只是讓大家有個初步印象,不必做過多的糾纏。
Executor executor:
負責啟動Reactor執行緒
進而Reactor才可以開始工作。
Reactor執行緒組
NioEventLoopGroup
負責建立Reactor執行緒
,在建立的時候會將executor
傳入。
-
RejectedExecutionHandler:
當向Reactor
新增非同步任務新增失敗時,採用的拒絕策略。Reactor的任務不只是監聽IO活躍事件和IO任務的處理,還包括對非同步任務的處理。這裡大家只需有個這樣的概念,後面筆者會專門詳細介紹。 -
SelectorProvider selectorProvider:
Reactor中的IO模型為IO多路複用模型
,對應於JDK NIO中的實現為java.nio.channels.Selector
(就是我們上篇文章中提到的select,poll,epoll
),每個Reator中都包含一個Selector
,用於輪詢
註冊在該Reactor上的所有Channel
上的IO事件
。SelectorProvider
就是用來建立Selector
的。 -
SelectStrategyFactory selectStrategyFactory:
Reactor最重要的事情就是輪詢
註冊其上的Channel
上的IO就緒事件
,這裡的SelectStrategyFactory
用於指定輪詢策略
,預設為DefaultSelectStrategyFactory.INSTANCE
。
最終會將這些引數交給NioEventLoopGroup
的父類構造器,下面我們來看下NioEventLoopGroup類
的繼承結構:
NioEventLoopGroup類
的繼承結構乍一看比較複雜,大家不要慌,筆者會隨著主線的深入慢慢地介紹這些父類介面,我們現在重點關注Mutithread
字首的類。
我們知道NioEventLoopGroup
是Netty中的Reactor執行緒組
的實現,既然是執行緒組那麼肯定是負責管理和建立多個Reactor執行緒的
,所以Mutithread
字首的類定義的行為自然是對Reactor執行緒組
內多個Reactor執行緒
的建立和管理工作。
MultithreadEventLoopGroup
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(MultithreadEventLoopGroup.class);
//預設Reactor個數
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
/**
* @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...)
*/
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
...................省略.....................
}
MultithreadEventLoopGroup類
主要的功能就是用來確定Reactor執行緒組
內Reactor
的個數。
預設的Reactor
的個數存放於欄位DEFAULT_EVENT_LOOP_THREADS
中。
從static {}
靜態程式碼塊中我們可以看出預設Reactor
的個數的獲取邏輯:
-
可以通過系統變數
-D io.netty.eventLoopThreads"
指定。 -
如果不指定,那麼預設的就是
NettyRuntime.availableProcessors() * 2
當nThread
引數設定為0
採用預設設定時,Reactor執行緒組
內的Reactor
個數則設定為DEFAULT_EVENT_LOOP_THREADS
。
MultithreadEventExecutorGroup
MultithreadEventExecutorGroup
這裡就是本小節的核心,主要用來定義建立和管理Reactor
的行為。
public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
//Reactor執行緒組中的Reactor集合
private final EventExecutor[] children;
private final Set<EventExecutor> readonlyChildren;
//從Reactor group中選擇一個特定的Reactor的選擇策略 用於channel註冊繫結到一個固定的Reactor上
private final EventExecutorChooserFactory.EventExecutorChooser chooser;
/**
* Create a new instance.
*
* @param nThreads the number of threads that will be used by this instance.
* @param executor the Executor to use, or {@code null} if the default should be used.
* @param args arguments which will passed to each {@link #newChild(Executor, Object...)} call
*/
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
............................省略................................
}
首先介紹一個新的構造器引數EventExecutorChooserFactory chooserFactory
。當客戶端連線完成三次握手後,Main Reactor
會建立客戶端連線NioSocketChannel
,並將其繫結到Sub Reactor Group
中的一個固定Reactor
,那麼具體要繫結到哪個具體的Sub Reactor
上呢?這個繫結策略就是由chooserFactory
來建立的。預設為DefaultEventExecutorChooserFactory
。
下面就是本小節的主題Reactor執行緒組
的建立過程:
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
if (executor == null) {
//用於建立Reactor執行緒
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
//迴圈建立reaactor group中的Reactor
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
//建立reactor
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
................省略................
}
}
}
//建立channel到Reactor的繫結策略
chooser = chooserFactory.newChooser(children);
................省略................
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
1. 建立用於啟動Reactor執行緒的executor
在Netty Reactor Group中的單個Reactor
的IO執行緒模型
為上篇文章提到的單Reactor單執行緒模型
,一個Reactor執行緒
負責輪詢
註冊其上的所有Channel
中的IO就緒事件
,處理IO事件,執行Netty中的非同步任務等工作。正是這個Reactor執行緒
驅動著整個Netty的運轉,可謂是Netty的核心引擎。
而這裡的executor
就是負責啟動Reactor執行緒
的,從建立原始碼中我們可以看到executor
的型別為ThreadPerTaskExecutor
。
ThreadPerTaskExecutor
public final class ThreadPerTaskExecutor implements Executor {
private final ThreadFactory threadFactory;
public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory");
}
@Override
public void execute(Runnable command) {
threadFactory.newThread(command).start();
}
}
我們看到ThreadPerTaskExecutor
做的事情很簡單,從它的命名字首ThreadPerTask
我們就可以猜出它的工作方式,就是來一個任務就建立一個執行緒執行。而建立的這個執行緒正是netty的核心引擎Reactor執行緒。
在Reactor執行緒
啟動的時候,Netty會將Reactor執行緒
要做的事情封裝成Runnable
,丟給exexutor
啟動。
而Reactor執行緒
的核心就是一個死迴圈
不停的輪詢
IO就緒事件,處理IO事件,執行非同步任務。一刻也不停歇,堪稱996典範
。
這裡向大家先賣個關子,"Reactor執行緒是何時啟動的呢??"
2. 建立Reactor
Reactor執行緒組NioEventLoopGroup
包含多個Reactor
,存放於private final EventExecutor[] children
陣列中。
所以下面的事情就是建立nThread
個Reactor
,並存放於EventExecutor[] children
欄位中,
我們來看下用於建立Reactor
的newChild(executor, args)
方法:
newChild
newChild
方法是MultithreadEventExecutorGroup
中的一個抽象方法,提供給具體子類實現。
protected abstract EventExecutor newChild(Executor executor, Object... args) throws Exception;
這裡我們解析的是NioEventLoopGroup
,我們來看下newChild
在該類中的實現:
public class NioEventLoopGroup extends MultithreadEventLoopGroup {
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
}
}
前邊提到的眾多構造器引數,這裡會通過可變引數Object... args
傳入到Reactor類NioEventLoop
的構造器中。
這裡介紹下新的引數EventLoopTaskQueueFactory queueFactory
,前邊提到Netty中的Reactor
主要工作是輪詢
註冊其上的所有Channel
上的IO就緒事件
,處理IO就緒事件
。除了這些主要的工作外,Netty為了極致的壓榨Reactor
的效能,還會讓它做一些非同步任務的執行工作。既然要執行非同步任務,那麼Reactor
中就需要一個佇列
來儲存任務。
這裡的EventLoopTaskQueueFactory
就是用來建立這樣的一個佇列來儲存Reactor
中待執行的非同步任務。
可以把Reactor
理解成為一個單執行緒的執行緒池
,類似
於JDK
中的SingleThreadExecutor
,僅用一個執行緒來執行輪詢IO就緒事件
,處理IO就緒事件
,執行非同步任務
。同時待執行的非同步任務儲存在Reactor
裡的taskQueue
中。
NioEventLoop
public final class NioEventLoop extends SingleThreadEventLoop {
//用於建立JDK NIO Selector,ServerSocketChannel
private final SelectorProvider provider;
//Selector輪詢策略 決定什麼時候輪詢,什麼時候處理IO事件,什麼時候執行非同步任務
private final SelectStrategy selectStrategy;
/**
* The NIO {@link Selector}.
*/
private Selector selector;
private Selector unwrappedSelector;
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory queueFactory) {
super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
rejectedExecutionHandler);
this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
}
這裡就正式開始了Reactor
的建立過程,我們知道Reactor
的核心是採用的IO多路複用模型
來對客戶端連線上的IO事件
進行監聽
,所以最重要的事情是建立Selector
(JDK NIO 中IO多路複用技術的實現
)。
可以把
Selector
理解為我們上篇文章介紹的Select,poll,epoll
,它是JDK NIO
對作業系統核心提供的這些IO多路複用技術
的封裝。
openSelector
openSelector
是NioEventLoop類
中用於建立IO多路複用
的Selector
,並對建立出來的JDK NIO
原生的Selector
進行效能優化。
首先會通過SelectorProvider#openSelector
建立JDK NIO原生的Selector
。
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
//通過JDK NIO SelectorProvider建立Selector
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
..................省略.............
}
SelectorProvider
會根據作業系統的不同選擇JDK在不同作業系統版本下的對應Selector
的實現。Linux下會選擇Epoll
,Mac下會選擇Kqueue
。
下面我們就來看下SelectorProvider
是如何做到自動適配不同作業系統下IO多路複用
實現的
SelectorProvider
public NioEventLoopGroup(ThreadFactory threadFactory) {
this(0, threadFactory, SelectorProvider.provider());
}
SelectorProvider
是在前面介紹的NioEventLoopGroup類
建構函式中通過呼叫SelectorProvider.provider()
被載入,並通過NioEventLoopGroup#newChild
方法中的可變長引數Object... args
傳遞到NioEventLoop
中的private final SelectorProvider provider
欄位中。
SelectorProvider的載入過程:
public abstract class SelectorProvider {
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
}
從SelectorProvider
載入原始碼中我們可以看出,SelectorProvider
的載入方式有三種,優先順序如下:
- 通過系統變數
-D java.nio.channels.spi.SelectorProvider
指定SelectorProvider
的自定義實現類全限定名
。通過應用程式類載入器(Application Classloader)
載入。
private static boolean loadProviderFromProperty() {
String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
if (cn == null)
return false;
try {
Class<?> c = Class.forName(cn, true,
ClassLoader.getSystemClassLoader());
provider = (SelectorProvider)c.newInstance();
return true;
}
.................省略.............
}
- 通過
SPI
方式載入。在工程目錄META-INF/services
下定義名為java.nio.channels.spi.SelectorProvider
的SPI檔案
,檔案中第一個定義的SelectorProvider
實現類全限定名就會被載入。
private static boolean loadProviderAsService() {
ServiceLoader<SelectorProvider> sl =
ServiceLoader.load(SelectorProvider.class,
ClassLoader.getSystemClassLoader());
Iterator<SelectorProvider> i = sl.iterator();
for (;;) {
try {
if (!i.hasNext())
return false;
provider = i.next();
return true;
} catch (ServiceConfigurationError sce) {
if (sce.getCause() instanceof SecurityException) {
// Ignore the security exception, try the next provider
continue;
}
throw sce;
}
}
}
- 如果以上兩種方式均未被定義,那麼就採用
SelectorProvider
系統預設實現sun.nio.ch.DefaultSelectorProvider
。筆者當前使用的作業系統是MacOS
,從原始碼中我們可以看到自動適配了KQueue
實現。
public class DefaultSelectorProvider {
private DefaultSelectorProvider() {
}
public static SelectorProvider create() {
return new KQueueSelectorProvider();
}
}
不同作業系統中JDK對於
DefaultSelectorProvider
會有所不同,Linux核心版本2.6以上對應的Epoll
,Linux核心版本2.6以下對應的Poll
,MacOS對應的是KQueue
。
下面我們接著回到io.netty.channel.nio.NioEventLoop#openSelector
的主線上來。
Netty對JDK NIO 原生Selector的優化
首先在NioEventLoop
中有一個Selector優化開關DISABLE_KEY_SET_OPTIMIZATION
,通過系統變數-D io.netty.noKeySetOptimization
指定,預設是開啟的,表示需要對JDK NIO原生Selector
進行優化。
public final class NioEventLoop extends SingleThreadEventLoop {
//Selector優化開關 預設開啟 為了遍歷的效率 會對Selector中的SelectedKeys進行資料結構優化
private static final boolean DISABLE_KEY_SET_OPTIMIZATION =
SystemPropertyUtil.getBoolean("io.netty.noKeySetOptimization", false);
}
如果優化開關DISABLE_KEY_SET_OPTIMIZATION
是關閉的,那麼直接返回JDK NIO原生的Selector
。
private SelectorTuple openSelector() {
..........SelectorProvider建立JDK NIO 原生Selector..............
if (DISABLE_KEY_SET_OPTIMIZATION) {
//JDK NIO原生Selector ,Selector優化開關 預設開啟需要對Selector進行優化
return new SelectorTuple(unwrappedSelector);
}
}
下面為Netty對JDK NIO原生的Selector
的優化過程:
- 獲取
JDK NIO原生Selector
的抽象實現類sun.nio.ch.SelectorImpl
。JDK NIO原生Selector
的實現均繼承於該抽象類。用於判斷由SelectorProvider
建立出來的Selector
是否為JDK預設實現
(SelectorProvider
第三種載入方式)。因為SelectorProvider
可以是自定義載入,所以它建立出來的Selector
並不一定是JDK NIO 原生的。
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
JDK NIO Selector的抽象類sun.nio.ch.SelectorImpl
public abstract class SelectorImpl extends AbstractSelector {
// The set of keys with data ready for an operation
// //IO就緒的SelectionKey(裡面包裹著channel)
protected Set<SelectionKey> selectedKeys;
// The set of keys registered with this Selector
//註冊在該Selector上的所有SelectionKey(裡面包裹著channel)
protected HashSet<SelectionKey> keys;
// Public views of the key sets
//用於向呼叫執行緒返回的keys,不可變
private Set<SelectionKey> publicKeys; // Immutable
//當有IO就緒的SelectionKey時,向呼叫執行緒返回。只可刪除其中元素,不可增加
private Set<SelectionKey> publicSelectedKeys; // Removal allowed, but not addition
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = new HashSet<SelectionKey>();
selectedKeys = new HashSet<SelectionKey>();
if (Util.atBugLevel("1.4")) {
publicKeys = keys;
publicSelectedKeys = selectedKeys;
} else {
//不可變
publicKeys = Collections.unmodifiableSet(keys);
//只可刪除其中元素,不可增加
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
}
}
這裡筆者來簡單介紹下JDK NIO中的Selector
中這幾個欄位的含義,我們可以和上篇文章講到的epoll在核心中的結構做類比,方便大家後續的理解:
Set<SelectionKey> selectedKeys
類似於我們上篇文章講解Epoll
時提到的就緒佇列eventpoll->rdllist
,Selector
這裡大家可以理解為Epoll
。Selector
會將自己監聽到的IO就緒
的Channel
放到selectedKeys
中。
這裡的
SelectionKey
暫且可以理解為Channel
在Selector
中的表示,類比上圖中epitem結構
裡的epoll_event
,封裝IO就緒Socket的資訊。
其實SelectionKey
裡包含的資訊不止是Channel
還有很多IO相關的資訊。後面我們在詳細介紹。
HashSet<SelectionKey> keys:
這裡存放的是所有註冊到該Selector
上的Channel
。類比epoll中的紅黑樹結構rb_root
SelectionKey
在Channel
註冊到Selector
中後生成。
-
Set<SelectionKey> publicSelectedKeys
相當於是selectedKeys
的檢視,用於向外部執行緒返回IO就緒
的SelectionKey
。這個集合在外部執行緒中只能做刪除操作不可增加元素
,並且不是執行緒安全的
。 -
Set<SelectionKey> publicKeys
相當於keys
的不可變檢視,用於向外部執行緒返回所有註冊在該Selector
上的SelectionKey
這裡需要重點關注
抽象類sun.nio.ch.SelectorImpl
中的selectedKeys
和publicSelectedKeys
這兩個欄位,注意它們的型別都是HashSet
,一會優化的就是這裡!!!!
- 判斷由
SelectorProvider
建立出來的Selector
是否是JDK NIO原生的Selector
實現。因為Netty優化針對的是JDK NIO 原生Selector
。判斷標準為sun.nio.ch.SelectorImpl
類是否為SelectorProvider
建立出Selector
的父類。如果不是則直接返回。不在繼續下面的優化過程。
//判斷是否可以對Selector進行優化,這裡主要針對JDK NIO原生Selector的實現類進行優化,因為SelectorProvider可以載入的是自定義Selector實現
//如果SelectorProvider建立的Selector不是JDK原生sun.nio.ch.SelectorImpl的實現類,那麼無法進行優化,直接返回
if (!(maybeSelectorImplClass instanceof Class) ||
!((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
if (maybeSelectorImplClass instanceof Throwable) {
Throwable t = (Throwable) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
}
return new SelectorTuple(unwrappedSelector);
}
通過前面對SelectorProvider
的介紹我們知道,這裡通過provider.openSelector()
建立出來的Selector
實現類為KQueueSelectorImpl類
,它繼承實現了sun.nio.ch.SelectorImpl
,所以它是JDK NIO 原生的Selector
實現
class KQueueSelectorImpl extends SelectorImpl {
}
- 建立
SelectedSelectionKeySet
通過反射替換掉sun.nio.ch.SelectorImpl類
中selectedKeys
和publicSelectedKeys
的預設HashSet
實現。
為什麼要用SelectedSelectionKeySet
替換掉原來的HashSet
呢??
因為這裡涉及到對HashSet型別
的sun.nio.ch.SelectorImpl#selectedKeys
集合的兩種操作:
-
插入操作: 通過前邊對
sun.nio.ch.SelectorImpl類
中欄位的介紹我們知道,在Selector
監聽到IO就緒
的SelectionKey
後,會將IO就緒
的SelectionKey
插入sun.nio.ch.SelectorImpl#selectedKeys
集合中,這時Reactor執行緒
會從java.nio.channels.Selector#select(long)
阻塞呼叫中返回(類似上篇文章提到的epoll_wait
)。 -
遍歷操作:
Reactor執行緒
返回後,會從Selector
中獲取IO就緒
的SelectionKey
集合(也就是sun.nio.ch.SelectorImpl#selectedKeys
),Reactor執行緒
遍歷selectedKeys
,獲取IO就緒
的SocketChannel
,並處理SocketChannel
上的IO事件
。
我們都知道HashSet
底層資料結構是一個雜湊表
,由於Hash衝突
這種情況的存在,所以導致對雜湊表
進行插入
和遍歷
操作的效能不如對陣列
進行插入
和遍歷
操作的效能好。
還有一個重要原因是,陣列可以利用CPU快取的優勢來提高遍歷的效率。後面筆者會有一篇專門的文章來講述利用CPU快取行如何為我們帶來效能優勢。
所以Netty為了優化對sun.nio.ch.SelectorImpl#selectedKeys
集合的插入,遍歷
效能,自己用陣列
這種資料結構實現了SelectedSelectionKeySet
,用它來替換原來的HashSet
實現。
SelectedSelectionKeySet
-
初始化
SelectionKey[] keys
陣列大小為1024
,當陣列容量不夠時,擴容為原來的兩倍大小。 -
通過陣列尾部指標
size
,在向陣列插入元素的時候可以直接定位到插入位置keys[size++]
。操作一步到位,不用像雜湊表
那樣還需要解決Hash衝突
。 -
對陣列的遍歷操作也是如絲般順滑,CPU直接可以在快取行中遍歷讀取陣列元素無需訪問記憶體。比
HashSet
的迭代器java.util.HashMap.KeyIterator
遍歷方式效能不知高到哪裡去了。
final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
//採用陣列替換到JDK中的HashSet,這樣add操作和遍歷操作效率更高,不需要考慮hash衝突
SelectionKey[] keys;
//陣列尾部指標
int size;
SelectedSelectionKeySet() {
keys = new SelectionKey[1024];
}
/**
* 陣列的新增效率高於 HashSet 因為不需要考慮hash衝突
* */
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
//時間複雜度O(1)
keys[size++] = o;
if (size == keys.length) {
//擴容為原來的兩倍大小
increaseCapacity();
}
return true;
}
private void increaseCapacity() {
SelectionKey[] newKeys = new SelectionKey[keys.length << 1];
System.arraycopy(keys, 0, newKeys, 0, size);
keys = newKeys;
}
/**
* 採用陣列的遍歷效率 高於 HashSet
* */
@Override
public Iterator<SelectionKey> iterator() {
return new Iterator<SelectionKey>() {
private int idx;
@Override
public boolean hasNext() {
return idx < size;
}
@Override
public SelectionKey next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return keys[idx++];
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
看到這裡不禁感嘆,從各種小的細節可以看出Netty對效能的優化簡直淋漓盡致,對效能的追求令人髮指。細節真的是魔鬼。
- Netty通過反射的方式用
SelectedSelectionKeySet
替換掉sun.nio.ch.SelectorImpl#selectedKeys
,sun.nio.ch.SelectorImpl#publicSelectedKeys
這兩個集合中原來HashSet
的實現。
- 反射獲取
sun.nio.ch.SelectorImpl
類中selectedKeys
和publicSelectedKeys
。
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
Java9
版本以上通過sun.misc.Unsafe
設定欄位值的方式
if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
long publicSelectedKeysFieldOffset =
PlatformDependent.objectFieldOffset(publicSelectedKeysField);
if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
PlatformDependent.putObject(
unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
PlatformDependent.putObject(
unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
return null;
}
}
- 通過反射的方式用
SelectedSelectionKeySet
替換掉hashSet
實現的sun.nio.ch.SelectorImpl#selectedKeys,sun.nio.ch.SelectorImpl#publicSelectedKeys
。
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
//Java8反射替換欄位
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
- 將與
sun.nio.ch.SelectorImpl
類中selectedKeys
和publicSelectedKeys
關聯好的Netty優化實現SelectedSelectionKeySet
,設定到io.netty.channel.nio.NioEventLoop#selectedKeys
欄位中儲存。
//會通過反射替換selector物件中的selectedKeySet儲存就緒的selectKey
//該欄位為持有selector物件selectedKeys的引用,當IO事件就緒時,直接從這裡獲取
private SelectedSelectionKeySet selectedKeys;
後續
Reactor執行緒
就會直接從io.netty.channel.nio.NioEventLoop#selectedKeys
中獲取IO就緒
的SocketChannel
- 用
SelectorTuple
封裝unwrappedSelector
和wrappedSelector
返回給NioEventLoop
建構函式。到此Reactor
中的Selector
就建立完畢了。
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
private static final class SelectorTuple {
final Selector unwrappedSelector;
final Selector selector;
SelectorTuple(Selector unwrappedSelector) {
this.unwrappedSelector = unwrappedSelector;
this.selector = unwrappedSelector;
}
SelectorTuple(Selector unwrappedSelector, Selector selector) {
this.unwrappedSelector = unwrappedSelector;
this.selector = selector;
}
}
-
所謂的
unwrappedSelector
是指被Netty優化過的JDK NIO原生Selector。 -
所謂的
wrappedSelector
就是用SelectedSelectionKeySetSelector
裝飾類將unwrappedSelector
和與sun.nio.ch.SelectorImpl類
關聯好的Netty優化實現SelectedSelectionKeySet
封裝裝飾起來。
wrappedSelector
會將所有對Selector
的操作全部代理給unwrappedSelector
,並在發起輪詢IO事件
的相關操作中,重置SelectedSelectionKeySet
清空上一次的輪詢結果。
final class SelectedSelectionKeySetSelector extends Selector {
//Netty優化後的 SelectedKey就緒集合
private final SelectedSelectionKeySet selectionKeys;
//優化後的JDK NIO 原生Selector
private final Selector delegate;
SelectedSelectionKeySetSelector(Selector delegate, SelectedSelectionKeySet selectionKeys) {
this.delegate = delegate;
this.selectionKeys = selectionKeys;
}
@Override
public boolean isOpen() {
return delegate.isOpen();
}
@Override
public SelectorProvider provider() {
return delegate.provider();
}
@Override
public Set<SelectionKey> keys() {
return delegate.keys();
}
@Override
public Set<SelectionKey> selectedKeys() {
return delegate.selectedKeys();
}
@Override
public int selectNow() throws IOException {
//重置SelectedKeys集合
selectionKeys.reset();
return delegate.selectNow();
}
@Override
public int select(long timeout) throws IOException {
//重置SelectedKeys集合
selectionKeys.reset();
return delegate.select(timeout);
}
@Override
public int select() throws IOException {
//重置SelectedKeys集合
selectionKeys.reset();
return delegate.select();
}
@Override
public Selector wakeup() {
return delegate.wakeup();
}
@Override
public void close() throws IOException {
delegate.close();
}
}
到這裡Reactor的核心Selector就建立好了,下面我們來看下用於儲存非同步任務的佇列是如何建立出來的。
newTaskQueue
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory queueFactory) {
super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
rejectedExecutionHandler);
this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
final SelectorTuple selectorTuple = openSelector();
//通過用SelectedSelectionKeySet裝飾後的unwrappedSelector
this.selector = selectorTuple.selector;
//Netty優化過的JDK NIO遠端Selector
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
我們繼續回到建立Reactor
的主線上,到目前為止Reactor
的核心Selector
就建立好了,前邊我們提到Reactor
除了需要監聽IO就緒事件
以及處理IO就緒事件
外,還需要執行一些非同步任務,當外部執行緒向Reactor
提交非同步任務後,Reactor
就需要一個佇列來儲存這些非同步任務,等待Reactor執行緒
執行。
下面我們來看下Reactor
中任務佇列的建立過程:
//任務佇列大小,預設是無界佇列
protected static final int DEFAULT_MAX_PENDING_TASKS = Math.max(16,
SystemPropertyUtil.getInt("io.netty.eventLoop.maxPendingTasks", Integer.MAX_VALUE));
private static Queue<Runnable> newTaskQueue(
EventLoopTaskQueueFactory queueFactory) {
if (queueFactory == null) {
return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
}
return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
}
private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
// This event loop never calls takeTask()
return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
: PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
}
-
在
NioEventLoop
的父類SingleThreadEventLoop
中提供了一個靜態變數DEFAULT_MAX_PENDING_TASKS
用來指定Reactor
任務佇列的大小。可以通過系統變數-D io.netty.eventLoop.maxPendingTasks
進行設定,預設為Integer.MAX_VALUE
,表示任務佇列預設為無界佇列
。 -
根據
DEFAULT_MAX_PENDING_TASKS
變數的設定,來決定建立無界任務佇列還是有界任務佇列。
//建立無界任務佇列
PlatformDependent.<Runnable>newMpscQueue()
//建立有界任務佇列
PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks)
public static <T> Queue<T> newMpscQueue() {
return Mpsc.newMpscQueue();
}
public static <T> Queue<T> newMpscQueue(final int maxCapacity) {
return Mpsc.newMpscQueue(maxCapacity);
}
Reactor
內的非同步任務佇列的型別為MpscQueue
,它是由JCTools
提供的一個高效能無鎖佇列,從命名字首Mpsc
可以看出,它適用於多生產者單消費者
的場景,它支援多個生產者執行緒安全的訪問佇列,同一時刻只允許一個消費者執行緒讀取佇列中的元素。
我們知道Netty中的
Reactor
可以執行緒安全
的處理註冊其上的多個SocketChannel
上的IO資料
,保證Reactor執行緒安全
的核心原因正是因為這個MpscQueue
,它可以支援多個業務執行緒在處理完業務邏輯後,執行緒安全的向MpscQueue
新增非同步寫任務
,然後由單個Reactor執行緒
來執行這些寫任務
。既然是單執行緒執行,那肯定是執行緒安全的了。
Reactor對應的NioEventLoop型別繼承結構
NioEventLoop
的繼承結構也是比較複雜,這裡我們只關注在Reactor
建立過程中涉及的到兩個父類SingleThreadEventLoop
,SingleThreadEventExecutor
。
剩下的繼承體系,我們在後邊隨著Netty
原始碼的深入在慢慢介紹。
前邊我們提到,其實Reactor
我們可以看作是一個單執行緒的執行緒池,只有一個執行緒用來執行IO就緒事件的監聽
,IO事件的處理
,非同步任務的執行
。用MpscQueue
來儲存待執行的非同步任務。
命名字首為SingleThread
的父類都是對Reactor
這些行為的分層定義。也是本小節要介紹的物件
SingleThreadEventLoop
Reactor
負責執行的非同步任務分為三類:
普通任務:
這是Netty最主要執行的非同步任務,存放在普通任務佇列taskQueue
中。在NioEventLoop
建構函式中建立。定時任務:
存放在優先順序佇列中。後續我們介紹。尾部任務:
存放於尾部任務佇列tailTasks
中,尾部任務一般不常用,在普通任務執行完後 Reactor執行緒會執行尾部任務。使用場景:比如對Netty 的執行狀態做一些統計資料,例如任務迴圈的耗時、佔用實體記憶體的大小等等都可以向尾部佇列新增一個收尾任務完成統計資料的實時更新。
SingleThreadEventLoop
負責對尾部任務佇列tailTasks
進行管理。並且提供Channel
向Reactor
註冊的行為。
public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop {
//任務佇列大小,預設是無界佇列
protected static final int DEFAULT_MAX_PENDING_TASKS = Math.max(16,
SystemPropertyUtil.getInt("io.netty.eventLoop.maxPendingTasks", Integer.MAX_VALUE));
//尾部任務佇列
private final Queue<Runnable> tailTasks;
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
boolean addTaskWakesUp, Queue<Runnable> taskQueue, Queue<Runnable> tailTaskQueue,
RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler);
//尾部佇列 執行一些統計任務 不常用
tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue");
}
@Override
public ChannelFuture register(Channel channel) {
//註冊channel到繫結的Reactor上
return register(new DefaultChannelPromise(channel, this));
}
}
SingleThreadEventExecutor
SingleThreadEventExecutor
主要負責對普通任務佇列
的管理,以及非同步任務的執行
,Reactor執行緒的啟停
。
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, Queue<Runnable> taskQueue, RejectedExecutionHandler rejectedHandler) {
//parent為Reactor所屬的NioEventLoopGroup Reactor執行緒組
super(parent);
//向Reactor新增任務時,是否喚醒Selector停止輪詢IO就緒事件,馬上執行非同步任務
this.addTaskWakesUp = addTaskWakesUp;
//Reactor非同步任務佇列的大小
this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;
//用於啟動Reactor執行緒的executor -> ThreadPerTaskExecutor
this.executor = ThreadExecutorMap.apply(executor, this);
//普通任務佇列
this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue");
//任務佇列滿時的拒絕策略
this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
}
到現在為止,一個完整的Reactor架構
就被建立出來了。
3. 建立Channel到Reactor的繫結策略
到這一步,Reactor執行緒組NioEventLoopGroup
裡邊的所有Reactor
就已經全部建立完畢。
無論是Netty服務端NioServerSocketChannel
關注的OP_ACCEPT
事件也好,還是Netty客戶端NioSocketChannel
關注的OP_READ
和OP_WRITE
事件也好,都需要先註冊到Reactor
上,Reactor
才能監聽Channel
上關注的IO事件
實現IO多路複用
。
NioEventLoopGroup
(Reactor執行緒組)裡邊有眾多的Reactor
,那麼以上提到的這些Channel
究竟應該註冊到哪個Reactor
上呢?這就需要一個繫結的策略來平均分配。
還記得我們前邊介紹MultithreadEventExecutorGroup類
的時候提到的構造器引數EventExecutorChooserFactory
嗎?
這時候它就派上用場了,它主要用來建立Channel
到Reactor
的繫結策略。預設為DefaultEventExecutorChooserFactory.INSTANCE
。
public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
//從Reactor集合中選擇一個特定的Reactor的繫結策略 用於channel註冊繫結到一個固定的Reactor上
private final EventExecutorChooserFactory.EventExecutorChooser chooser;
chooser = chooserFactory.newChooser(children);
}
下面我們來看下具體的繫結策略:
DefaultEventExecutorChooserFactory
public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {
public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();
private DefaultEventExecutorChooserFactory() { }
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
if (isPowerOfTwo(executors.length)) {
return new PowerOfTwoEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
}
private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
}
...................省略.................
}
我們看到在newChooser
方法繫結策略有兩個分支,不同之處在於需要判斷Reactor執行緒組中的Reactor
個數是否為2的次冪
。
Netty中的繫結策略就是採用round-robin
輪詢的方式來挨個選擇Reactor
進行繫結。
採用round-robin
的方式進行負載均衡,我們一般會用round % reactor.length
取餘的方式來挨個平均的定位到對應的Reactor
上。
如果Reactor
的個數reactor.length
恰好是2的次冪
,那麼就可以用位操作&
運算round & reactor.length -1
來代替%
運算round % reactor.length
,因為位運算的效能更高。具體為什麼&
運算能夠代替%
運算,筆者會在後面講述時間輪的時候為大家詳細證明,這裡大家只需記住這個公式,我們還是聚焦本文的主線。
瞭解了優化原理,我們在看程式碼實現就很容易理解了。
利用%
運算的方式Math.abs(idx.getAndIncrement() % executors.length)
來進行繫結。
private static final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicLong idx = new AtomicLong();
private final EventExecutor[] executors;
GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
public EventExecutor next() {
return executors[(int) Math.abs(idx.getAndIncrement() % executors.length)];
}
}
利用&
運算的方式idx.getAndIncrement() & executors.length - 1
來進行繫結。
private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
}
又一次被Netty對效能的極致追求所折服~~~~
4. 向Reactor執行緒組中所有的Reactor註冊terminated回撥函式
當Reactor執行緒組NioEventLoopGroup
中所有的Reactor
已經建立完畢,Channel
到Reactor
的繫結策略也建立完畢後,我們就來到了建立NioEventGroup
的最後一步。
俗話說的好,有建立就有啟動,有啟動就有關閉,這裡會建立Reactor關閉
的回撥函式terminationListener
,在Reactor
關閉時回撥。
terminationListener
回撥的邏輯很簡單:
-
通過
AtomicInteger terminatedChildren
變數記錄已經關閉的Reactor
個數,用來判斷NioEventLoopGroup
中的Reactor
是否已經全部關閉。 -
如果所有
Reactor
均已關閉,設定NioEventLoopGroup
中的terminationFuture
為success
。表示Reactor執行緒組
關閉成功。
//記錄關閉的Reactor個數,當Reactor全部關閉後,才可以認為關閉成功
private final AtomicInteger terminatedChildren = new AtomicInteger();
//關閉future
private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
//當所有Reactor關閉後 才認為是關閉成功
terminationFuture.setSuccess(null);
}
}
};
//為所有Reactor新增terminationListener
for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}
我們在回到文章開頭Netty服務端程式碼模板
public final class EchoServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// Configure the server.
//建立主從Reactor執行緒組
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
...........省略............
}
}
現在Netty的主從Reactor執行緒組
就已經建立完畢,此時Netty服務端的骨架已經搭建完畢,骨架如下:
總結
本文介紹了首先介紹了Netty對各種IO模型
的支援以及如何輕鬆切換各種IO模型
。
還花了大量的篇幅介紹Netty服務端的核心引擎主從Reactor執行緒組
的建立過程。在這個過程中,我們還提到了Netty對各種細節進行的優化,展現了Netty對效能極致的追求。
好了,Netty服務端的骨架已經搭好,剩下的事情就該繫結埠地址然後接收連線了,我們下篇文章再見~~~