圖說Netty服務端啟動過程

作者逅弈


我們知道Netty是一個基於JDK的nio實現的網絡編程框架,那Netty的服務端是怎麼啟動的呢,包括他是何時register 的,何時 bind 端口的,以及何時開始讀取網絡中的數據的?

讓我們帶著這個疑問,通過一個官方的例子來深入探究Netty服務端的啟動過程。

PS:本文基於netty源碼的4.1分支進行分析。

首先我們拿一個最簡單的EchoServer的例子來舉例說明,具體的代碼如下:


圖說Netty服務端啟動過程


從上面的代碼來看,在啟動的過程中共有5處地方需要我們關注,不過最重要的啟動服務端的代碼,還是在最後第5步的時候。

為了更加清晰的描述整個啟動的過程,也便於我們更好的理解和記憶,我將使用多圖形少代碼的形式來表達。

首先我把啟動過程的一個大致流程畫成如下的圖:

圖說Netty服務端啟動過程

其中有以下幾個核心的方法:

  • channel()
  • handler()
  • childHandler()
  • doBind()

除此之外,還有一個初始化EventLoopGroup類的方法:

  • NioEventLoopGroup()

一、初始化EventLoopGroup

我們從最初的初始化 EventLoopGroup 類開始吧,從源碼中可以看到是一層一層的構造方法的調用,然後再super到了父類中,最終會調用到 AbstractEventExecutor 類,具體的調用流程如下圖所示:

圖說Netty服務端啟動過程

這個過程中創建了幾個重要的實例,我用淡藍色標記出來了。

首先我們需要知道的是,在Netty中有幾個比較重要的類:

  • EventLoop
  • EventLoopGroup
  • EventExecutor
  • EventExecutorGroup

他們之間的關係圖如下所示:

圖說Netty服務端啟動過程

EventLoop和EventExecutor說到底都是一種Executor。

然後通過調用ServerBootstrap的group()方法,我們將創建的EventLoopGroup對象分別賦值給了ServerBootstrap的 group 和 childGroup 屬性。

二、執行channel()方法

初始化完了EventLoopGroup之後,接著就開始執行 channel() 方法了,這個方法很簡單,就是通過 ReflectiveChannelFactory 類創建了一個 channelFactory ,這個 channelFactory 後面會很有用,都是通過它來創建需要的Channel實例的。這裡我就不貼具體的代碼了,具體的執行過程可以用下面的圖來表示:

圖說Netty服務端啟動過程

通過調用該方法,ServerBootstrap類的 channelFactory 屬性就被賦予了值,並且該ChannelFactory的實現類是通過反射來創建Channel的。

後面在需要創建Channel的時候,會調用該channelFactory的 newChannel() 方法,執行該方法之後,會創建三種非常有用的對象:

  • channel
  • pipeline
  • unsafe

三、執行handler()方法

該方法沒有創建其他的對象,只是把用戶提供的方法參數中所表示的ChannelHandler對象通過該方法來賦值給ServerBootstrap的 handler 屬性。

PS:這裡創建的handler在後面的初始化時會使用到

四、執行childHandler()方法

該方法沒有創建其他的對象,只是把用戶提供的方法參數中所表示的ChannelHandler對象通過該方法來賦值給ServerBootstrap的 childHandler 屬性。

PS:這裡創建的childHandler在後面的初始化時會使用到

五、執行doBind()方法

Netty啟動過程中最複雜,步驟最多的就是這個方法了,不過不用擔心,我已經把該方法核心的執行過程整理好了,如下圖所示:

圖說Netty服務端啟動過程

這裡我推薦大家在讀源碼的時候,可以拿一張紙,一支筆,用畫圖的形式把方法的調用過程,以及創建了哪些屬性等等這些都記下來,一開始可以不用知道那些方法和屬性具體是幹什麼的。先把整個調用流程理清楚,然後再一點一點細化,由點到面的擴展開來,最終把你那張圖豐富成一個完整的調用圖。

從圖中可以看的出來,doBind方法拆分成了兩個核心的方法:

  • initAndRegister()
  • doBind0()

