解決Netty那些事兒之Reactor在Netty中的實現(xiàn)(創(chuàng)建篇)-上
本系列Netty源碼解析文章基于?4.1.56.Final版本
在上篇文章《聊聊Netty那些事兒之從內(nèi)核角度看IO模型》中我們花了大量的篇幅來從內(nèi)核角度詳細講述了五種IO模型
的演進過程以及ReactorIO線程模型
的底層基石IO多路復用技術(shù)在內(nèi)核中的實現(xiàn)原理。
最后我們引出了netty中使用的主從Reactor IO線程模型。

通過上篇文章的介紹,我們已經(jīng)清楚了在IO調(diào)用的過程中內(nèi)核幫我們搞了哪些事情,那么俗話說的好內(nèi)核領(lǐng)進門,修行在netty
,netty在用戶空間又幫我們搞了哪些事情?
那么從本文開始,筆者將從源碼角度來帶大家看下上圖中的Reactor IO線程模型
在Netty中是如何實現(xiàn)的。
本文作為Reactor在Netty中實現(xiàn)系列文章中的開篇文章,筆者先來為大家介紹Reactor的骨架是如何創(chuàng)建出來的。
在上篇文章中我們提到Netty采用的是主從Reactor多線程
的模型,但是它在實現(xiàn)上又與Doug Lea在Scalable IO in Java論文中提到的經(jīng)典主從Reactor多線程模型
有所差異。

Netty中的Reactor
是以Group
的形式出現(xiàn)的,主從Reactor
在Netty中就是主從Reactor組
,每個Reactor Group
中會有多個Reactor
用來執(zhí)行具體的IO任務(wù)
。當然在netty中Reactor
不只用來執(zhí)行IO任務(wù)
,這個我們后面再說。
Main Reactor Group
中的Reactor
數(shù)量取決于服務(wù)端要監(jiān)聽的端口個數(shù),通常我們的服務(wù)端程序只會監(jiān)聽一個端口,所以Main Reactor Group
只會有一個Main Reactor
線程來處理最重要的事情:綁定端口地址
,接收客戶端連接
,為客戶端創(chuàng)建對應(yīng)的SocketChannel
,將客戶端SocketChannel分配給一個固定的Sub Reactor
。也就是上篇文章筆者為大家舉的例子,飯店最重要的工作就是先把客人迎接進來。“我家大門常打開,開放懷抱等你,擁抱過就有了默契你會愛上這里......”

Sub Reactor Group
里有多個Reactor
線程,Reactor
線程的個數(shù)可以通過系統(tǒng)參數(shù)-D io.netty.eventLoopThreads
指定。默認的Reactor
的個數(shù)為CPU核數(shù) * 2
。Sub Reactor
線程主要用來輪詢客戶端SocketChannel上的IO就緒事件
,處理IO就緒事件
,執(zhí)行異步任務(wù)
。Sub Reactor Group
做的事情就是上篇飯店例子中服務(wù)員的工作,客人進來了要為客人分配座位,端茶送水,做菜上菜。“不管遠近都是客人,請不用客氣,相約好了在一起,我們歡迎您......”

一個
客戶端SocketChannel
只能分配給一個固定的Sub Reactor
。一個Sub Reactor
負責處理多個客戶端SocketChannel
,這樣可以將服務(wù)端承載的全量客戶端連接
分攤到多個Sub Reactor
中處理,同時也能保證客戶端SocketChannel上的IO處理的線程安全性
。
由于文章篇幅的關(guān)系,作為Reactor在netty中實現(xiàn)的第一篇我們主要來介紹主從Reactor Group
的創(chuàng)建流程,骨架脈絡(luò)先搭好。
下面我們來看一段Netty服務(wù)端代碼的編寫模板,從代碼模板的流程中我們來解析下主從Reactor的創(chuàng)建流程以及在這個過程中所涉及到的Netty核心類。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ??


Netty服務(wù)端代碼模板
首先我們要創(chuàng)建Netty最核心的部分 ->?創(chuàng)建主從Reactor Group
,在Netty中EventLoopGroup
就是Reactor Group
的實現(xiàn)類。對應(yīng)的EventLoop
就是Reactor
的實現(xiàn)類。
創(chuàng)建用于IO處理
的ChannelHandler
,實現(xiàn)相應(yīng)IO事件
的回調(diào)函數(shù),編寫對應(yīng)的IO處理
邏輯。注意這里只是簡單示例哈,詳細的IO事件處理,筆者會單獨開一篇文章專門講述。
創(chuàng)建
ServerBootstrap
Netty服務(wù)端啟動類,并在啟動類中配置啟動Netty服務(wù)端所需要的一些必備信息。在上篇文章介紹
Socket內(nèi)核結(jié)構(gòu)
小節(jié)中我們提到,在編寫服務(wù)端網(wǎng)絡(luò)程序時,我們首先要創(chuàng)建一個Socket
用于listen和bind
端口地址,我們把這個叫做監(jiān)聽Socket
,這里對應(yīng)的就是NioServerSocketChannel.class
。當客戶端連接完成三次握手,系統(tǒng)調(diào)用accept
函數(shù)會基于監(jiān)聽Socket
創(chuàng)建出來一個新的Socket
專門用于與客戶端之間的網(wǎng)絡(luò)通信我們稱為客戶端連接Socket
,這里對應(yīng)的就是NioSocketChannel.class
netty有兩種
Channel類型
:一種是服務(wù)端用于監(jiān)聽綁定端口地址的NioServerSocketChannel
,一種是用于客戶端通信的NioSocketChannel
。每種Channel類型實例
都會對應(yīng)一個PipeLine
用于編排對應(yīng)channel實例
上的IO事件處理邏輯。PipeLine
中組織的就是ChannelHandler
用于編寫特定的IO處理邏輯。注意
serverBootstrap.handler
設(shè)置的是服務(wù)端NioServerSocketChannel PipeLine
中的ChannelHandler
。ServerBootstrap
啟動類方法帶有child
前綴的均是設(shè)置客戶端NioSocketChannel
屬性的。ChannelInitializer
是用于當SocketChannel
成功注冊到綁定的Reactor
上后,用于初始化該SocketChannel
的Pipeline
。它的initChannel
方法會在注冊成功后執(zhí)行。這里只是捎帶提一下,讓大家有個初步印象,后面我會專門介紹。serverBootstrap.childHandler(ChannelHandler childHandler)
用于設(shè)置客戶端NioSocketChannel
中對應(yīng)Pipieline
中的ChannelHandler
。我們通常配置的編碼解碼器就是在這里。serverBootstrap.option(ChannelOption.SO_BACKLOG, 100)
設(shè)置服務(wù)端ServerSocketChannel
中的SocketOption
。關(guān)于SocketOption
的選項我們后邊的文章再聊,本文主要聚焦在Netty?Main Reactor Group
的創(chuàng)建及工作流程。serverBootstrap.handler(....)
設(shè)置服務(wù)端NioServerSocketChannel
中對應(yīng)Pipieline
中的ChannelHandler
。通過
serverBootstrap.group(bossGroup, workerGroup)
為Netty服務(wù)端配置主從Reactor Group
實例。通過
serverBootstrap.channel(NioServerSocketChannel.class)
配置Netty服務(wù)端的ServerSocketChannel
用于綁定端口地址
以及創(chuàng)建客戶端SocketChannel
。Netty中的NioServerSocketChannel.class
就是對JDK NIO中ServerSocketChannel
的封裝。而用于表示客戶端連接
的NioSocketChannel
是對JDK NIO?SocketChannel
封裝。ChannelFuture f = serverBootstrap.bind(PORT).sync()
這一步會是下篇文章要重點分析的主題Main Reactor Group
的啟動,綁定端口地址,開始監(jiān)聽客戶端連接事件(OP_ACCEPT
)。本文我們只關(guān)注創(chuàng)建流程。f.channel().closeFuture().sync()
等待服務(wù)端NioServerSocketChannel
關(guān)閉。Netty服務(wù)端到這里正式啟動,并準備好接受客戶端連接的準備。shutdownGracefully
優(yōu)雅關(guān)閉主從Reactor線程組
里的所有Reactor線程
。
Netty對IO模型的支持
在上篇文章中我們介紹了五種IO模型
,Netty中支持BIO
,NIO
,AIO
以及多種操作系統(tǒng)下的IO多路復用技術(shù)
實現(xiàn)。
在Netty中切換這幾種IO模型
也是非常的方便,下面我們來看下Netty如何對這幾種IO模型進行支持的。
首先我們介紹下幾個與IO模型
相關(guān)的重要接口:
EventLoop
EventLoop
就是Netty中的Reactor
,可以說它就是Netty的引擎,負責Channel上IO就緒事件的監(jiān)聽
,IO就緒事件的處理
,異步任務(wù)的執(zhí)行
驅(qū)動著整個Netty的運轉(zhuǎn)。
不同IO模型
下,EventLoop
有著不同的實現(xiàn),我們只需要切換不同的實現(xiàn)類就可以完成對NettyIO模型
的切換。

