原來 Netty 的核心啟動邏輯是這樣的!
你好,我是yes。
上篇我們已經(jīng)了解了 Netty 的啟動流程,還剩一個 bind 方法沒有細講,這篇我們就著重的說下 bind 方法,這個方法也是觸發(fā) Netty 真正啟動的方法。
先打個預(yù)防針,源碼也不是那么簡單的,有時候看著有點繞,如果你想面試的時候胸有成竹,還是得有點耐心的,如果中間沒看懂沒事,最后我有總結(jié),看完總結(jié)之后應(yīng)該會清晰的。
對了,如果有條件的話,建議在電腦上看這篇文章,會更加舒適。
好了,開局先來一張圖,bind 的核心操作就如下圖所示,下面長篇的源碼分析也是為了說清楚這個流程,所以什么類名,方法名都不重要,重要的是知曉整體流程:
注意,上圖的 Channel 指的是 ServerChannel。
bind 的很多方法都是異步執(zhí)行的,所以有些流程是主線程執(zhí)行,有些是 eventloop 執(zhí)行的,這個需要注意下,不然會感覺有點亂。
先看看這張圖大體有個印象,然后我們開始盤 bind 方法~
bind 流程
從 bind 方法進入,實際上就會調(diào)用父類 AbstractBootstrap#doBind。
我們來簡單的看下 doBind 這個方法,要點我都用文字標明了:
可以看到,這個方法主要做了下面這四件事:
- 創(chuàng)建 channel
- 初始化 channel
- 綁定 channel 至 group 內(nèi)的某個 eventLoop 上
- 綁定端口
下面我會逐一的分析。
創(chuàng)建 channel
先來看下 initAndRegister 方法。
從下面的源碼可以看到,這個方法主要就是創(chuàng)建一個 Channel 對象,然后初始化 Channel,最后將它注冊到 eventLoop 上。
channelFactory.newChannel() 是利用反射得到一個 Channel 對象。還記得我們構(gòu)建 ServerBootstrap 時候設(shè)置的 channel 類型嗎:.channel(NioServerSocketChannel.class)
通過傳入的這個 class 就可以得到構(gòu)造器,然后調(diào)用 newInstance 即可得到一個對象。
這樣就創(chuàng)建了一個 NioServerSocketChannel 對象。
通過繼承鏈,我們可以發(fā)現(xiàn) NioServerSocketChannel 會調(diào)用父類 AbstractChannel 的構(gòu)造器,而在這里就會創(chuàng)建三個重要的東西:
- id,channel 的標識
- unsafe,用來操作底層數(shù)據(jù)讀寫
- pipeline,handler的編排
所以,從這里可以得知,創(chuàng)建一個 Channel 配套就會新建一個 pipeline,即每個 Channel 都有自己的 pipeline。
初始化 channel
創(chuàng)建完 Channel 的操作之后,緊接著就初始化 Channel ,即 init() 方法。
可以看到,初始化首先就是把之前在 ServerBootstrap 時配置的 option 和 attr 塞到已經(jīng)創(chuàng)建的 ServerSocketChannel 中,這個很好理解。
然后往 ServerSocketChannel 的 pipeline 中塞入一個 ChannelInitializer。
那 ChannelInitializer 是個什么玩意?它其實是一個抽象類,且是一個入站的 handler。
不過它是一個特殊的 ChannelHandler ,從 ChannelInitializer 類的注釋就知道:
它的使命就是簡化注冊完畢后的初始化操作,可以理解為是一個中轉(zhuǎn)的 handler,一個工具類。它在完成初始化動作之后會從 pipeline 中被移除。
所以,利用 ChannelInitializer 將初始化邏輯封裝起來,當 channel 注冊到 eventLoop 中之后會觸發(fā)事件,然后就會調(diào)用 ChannelInitializer#initChannel 來執(zhí)行初始化,僅此而已。
我們可以看到,上面 initChannel 邏輯是先添加之前配置給 ServerSocketChannel 的 handler,在我們的 helloworld 示例中就是添加 LoggingHandler。
然后再異步添加一個 ServerBootstrapAcceptor 這個 handler,從名字來看,這個 handler 主要是接收處理新連接。
小貼士:此時的 initChannel 邏輯還未執(zhí)行,是要等到后面事件觸發(fā)了才會執(zhí)行,且執(zhí)行的線程是 eventLoop 線程。
好了,看到這肯定有人會有疑問,為什么 initChannel 里面要異步添加 ServerBootstrapAcceptor?
為什么要異步添加 ServerBootstrapAcceptor?不直接添加?
其實源碼注釋說明的很清楚(上面為了清晰結(jié)構(gòu),把注釋都刪了)
簡單翻譯下,就是用戶可能會用 ChannelInitializer 來設(shè)置 ServerSocketChannel 的 handler ,注意是ServerSocketChannel 的 handler,不是那個 childHandler 哈。
來看下示例代碼:
- // 這樣是沒問題的,不用異步添加 ServerBootstrapAcceptor
- ServerBootstrap sb = new ServerBootstrap();
- sb.channel(...).group(...).childHandler(...).handler(ourHandler);
- //這樣的就需要異步添加 ServerBootstrapAcceptor
- ServerBootstrap sb = new ServerBootstrap();
- sb.channel(...).group(...).childHandler(...).handler(
- new ChannelInitializer<Channel>() {
- @Override
- protected void initChannel(Channel ch) throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- pipeline.addLast(ourHandler);
- }
- }
- );
因為在利用 ChannelInitializer 設(shè)置 handler 的情況下,initChannel(…)方法只會在該方法(init 內(nèi)添加ServerBootstrapAcceptor的方法)返回后被調(diào)用。
因此,需要確保以延遲的方式添加,使得用戶定義的 handler 都放在 ServerBootstrapAcceptor 前面。
簡單地說,就是讓 ServerBootstrapAcceptor 成為 ServerSocketChannel 的 pipeline 中最后一個 inboundHandler,這樣用戶定義的 handler 邏輯才會被調(diào)用到。
因為當事件傳遞到 ServerBootstrapAcceptor 過就不會再繼續(xù)通過 pipeline 傳遞了,會將接待的子 channel 直接分配給 workerGroup了,如果用戶自定義的 handler 在 ServerBootstrapAcceptor 后面的話,里面的邏輯是不會被執(zhí)行的,等于白加。
不理解的可以多讀幾遍上面的話哈,有一點點小繞。
都說到這了,那就來看看 ServerBootstrapAcceptor 的內(nèi)部邏輯吧。
ServerBootstrapAcceptor 的內(nèi)部邏輯
很簡單,就是根據(jù) selector 得到新連接對應(yīng)的 channel(子channel),然后為其配置之前(初始化ServerBootstrap時)設(shè)置的 childhandler、childoption、childattr,緊接著從 workerGroup 中選擇一個 eventLoop ,將 channel 注冊到這個 eventLoop 上:
這樣,新建的子 channel 之后的所有事件(讀、寫等 I/O 事件),都由從 workerGroup 中選定的那個 eventLoop 負責。
至此,我們講完了 init(channel) 的操作。
channel 注冊至 eventLoop
創(chuàng)建且初始化完 channel 之后,就需要把已經(jīng)準備完畢的 channel 注冊到一個 eventLoop 中。
即上面的ChannelFuture regFuture = config().group().register(channel);(從返回值可以得知這是一個異步執(zhí)行流程)
這個動作就是從 bossGroup 中選一個 EventLoop ,然后將 channel 注冊到選定的 EventLoop 上。
這個 next() 實際就是調(diào)用我們之前提到的 chooser 來選擇一個 eventLoop,最終會將此 eventLoop 傳遞到 AbstractUnsafe#register 中,執(zhí)行注冊邏輯,核心就在這個 register0 方法。
可以看到,無論如何都是由 eventLoop 線程來執(zhí)行 register0 操作(所以對主線程而言,這是異步的)。
我們來看下 register0 都做了什么事:
- 調(diào)用底層接口,將 channel 注冊到 selector 上
- 觸發(fā)之前配置的 ChannelInitializer#initChannel
- 異步添加綁定端口的任務(wù)到 eventLoop 中
- 觸發(fā) Registered 事件,使之在 pipeline 上傳遞
我們先看第一步 doRegister,看看具體是怎么注冊 Channel 至 Selector 上的。
因為我們都知道 ServerSocketChannel 是 Netty 定義的類,和 JDK 沒任何關(guān)系,那如何與 JDK 的類適配呢?到底是如何注冊到 JDK 的 Selector 上的呢?
看到我圈起來的地方?jīng)],實際會把之前創(chuàng)建的 JDK 的 Channel 注冊到 Selector 上,而 Netty 的 Channel 會作為一個 attachment 綁定到 JDK 的 Channel 上。
這樣一來,每次 Selector 的時候,如對應(yīng)的 JDK Channel 有事件發(fā)生,則 Netty 都可以從返回的 JDK Channel 的 attachment 中獲取自己的 Channel,然后觸發(fā)后續(xù)的邏輯,這招叫移花接木。
然后再來看第二步 pipeline.invokeHandlerAddedIfNeeded()。
此時才會調(diào)用 ChannelInitializer#initChannel 來添加 handler,且異步提交了一個添加 ServerBootstrapAcceptor 的任務(wù),隨后將 ChannelInitializer 從 pipeline 中移除。
緊接著就是觸發(fā) Registered 事件,這個事件會隨著 pipeline 傳播至所有 handler,只要是入站的 handler,且實現(xiàn)了下面這個方法就會被調(diào)用,所以如果你想在注冊完畢之后做一些邏輯處理,那么就可以創(chuàng)建一個 handler 且實現(xiàn) channelRegistered 方法。
好了,至此我們終于把 Channel 的創(chuàng)建、初始化、注冊給說完了,后面就是綁定的端口的操作了。
綁定端口
綁定端口主要有兩個事情需要關(guān)注下,一個是調(diào)用底層 JDK 綁定端口的實現(xiàn),二是綁定完之后觸發(fā) active 事件,表明 channel 一切都已就緒。
底層 JDK Channel 的 bind 方法,我就不說了,這個觸發(fā) active 事件比較關(guān)鍵,最終會觸發(fā) AbstractNioChannel#doBeginRead,注冊 accept 事件,這樣 ServerSocketChannel 感興趣事件也注冊完畢,可以干活了!
好了,現(xiàn)在我們再來看下這張圖,來對整體的過程捋一捋,至于上面的那些代碼理不清沒事,你只要看懂下面這種圖,知曉大體的流程就夠了:
現(xiàn)在看這張圖是不是有不一樣的感覺?其實Netty 服務(wù)端的啟動流程也就這么回事兒,我再用語言組織一下:
- 創(chuàng)建 ServerSocketChannel(配套指定一個ID、底層操作 unsafe對象、pipeline)
- 將配置的option、attr設(shè)置在創(chuàng)建的 ServerSocketChannel 上
- 添加一個 ChannelInitializer 至 ServerSocketChannel 的 pipeline 中
- 從 bossGroup 中選擇一個 eventLoop,將 ServerSocketChannel 與之綁定,且之后生命周期內(nèi)的操作都由這個 eventLoop 執(zhí)行
- 觸發(fā)第三步添加的 ChannelInitializer 的 initChannel 方法,將用戶配置的 handler 添加到 ServerSocketChannel 的 pipeline 中,且由于需要保證框架內(nèi)置的 ServerBootstrapAcceptor 這個 handler 處于 inboundhandler 的最后一個,因此是異步添加。
- 觸發(fā) registered 事件,使之在 pipeline 上傳播
- 調(diào)用 JDK 底層方法,綁定端口
- 觸發(fā) active 事件,注冊 ServerSocketChannel 感興趣的事件(OP_ACCEPT),用于接收新連接
- over
最后
好了,我相信通過最后一張圖和最后一段話,你應(yīng)該大致了解了整個 Netty 的啟動流程。
如果不明白的話再看看圖,最后是在電腦上看哈,看著代碼應(yīng)該不難理解的,重點就是分清上面的 Channel 指的是 Server 端的 Channel,然后主線程和 eventLoop 線程的執(zhí)行邏輯,有條件的建議自己打斷點試試。
下篇我們將談?wù)?Netty 的 Reactor 模型,這玩意面試被問的幾率好像挺大,如果你還不清楚,沒事,下篇我們慢慢盤!
我是 yes,從一點點到億點點,我們下篇見~