作者:星巴刻
? ? ? ?
一、Netty 內(nèi)核組
? ? ? ? Netty 運(yùn)行時(shí)包含了多個(gè)內(nèi)核。在服務(wù)端程序中,需要分別創(chuàng)建 parent 和 child 兩種內(nèi)核: 1 個(gè) parent 內(nèi)核和 16 個(gè) child 內(nèi)核( 8 核 CPU系統(tǒng)下的默認(rèn)數(shù))。為簡(jiǎn)便起見(jiàn),以下簡(jiǎn)單以 16 來(lái)代替實(shí)際的 child 內(nèi)核數(shù)。因此,用如下大圖來(lái)概括 Netty 內(nèi)核組:中間是 17 個(gè)內(nèi)核組成的內(nèi)核組,左邊是操作系統(tǒng),右邊是應(yīng)用程序:

? ? ? ? 每一個(gè) Channel 都必須屬于且僅屬于某一個(gè)內(nèi)核。系統(tǒng)中代表服務(wù)端偵聽(tīng)端口的 NioServerSocketChannel 屬于 parent 。代表不同客戶端連接的 NioSocketChannel 屬于 16 個(gè)child 中的一個(gè)。當(dāng) Channel 創(chuàng)建后,Netty 需要安排一個(gè)內(nèi)核來(lái)負(fù)責(zé)它,這個(gè)過(guò)程稱為 register (注冊(cè))。注冊(cè)過(guò)程就是調(diào)用 NioEventLoopGroup.next() 方法返回一個(gè) NioEventLoop,調(diào)用 NioEventLoop 的 register(channel)? 方法完成。
二、端口偵聽(tīng)內(nèi)核

1、初始化
? ? ? ? 端口偵聽(tīng)內(nèi)核的創(chuàng)建與初始化屬于服務(wù)端整體初始化的一部分。這一部分可通過(guò) ServerBootstrap 完成。對(duì)照上圖,可以清晰地看出,初始化工作主要要構(gòu)建如下對(duì)象,并把它們「串在一起」:
1. 創(chuàng)建一個(gè) NioServerSocketChannel? ,用于表示用于接收接受客戶端連接的端口
2. 打開(kāi)一個(gè) Selector,用于發(fā)現(xiàn) NioServerSocketChannel 上有新的客戶端連接
3. 創(chuàng)建一個(gè) NioEventLoop 線程以及內(nèi)部隊(duì)列,讓執(zhí)行端口綁定、客戶端連接到來(lái)等程序在這個(gè)線程執(zhí)行
4. 創(chuàng)建一個(gè)? ServerBootstrapAcceptor? ,接受新來(lái)的客戶端連接,并初始化后續(xù)處理它的對(duì)象。
? ? ? ? 應(yīng)用程序創(chuàng)建 NioEventLoopGroup 時(shí),NioEventLoopGroup 內(nèi)部將根據(jù)指定的參數(shù)自動(dòng)創(chuàng)建 NioEventLoop 實(shí)例。NioEventLoop 隨之把其內(nèi)部線程、隊(duì)列創(chuàng)建起來(lái),并把打開(kāi)一個(gè)新的 Selector 選擇器。這樣,內(nèi)核線程、內(nèi)部隊(duì)列、Selector 選擇器就天然地屬于這個(gè) NioEventLoop 了。
? ? ? ? 應(yīng)用程序調(diào)用 ServerBootstrap.bind() 方法時(shí),ServerBootstrap 將創(chuàng)建 NioServerSocketChannel 對(duì)象及其? ServerBootstrapAcceptor 實(shí)例放入 NioServerSocketChannel 的 pipeline 中去。隨后將創(chuàng)建的 NioServerSocketChannel 注冊(cè)到 NioEventLoop 中,由 NioEventLoop 在其內(nèi)部線程中執(zhí)行將 NioServerSocketChannel 注冊(cè)到 Selector 選擇器的代碼:

? ? ? ? 至此,端口偵聽(tīng)內(nèi)核的所有對(duì)象都創(chuàng)建完畢,內(nèi)部對(duì)象已經(jīng)關(guān)聯(lián)起來(lái)只差把內(nèi)核綁定到操作系統(tǒng)中。
2、注冊(cè) OP_ACCEPT
? ? ? ? 偵聽(tīng)內(nèi)核為了能夠感知有新的客戶端到來(lái),必須注冊(cè)對(duì) OP_ACCEPT 事件的興趣,這個(gè)工作在上面的初始化中完成,這里單獨(dú)列出來(lái)說(shuō)明。在 NioServerSocketChannel 注冊(cè)到內(nèi)核工作完成后, DefaultPipeline.channelActive 方法除了通知 channel 已經(jīng)打開(kāi),緊接著馬上調(diào)用 channel.read() ,在 Netty 中,channel.read 不是真正要去從系統(tǒng)緩沖區(qū)讀取信息,而是表示要注冊(cè)一個(gè)讀取事件。因此,channel.read() 的調(diào)用通過(guò) pipeline 后,最終將調(diào)用到 channel 自身的 doBeginRead() 方法,將? selectionKey 的 interestOps 屬性增加 OP_ACCEPT 值。相關(guān)源代碼如下:


3、端口綁定
? ? ? ? 應(yīng)用程序調(diào)用 ServerBootstrap.bind() 完成相關(guān)的初始化工作后,最后就是將整個(gè)內(nèi)核和操作系統(tǒng)關(guān)聯(lián)起來(lái),也就是真正將 NioServerSocketChannel 綁定到指定的端口上。類(lèi)似 register,將 NioServerSocketChannel bind 到操作系統(tǒng)上,需要調(diào)用 Java Nio 的 ServerSocketChannel 的 bind 方法,這個(gè)工作在 Netty 內(nèi)核下,也將在 NioEventLoop 內(nèi)部線程來(lái)實(shí)際執(zhí)行:


? ? ? ? 至此,Netty 已經(jīng)可以接收客戶端連接了。
4、接受連接
? ? ? ? 對(duì)照《端口偵聽(tīng)內(nèi)核圖》,當(dāng)有新的客戶端連接到來(lái)時(shí),NioEventLoop 調(diào)用選擇器選擇當(dāng)前發(fā)生的 I/O 事件時(shí),將得到含有 OP_ACCEPT 事件的 selectionKey。NioEventLoop 的 processSelectedKey 方法一一處理這些 I/O 事件,對(duì)于 OP_ACCEPT 事件, NioServerSocketChannel 的 doReadMessages 方法將封裝出一個(gè) NioSocketChannel:

? ? ? ? 這個(gè) NioSocketChannel 對(duì)象將被之前初始化時(shí)創(chuàng)建到 pipeline 中的 ServerBootstrapAcceptor 獲得,在里面將新的客戶端連接安排到某個(gè) child 內(nèi)核實(shí)例中:

? ? ? ? 至此,就可以進(jìn)行客戶端連接的讀寫(xiě)了。
三、連接的讀寫(xiě)
? ? ? ? 客戶端和服務(wù)端之間連接上的信息讀寫(xiě)以及處理,在 Netty 中使用如下統(tǒng)一的內(nèi)核來(lái)完成。在服務(wù)端程序中,由于多了一個(gè)偵聽(tīng)端口的組,此內(nèi)核在服務(wù)端中歸為 child 組;但在客戶端中,就只有這個(gè)組,此時(shí)它歸為客戶端中的 parent 組。這樣的分組稍微拗口,我們完全可以簡(jiǎn)單地直接稱為它「端口讀寫(xiě)內(nèi)核」,以區(qū)別服務(wù)端程序特有的「端口偵聽(tīng)內(nèi)核」。