在NIO模型
下Netty會自動
根據(jù)操作系統(tǒng)以及版本的不同選擇對應(yīng)的IO多路復用技術(shù)實現(xiàn)
。比如Linux 2.6版本以上用的是Epoll
,2.6版本以下用的是Poll
,Mac下采用的是Kqueue
。
其中Linux kernel 在5.1版本引入的異步IO庫io_uring正在netty中孵化。
EventLoopGroup
Netty中的Reactor
是以Group
的形式出現(xiàn)的,EventLoopGroup
正是Reactor組
的接口定義,負責管理Reactor
,Netty中的Channel
就是通過EventLoopGroup
注冊到具體的Reactor
上的。
Netty的IO線程模型是主從Reactor多線程模型
,主從Reactor線程組
在Netty源碼中對應(yīng)的其實就是兩個EventLoopGroup
實例。
不同的IO模型
也有對應(yīng)的實現(xiàn):

SocketChannel
用于與客戶端通信的SocketChannel
,對應(yīng)于上篇文章提到的客戶端連接Socket
,當客戶端完成三次握手后,由系統(tǒng)調(diào)用accept
函數(shù)根據(jù)監(jiān)聽Socket
創(chuàng)建。
不同的IO模型
下的實現(xiàn):

我們看到在不同IO模型
的實現(xiàn)中,Netty這些圍繞IO模型
的核心類只是前綴的不同:
BIO對應(yīng)的前綴為
Oio
表示old io
,現(xiàn)在已經(jīng)廢棄不推薦使用。NIO對應(yīng)的前綴為
Nio
,正是Netty推薦也是我們常用的非阻塞IO模型
。AIO對應(yīng)的前綴為
Aio
,由于Linux下的異步IO
機制實現(xiàn)的并不成熟,性能提升表現(xiàn)上也不明顯,現(xiàn)已被刪除。
我們只需要將IO模型
的這些核心接口對應(yīng)的實現(xiàn)類前綴
改為對應(yīng)IO模型
的前綴,就可以輕松在Netty中完成對IO模型
的切換。

多種NIO的實現(xiàn)

我們通常在使用NIO模型
的時候會使用Common列
下的這些IO模型
核心類,Common類
也會根據(jù)操作系統(tǒng)的不同自動選擇JDK
在對應(yīng)平臺下的IO多路復用技術(shù)
的實現(xiàn)。
而Netty自身也根據(jù)操作系統(tǒng)的不同提供了自己對IO多路復用技術(shù)
的實現(xiàn),比JDK
的實現(xiàn)性能更優(yōu)。比如:
JDK
的 NIO?默認
實現(xiàn)是水平觸發(fā)
,Netty 是邊緣觸發(fā)(默認)
和水平觸發(fā)可切換。。Netty 實現(xiàn)的垃圾回收更少、性能更好。
我們編寫Netty服務(wù)端程序的時候也可以根據(jù)操作系統(tǒng)的不同,采用Netty自身的實現(xiàn)來進一步優(yōu)化程序。做法也很簡單,直接將上圖中紅框里的實現(xiàn)類替換成Netty的自身實現(xiàn)類即可完成切換。
經(jīng)過以上對Netty服務(wù)端代碼編寫模板以及IO模型
相關(guān)核心類的簡單介紹,我們對Netty的創(chuàng)建流程有了一個簡單粗略的總體認識,下面我們來深入剖析下創(chuàng)建流程過程中的每一個步驟以及這個過程中涉及到的核心類實現(xiàn)。
以下源碼解析部分我們均采用Common列
下NIO
相關(guān)的實現(xiàn)進行解析。
創(chuàng)建主從Reactor線程組
在Netty服務(wù)端程序編寫模板的開始,我們首先會創(chuàng)建兩個Reactor線程組:

一個是主Reactor線程組
bossGroup
用于監(jiān)聽客戶端連接,創(chuàng)建客戶端連接NioSocketChannel
,并將創(chuàng)建好的客戶端連接NioSocketChannel
注冊到從Reactor線程組中一個固定的Reactor
上。一個是從Reactor線程組
workerGroup
,workerGroup
中的Reactor
負責監(jiān)聽綁定在其上的客戶端連接NioSocketChannel
上的IO就緒事件
,并處理IO就緒事件
,執(zhí)行異步任務(wù)
。
Netty中Reactor線程組的實現(xiàn)類為NioEventLoopGroup
,在創(chuàng)建bossGroup
和workerGroup
的時候用到了NioEventLoopGroup
的兩個構(gòu)造函數(shù):
帶
nThreads
參數(shù)的構(gòu)造函數(shù)public NioEventLoopGroup(int nThreads)
。不帶
nThreads
參數(shù)的默認
構(gòu)造函數(shù)public NioEventLoopGroup()
nThreads
參數(shù)表示當前要創(chuàng)建的Reactor線程組
內(nèi)包含多少個Reactor線程
。不指定nThreads
參數(shù)的話采用默認的Reactor線程
個數(shù),用0
表示。
最終會調(diào)用到構(gòu)造函數(shù)
下面簡單介紹下構(gòu)造函數(shù)中這幾個參數(shù)的作用,后面我們在講解本文主線的過程中還會提及這幾個參數(shù),到時在詳細介紹,這里只是讓大家有個初步印象,不必做過多的糾纏。
Executor executor:
負責啟動Reactor線程
進而Reactor才可以開始工作。
Reactor線程組
NioEventLoopGroup
負責創(chuàng)建Reactor線程
,在創(chuàng)建的時候會將executor
傳入。
RejectedExecutionHandler:
?當向Reactor
添加異步任務(wù)添加失敗時,采用的拒絕策略。Reactor的任務(wù)不只是監(jiān)聽IO活躍事件和IO任務(wù)的處理,還包括對異步任務(wù)的處理。這里大家只需有個這樣的概念,后面筆者會專門詳細介紹。SelectorProvider selectorProvider:
?Reactor中的IO模型為IO多路復用模型
,對應(yīng)于JDK NIO中的實現(xiàn)為java.nio.channels.Selector
(就是我們上篇文章中提到的select,poll,epoll
),每個Reator中都包含一個Selector
,用于輪詢
注冊在該Reactor上的所有Channel
上的IO事件
。SelectorProvider
就是用來創(chuàng)建Selector
的。SelectStrategyFactory selectStrategyFactory:
?Reactor最重要的事情就是輪詢
注冊其上的Channel
上的IO就緒事件
,這里的SelectStrategyFactory
用于指定輪詢策略
,默認為DefaultSelectStrategyFactory.INSTANCE
。
最終會將這些參數(shù)交給NioEventLoopGroup
的父類構(gòu)造器,下面我們來看下NioEventLoopGroup類
的繼承結(jié)構(gòu):

