Netty是一款非常優(yōu)秀的網(wǎng)絡(luò)編程框架,是對NIO的二次封裝,本文將重點剖析Netty服務(wù)端的啟動流程,深入底層了解如何使用NIO編程服務(wù)端。
本文是筆者基于問題的啟發(fā)式源碼閱讀技巧的展示,建議帶著如下問題開始本文的閱讀:
ServerBootstrap 的 option 與 childOption 分別有什么作用
服務(wù)端IO通道如何綁定事件鏈。
ServerBootstrap 的 handler 方法與 childHandler 方法的區(qū)別又是什么?
childHandler中的方法在服務(wù)端bind方法時會被調(diào)用嗎?
1、Netty服務(wù)端啟動示例
基于Netty的使用示例如下:
代碼@1:創(chuàng)建主從多Reactor線程模型的Boss線程組,通常只需要設(shè)置一個線程,用于監(jiān)聽客戶端的連接請求(OP_ACCEPT)。
代碼@2:創(chuàng)建主從多Reactor線程模型的Work線程組,即IO線程組,默認為CPU核數(shù)的兩倍。
代碼@3:創(chuàng)建Netty服務(wù)端啟動工具類ServerBootstrap。
代碼@4:調(diào)用group方法設(shè)置主從線程組。
代碼@5:設(shè)置通道的類型,服務(wù)端NIO通道類型 NioServerSocketChannel。
代碼@6:通過option方法為通道服務(wù)端通道選項。
代碼@7:通過chiildOption方法為IO通道設(shè)置選項。
代碼@8:通過ChannelInitializer添加自定義的ChannelHandler,通常包括編碼解碼器、業(yè)務(wù)Handler。
代碼@9:調(diào)用管道的addLast添加自定義編碼解碼器。
代碼@10:調(diào)用bind方法綁定到服務(wù)端指定接口,綁定完成后則在指點端口上監(jiān)聽客戶端的連接。
服務(wù)端的核心流程入口為bind方法,接下來我們將詳細分析其實現(xiàn)原理,繼續(xù)體會NIO編程技巧。
2、Netty服務(wù)端啟動流程
通過跟蹤其bind方法,最終將進入到AbstractBootstrap的doBind方法。
其關(guān)鍵實現(xiàn)點:
代碼@1:通過調(diào)用initAndRegister方法完成底層網(wǎng)絡(luò)初始化與通道注冊工作。
代碼@2:如果初始化與注冊工作已完成,則直接調(diào)用doBind0方法完成綁定操作。
代碼@3:如果初始化與注冊工作未完成,則通過regFuture(注冊憑證)中添加監(jiān)聽器,等注冊完成后再執(zhí)行doBind0方法。
技巧提示:基于Future異步編程,在主線程中通過調(diào)用future.isDone方法判斷異步方法是否已完成,如果未完成,通過在該憑證上添加監(jiān)聽器(事件回調(diào)),操作完成后執(zhí)行回調(diào)邏輯。
從上面的方法來看服務(wù)端的綁定流程包含初始化與綁定兩個子流程,接下來將分別深入探討。
2.1 通道初始化
基于NIO編程,需要先創(chuàng)建通道,然后將其注冊到事件選擇器,這個過程由 AbstractBootstrap 的 initAndRegister 方法實現(xiàn)。
實現(xiàn)的關(guān)鍵點如下:
代碼@1:創(chuàng)建NIO服務(wù)端通道實現(xiàn)類NioServerSocketChannel的實例。
代碼@2:調(diào)用 init 方法初始化通道。
代碼@3:將通道注冊到事件輪詢器EventLoopGroup
代碼@4::如果通道已注冊,但發(fā)生了錯誤則調(diào)用通道close方法回收相關(guān)資源,如果未注冊成功,則強制清除通道占用的資源,特別是文件占用符。
關(guān)于通道的注冊邏輯已經(jīng)在手把手教你如何編寫一個NIO客戶端 中已詳細介紹,故接下來重點關(guān)注一下服務(wù)端通道的注冊流程。
2.1.1 服務(wù)端通道初始化流程
AbstractBootstrap 的 init 方法是一個抽象方法,具體有其子類實現(xiàn):
服務(wù)端通道的初始化代碼由ServerBootstrap的init方法。
Step1:首先將通過ServerBootstrap設(shè)置的選項與附加選項初始化到通道中。
Step2:init 方法的關(guān)鍵點:將 handler 方法設(shè)置的事件鏈,同時新增 ServerBootstrapAcceptor 事件處理方法加入到 NioServerSocketChannel 的事件鏈,但并沒有把 childHandler 中添加的事件鏈添加到NioServerSocketChannel。
讀者朋友們,請停下來思考一下,為什么會這樣?從現(xiàn)在可以肯定的是 handler 方法定義的事件處理方法將在與 NioServerSocketChannel 相關(guān)的事件發(fā)生時其作用。
要解開這個謎題,我們有必要來看看 ServerBootstrapAcceptor 是如何工作的。
2.1.2 ServerBootstrapAcceptor 詳解
ServerBootstrapAcceptor 類圖如下所示:
ServerBootstrapAcceptor方法只實現(xiàn)了inbound事件的channelRead事件。在詳細探究它之前先看看屬性:
EventLoopGroup childGroup
事件執(zhí)行器組,ServerBootstrap設(shè)置的從Reactor線程組,即Work線程組。ChannelHandler childHandler
ServerBootstrap#childHandler 設(shè)置的事件處理器,也就是用戶定義的事件處理器。
接下來探究其 channelRead 方法的實現(xiàn)邏輯:
Step1:channelRead竟然傳入的是一個Channel,那這個Channel對象是NioSocketChannel嗎?
是的,原來當 OP_ACCEPT 事件觸發(fā)后,Server端會通過調(diào)用ServerSocketChannel 的 accept()方法,將返回一個 NioSocketChannel,讀寫操作的載體,在NIO中負責數(shù)據(jù)的讀寫。
Step2:將通過 childHandler 定義的事件處理器綁定到 NioSocketChannel。
最終完成 NioServerSocketChannel 與 NioSocketChannel 的初始化與事件綁定。
關(guān)于 NioSocketChannel 詳細的初始化流程蘊含在 ChannelInitializer,其機制已經(jīng)在 手把手教你如何編寫一個NIO客戶端 中詳細介紹。
2.2 NIO綁定機制
在通道完成初始化與注冊后,服務(wù)端需要進行端口綁定,由 AbstractBootstrap 的 doBind0 方法實現(xiàn)。
bind 的核心實現(xiàn)最終是調(diào)用 Channel 的 bind 方法,最終由 AbstractChannel 類實現(xiàn):
bind事件將傳播,根據(jù)Netty事件傳播機制,bind 屬于 ChannelOutbound事件,最終將調(diào)用 HeadContext的bind方法,最終將調(diào)用Unsafe的bind方法,更加具體是調(diào)用 AbstractChannel的內(nèi)部類AbstractUnsafe的bind方法,其代碼如下所示:
doBind 方法是一個抽象方法,NIO服務(wù)端的實現(xiàn):NioServerSocketChannel。
即最終通過調(diào)用NIO底層NioServerSocketChannel 的 bind 方法完成服務(wù)端通道的綁定操作,即實現(xiàn)服務(wù)端在特定端口監(jiān)聽客戶客戶端的連接請求。