? ? ? ? 如前所述,一般地,服務(wù)端程序中會(huì)有 16 個(gè)連接讀寫(xiě)內(nèi)核,典型的客戶端通常只有 1 個(gè)。這主要是因?yàn)?,客戶端往往只和服?wù)端建立 1 個(gè)或少數(shù)幾個(gè)連接,而服務(wù)端則要同時(shí)維護(hù)數(shù)量龐大的客戶端連接。好在,1 個(gè)或多個(gè),對(duì) Netty 來(lái)說(shuō)其內(nèi)核架構(gòu)是統(tǒng)一的,我們可以統(tǒng)一來(lái)理解,不用分開(kāi)看。
? ? ? ? 端口讀寫(xiě)內(nèi)核中,一個(gè)內(nèi)核負(fù)責(zé)多個(gè) NioSocketChannel 連接,這些連接注冊(cè)到選擇器中,以便通過(guò)選擇器發(fā)現(xiàn)該 channel 的 I/O事件,其中 OP_READ 是最關(guān)鍵的 I/O 事件。NioEventLoop 的內(nèi)部線程調(diào)用選擇器進(jìn)行選擇,當(dāng)注冊(cè)到選擇器中的 NioSocketChannel 有新的 OP_READ 等 I/O 事件時(shí),完成底層操作后(比如將信息讀入 ByteBuf),NioEventLoop 將調(diào)用和該 channel 一一對(duì)應(yīng)的 ChannelPipeline 中的 ChannelInboundHandler 的 channelRead 等方法進(jìn)行處理,最終使得最右邊的應(yīng)用程序邏輯得到執(zhí)行。
? ? ? ? 每個(gè) NioSocketChannel 都有自己的 ChannelPipeline 對(duì)象。對(duì)照上面的內(nèi)核圖中 pipeline 的部分,左邊是它的 head,右邊是它的 tail。每個(gè) ChannelPipeline 可以簡(jiǎn)單地看做有 2 行,上面行是處理來(lái)自內(nèi)核發(fā)出的事件(簡(jiǎn)稱處理 InboundEvent ),底下行處理來(lái)自應(yīng)用程序發(fā)出的動(dòng)作(簡(jiǎn)稱處理 OutboundEvent )。每一行都可以包含不限制個(gè)數(shù)的 ChannelHandler 模塊。
? ? ? Netty 內(nèi)核是在其內(nèi)核線程中調(diào)用 ChannelPipleline 的方法提交處理 InboundEvent 或 OutboundEvent,但并不意味著 ChannelPipeline 中的 ChannelHandler 的 channelRead 等方法一定是在 Netty 的內(nèi)核線程中執(zhí)行的。這主要 bootstrap 中,ChannelInitializer.initChannel 方法中是如何調(diào)用 pipeline 的,以調(diào)用 addLast 為例子,如果調(diào)用的是 addLast(EventExecutorGroup, ChannelHandler...handlers),即在第 1 個(gè)參數(shù)指定了一個(gè) EventExecutorGroup,那么 handlers 中的方法將由這個(gè) EventExecutorGroup 提供的一個(gè) EventExecutor 執(zhí)行,并且之后這個(gè) handlers 的執(zhí)行一直都由這個(gè) EventExecutor 執(zhí)行,不再在 Netty 的內(nèi)核線程了!這個(gè)特性的使用需要精心去了解、適時(shí)使用,它對(duì)性能有重大幫助或影響。
四、總結(jié)
? ? ? ? 雖然 Netty 為網(wǎng)絡(luò)開(kāi)發(fā)提供了高性能的能力,以及簡(jiǎn)便的開(kāi)發(fā)框架和各種開(kāi)箱套件。但要寫(xiě)出良好的 Netty 程序,花點(diǎn)時(shí)間看下 Netty 的要點(diǎn)還是值得的。如果只是模模糊糊地使用 Netty 也總能被坑。
? ? ? ? 本文是市面上 第一個(gè)提出 Netty 內(nèi)核 概念的文章,希望借此有助于理解 Netty 的核心要點(diǎn)。Netty 的要點(diǎn)在其內(nèi)核體現(xiàn)了 Reactor 編碼架構(gòu),并根據(jù)實(shí)際需要進(jìn)行了擴(kuò)展。
? ? ? ? 以服務(wù)端程序?yàn)槔粋€(gè)應(yīng)用程序會(huì)包含一個(gè) 端口偵聽(tīng)內(nèi)核 以及 16 個(gè)連接讀寫(xiě)內(nèi)核(8 核 CPU下的默認(rèn)設(shè)置)。這些內(nèi)核具有同構(gòu)性。內(nèi)核包含一個(gè)內(nèi)部線程和隊(duì)列。代表服務(wù)端端口的 NioServerSocketChannel 或者代表客戶端的 NioSocketChannel 必須選擇注冊(cè)到某一個(gè)內(nèi)核中,內(nèi)核通過(guò)選擇器發(fā)現(xiàn)新的 I/O 事件的到來(lái),進(jìn)行初步加工,然后交給各自 channel 對(duì)應(yīng)的生產(chǎn)線去 pipeline 處理。應(yīng)用程序也會(huì)主動(dòng)發(fā)起一些工作,這些被稱為 Outbound 事件,比如往 channel 寫(xiě)入信息或者關(guān)閉 channel。這些 Outbound 事件也會(huì)由經(jīng)過(guò) pipleline ,最終再進(jìn)入內(nèi)核處理。
? ? ? ? ChannelPipeline 處理 Inbound 由內(nèi)核發(fā)起,處理 Outbound 事件由應(yīng)用程序發(fā)起,但是 pipeline 中的每個(gè)處理器在處理事件時(shí),都可以事先通過(guò) EventExecutorGroup 獲得的一個(gè) EventExecutor 執(zhí)行。對(duì)于那些耗時(shí)的工作,比如調(diào)用數(shù)據(jù)庫(kù)、遠(yuǎn)程服務(wù)的處理模塊,設(shè)置一個(gè)獨(dú)立于內(nèi)核線程的 EventExecutorGroup 是有絕對(duì)必要的。
2017-11-22