NioEventLoopGroup類
的繼承結(jié)構(gòu)乍一看比較復雜,大家不要慌,筆者會隨著主線的深入慢慢地介紹這些父類接口,我們現(xiàn)在重點關(guān)注Mutithread
前綴的類。
我們知道NioEventLoopGroup
是Netty中的Reactor線程組
的實現(xiàn),既然是線程組那么肯定是負責管理和創(chuàng)建多個Reactor線程的
,所以Mutithread
前綴的類定義的行為自然是對Reactor線程組
內(nèi)多個Reactor線程
的創(chuàng)建和管理工作。
MultithreadEventLoopGroup
MultithreadEventLoopGroup類
主要的功能就是用來確定Reactor線程組
內(nèi)Reactor
的個數(shù)。
默認的Reactor
的個數(shù)存放于字段DEFAULT_EVENT_LOOP_THREADS
中。
從static {}
靜態(tài)代碼塊中我們可以看出默認Reactor
的個數(shù)的獲取邏輯:
可以通過系統(tǒng)變量?
-D io.netty.eventLoopThreads"
指定。如果不指定,那么默認的就是
NettyRuntime.availableProcessors() * 2
當nThread
參數(shù)設(shè)置為0
采用默認設(shè)置時,Reactor線程組
內(nèi)的Reactor
個數(shù)則設(shè)置為DEFAULT_EVENT_LOOP_THREADS
。
MultithreadEventExecutorGroup
MultithreadEventExecutorGroup
這里就是本小節(jié)的核心,主要用來定義創(chuàng)建和管理Reactor
的行為。
首先介紹一個新的構(gòu)造器參數(shù)EventExecutorChooserFactory chooserFactory
。當客戶端連接完成三次握手后,Main Reactor
會創(chuàng)建客戶端連接NioSocketChannel
,并將其綁定到Sub Reactor Group
中的一個固定Reactor
,那么具體要綁定到哪個具體的Sub Reactor
上呢?這個綁定策略就是由chooserFactory
來創(chuàng)建的。默認為DefaultEventExecutorChooserFactory
。
下面就是本小節(jié)的主題Reactor線程組
的創(chuàng)建過程:
1. 創(chuàng)建用于啟動Reactor線程的executor
在Netty Reactor Group中的單個Reactor
的IO線程模型
為上篇文章提到的單Reactor單線程模型
,一個Reactor線程
負責輪詢
注冊其上的所有Channel
中的IO就緒事件
,處理IO事件,執(zhí)行Netty中的異步任務(wù)等工作。正是這個Reactor線程
驅(qū)動著整個Netty的運轉(zhuǎn),可謂是Netty的核心引擎。