第一個 initAndRegister 方法,從方法名字上就可以看得出來,它主要是執行某個init的過程,然後又執行了某個register的過程。

第二個 doBind0 方法,主要是執行了端口的綁定,然後創建了eventLoop不斷的執行JDK中的Selector.select()方法,從註冊到selector中的channel中選擇符合條件的channel。另外創建了一個task,用來從選中的channel中讀取數據,然後把讀取到的數據給到childHandler進行處理。

下面讓我們來深入到這兩個方法的執行過程中去,看看到底發生了什麼。

5.1 執行initAndRegister方法

initAndRegister方法的執行過程如下圖所示:

圖說Netty服務端啟動過程

initAndRegister方法做的事有兩件:init和register。在這之前首先通過channelFactory創建了一個channel。該方法是在初始化EventLoopGroup的時候出現的,可以回頭看一下,初始化的過程一共創建了三種對象:channel、unsafe、pipeline。

從該方法中慢慢的往下看,就可以看到,通過channelFactory創建了一個channel對象後,然後又拆分成了兩個部分,分別對channel進行了初始化,和對channel進行了register。其中register方法,最終會調用到JDK中最原始的register方法,即把一個channel註冊到一個selector中去。

  • init

初始化的過程主要是把用戶先前創建的handler和childHandler添加到pipeline中去。

  • register

註冊的過程主要是把該channel註冊到selector中去,這裡的channel就是用來接受客戶端連接的。

5.2 執行doBind0方法

doBind0方法的執行過程如下圖所示:

圖說Netty服務端啟動過程

doBind0做的事也很明確:bind、select以及runTask。

bind的過程最終是調用到JDK中原生的bind方法,其中在unsafe中執行bind的過程時,除了執行了具體的bind之外,還在NioEventLoop中啟動了一個線程,用來不斷的執行JDK中selector的select方法。然後讀取選中的channel中的數據,最後把讀取到的數據丟給childHandler去處理。

JDK的epoll空輪詢bug

我們知道JDK中的Selector會出現epoll空輪詢的bug,若Selector的輪詢結果為空,也沒有wakeup或新消息處理,則發生空輪詢,此時CPU使用率將達到100%。

Netty是通過重建Selector的方式修復該bug的,具體的做法是:

  • 對Selector的select操作週期進行統計,每完成一次空的select操作進行一次計數,
  • 若在某個週期內連續發生n(SELECTORAUTOREBUILD_THRESHOLD)次空輪詢,則觸發了epoll死循環bug。
  • 重建Selector,判斷是否是其他線程發起的重建請求,若不是則將原SocketChannel從舊的Selector上解除註冊,重新註冊到新的Selector上,並將原來的Selector關閉。

具體的代碼是在NioEventLoop中的select方法中執行的,代碼如下:


圖說Netty服務端啟動過程


圖說Netty服務端啟動過程

完整的啟動過程

通過上面的分析,我們最後來總結一下,Netty服務端在啟動的時候做了以下的事情:

  • 1.創建了EventLoopGroup、NioEventLoop的實例,並且創建了一個selector
  • 2.創建了一個channelHandler用來在未來實例化Channel
    • 創建Channel的過程中會一併創建pipeline和unsafe
  • 3.設置了ServerBootstrap的handler和childHandler屬性,用以在接收到數據後進行業務邏輯的處理
  • 4.通過channelFactory創建了channel實例,並對其進行了初始化和註冊到selector上
  • 5.通過Unsafe調用JDK的bind方法將服務綁定到了端口上,並通過EventLoop創建了一個線程來循環執行以下任務
    • 5.1.執行selector的select方法,並通過計數的方式,滿足一定條件的情況下對selector進行重建,以解決JDK的epoll空輪詢的bug
    • 5.2.對選中的channel執行讀操作,並將讀取到的數據丟給childHandler進行處理

一個完整的Netty服務端啟動過程如下圖所示:

圖說Netty服務端啟動過程



分享到:


相關文章: