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

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

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

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


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


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

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

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

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