而這里的executor
就是負責啟動Reactor線程
的,從創(chuàng)建源碼中我們可以看到executor
的類型為ThreadPerTaskExecutor
。
ThreadPerTaskExecutor
我們看到ThreadPerTaskExecutor
做的事情很簡單,從它的命名前綴ThreadPerTask
我們就可以猜出它的工作方式,就是來一個任務(wù)就創(chuàng)建一個線程執(zhí)行。而創(chuàng)建的這個線程正是netty的核心引擎Reactor線程。
在Reactor線程
啟動的時候,Netty會將Reactor線程
要做的事情封裝成Runnable
,丟給exexutor
啟動。
而Reactor線程
的核心就是一個死循環(huán)
不停的輪詢
IO就緒事件,處理IO事件,執(zhí)行異步任務(wù)。一刻也不停歇,堪稱996典范
。
這里向大家先賣個關(guān)子,"Reactor線程是何時啟動的呢??"
2. 創(chuàng)建Reactor
Reactor線程組NioEventLoopGroup
包含多個Reactor
,存放于private final EventExecutor[] children
數(shù)組中。
所以下面的事情就是創(chuàng)建nThread
個Reactor
,并存放于EventExecutor[] children
字段中,
我們來看下用于創(chuàng)建Reactor
的newChild(executor, args)
方法:
newChild
newChild
方法是MultithreadEventExecutorGroup
中的一個抽象方法,提供給具體子類實現(xiàn)。
這里我們解析的是NioEventLoopGroup
,我們來看下newChild
在該類中的實現(xiàn):
前邊提到的眾多構(gòu)造器參數(shù),這里會通過可變參數(shù)Object... args
傳入到Reactor類NioEventLoop
的構(gòu)造器中。
這里介紹下新的參數(shù)EventLoopTaskQueueFactory queueFactory
,前邊提到Netty中的Reactor
主要工作是輪詢
注冊其上的所有Channel
上的IO就緒事件
,處理IO就緒事件
。除了這些主要的工作外,Netty為了極致的壓榨Reactor
的性能,還會讓它做一些異步任務(wù)的執(zhí)行工作。既然要執(zhí)行異步任務(wù),那么Reactor
中就需要一個隊列
來保存任務(wù)。
這里的EventLoopTaskQueueFactory
就是用來創(chuàng)建這樣的一個隊列來保存Reactor
中待執(zhí)行的異步任務(wù)。
可以把Reactor
理解成為一個單線程的線程池
,類似
于JDK
中的SingleThreadExecutor
,僅用一個線程來執(zhí)行輪詢IO就緒事件
,處理IO就緒事件
,執(zhí)行異步任務(wù)
。同時待執(zhí)行的異步任務(wù)保存在Reactor
里的taskQueue
中。
NioEventLoop
這里就正式開始了Reactor
的創(chuàng)建過程,我們知道Reactor
的核心是采用的IO多路復用模型
來對客戶端連接上的IO事件
進行監(jiān)聽
,所以最重要的事情是創(chuàng)建Selector
(JDK NIO 中IO多路復用技術(shù)的實現(xiàn)
)。
可以把
Selector
理解為我們上篇文章介紹的Select,poll,epoll
,它是JDK NIO
對操作系統(tǒng)內(nèi)核提供的這些IO多路復用技術(shù)
的封裝。
openSelector
openSelector
是NioEventLoop類
中用于創(chuàng)建IO多路復用
的Selector
,并對創(chuàng)建出來的JDK NIO
?原生的Selector
進行性能優(yōu)化。
首先會通過SelectorProvider#openSelector
創(chuàng)建JDK NIO原生的Selector
。
SelectorProvider
會根據(jù)操作系統(tǒng)的不同選擇JDK在不同操作系統(tǒng)版本下的對應(yīng)Selector
的實現(xiàn)。Linux下會選擇Epoll
,Mac下會選擇Kqueue
。
下面我們就來看下SelectorProvider
是如何做到自動適配不同操作系統(tǒng)下IO多路復用
實現(xiàn)的
SelectorProvider
SelectorProvider
是在前面介紹的NioEventLoopGroup類
構(gòu)造函數(shù)中通過調(diào)用SelectorProvider.provider()
被加載,并通過NioEventLoopGroup#newChild
方法中的可變長參數(shù)Object... args
傳遞到NioEventLoop
中的private final SelectorProvider provider
字段中。
SelectorProvider的加載過程:
從SelectorProvider
加載源碼中我們可以看出,SelectorProvider
的加載方式有三種,優(yōu)先級如下:
通過系統(tǒng)變量
-D java.nio.channels.spi.SelectorProvider
指定SelectorProvider
的自定義實現(xiàn)類全限定名
。通過應(yīng)用程序類加載器(Application Classloader)
加載。
通過SPI
方式加載。在工程目錄META-INF/services
下定義名為java.nio.channels.spi.SelectorProvider
的SPI文件
,文件中第一個定義的SelectorProvider
實現(xiàn)類全限定名就會被加載。
如果以上兩種方式均未被定義,那么就采用SelectorProvider
系統(tǒng)默認實現(xiàn)sun.nio.ch.DefaultSelectorProvider
。筆者當前使用的操作系統(tǒng)是MacOS
,從源碼中我們可以看到自動適配了KQueue
實現(xiàn)。
不同操作系統(tǒng)中JDK對于
DefaultSelectorProvider
會有所不同,Linux內(nèi)核版本2.6以上對應(yīng)的Epoll
,Linux內(nèi)核版本2.6以下對應(yīng)的Poll
,MacOS對應(yīng)的是KQueue
。
下面我們接著回到io.netty.channel.nio.NioEventLoop#openSelector
的主線上來。
文章篇幅有限 下文繼續(xù)講解
原文作者:bin的技術(shù)小屋
