Netty服務端接收的新連接是如何綁定到worker線程池的?


 

更多技術分享可關注我

前言

原文:Netty服務端接收的新連接是如何綁定到worker線程池的?

前面分析Netty服務端檢測新連接的過程提到了NioServerSocketChannel讀完新連接后會循環調用服務端Channel綁定的pipeline.fireChannelRead()方法,將每條新連接打包當做參數傳入,然后通過這個方法將其沿着服務端Channel的pipeline傳遞下去,即在Channel的handler鏈條上流動,這部分細節后續會詳細分解。 

下面看下,新連接在服務端Channel的pipeline的流動過程中,Netty配置的boss線程池和worker線程池是如何配合的。

服務器的新連接接入器源碼分析

簡單回顧前面文章:Netty是如何處理新連接接入事件的?中分析了Netty服務端檢測新連接的過程,回憶NioMessageUnsafe類的read()方法源碼:

看最后的紅色方框,是在循環中將新連接順着Channel的pipeline傳遞下去,NioMessageUnsafe是前面說的Netty的Channel的內部接口——Unsafe的服務端的實現類。

那么這些新連接后續被傳遞時會發生什么呢?這也是重點問題——即Netty客戶端新連接的Channel被封裝后,如何與Netty的I/O線程關聯。下面看之前提到的新連接接入器,關聯的功能主要是這個接入器實現。

言歸正傳看ServerBootstrapAcceptor源碼,它是一個內部類,繼承了ChannelInboundHandlerAdapter(后面詳解Netty的pipeline機制)。

現在先復習一下服務端啟動流程。服務端啟動的核心操作是綁定端口,即在用戶代碼中serverBootstrap.bind(xx);方法中啟動,里面會調用ServerBootstrap的doBind方法,在doBind方法里調用了ServerBootstrap的initAndRegister()方法,這是一個初始化服務端Channel並注冊I/O多路復用器的方法,如下圖:

該方法通過反射創建了服務端的NioServerSocketChannel,並且創建保存了JDK的ServerSocketChannel以及一些組件,比如pipeline等,接着執行Channel的初始化操作——即ServerBootstrap的init(channel)方法(分析的是服務端代碼,故只看ServerBootstrap類對init的實現),init方法里就有新連接接入器的創建邏輯。如下紅框處,在init里配置服務端的pipeline時,默認添加了一個ServerBootstrapAcceptor handler:

先捋一捋完整過程:

1、首先ServerBootstrap的init方法為服務端Channel的pipeline添加了一個ChannelInitializer,在該類實現的void initChannel(Channel ch)方法里先將用戶代碼里配置的服務端的handler添加,前面我也說過,這個服務端的handler配置一般很少用到(即.handler() API),常用的主要是給客戶端配置handler,即.childHandler()

2、然后異步的添加一個新連接接入器——ServerBootstrapAcceptor,具體的,是把添加ServerBootstrapAcceptor到pipeline的操作封裝為了一個task,委托給服務端的NIO線程異步執行,等到有新連接到來時,該task已執行完畢。即Netty服務端Channel的pipeline最小結構如下:

這里提前接觸Netty的入站事件和出站事件的概念,所謂入站事件——即inbound事件,即Netty的NIO線程主動發起的,是面向用戶業務handler的操作,即都是被動發起的事件,通過fireXXX方法傳播。

比如Channel連接成功,Channel關閉,Channel有數據可讀,Channel上注冊I/O多路復用器成功,Channel解除I/O多路復用器的注冊,異常拋出等,這些都是被動執行的回調事件,它們的處理有專門的handler實現,統一叫入站handler。反之還有出站事件和出站handler,出站事件——即outbound事件,都是用戶線程或者用戶代碼主動發起的事件,如下是出站事件:

比如服務器主動綁定端口,主動關閉連接,客戶端主動連接服務器,服務器(客戶端)主動寫出消息等操作,這些事件的特點就是由用戶主動發起。針對這兩類事件,除了Netty默認提供的handler,用戶還可以自定義入站/出站handler以實現自己的攔截邏輯,這也是職責鏈(也叫責任鏈)模式的思想。

言歸正傳,繼續分析服務器讀取新連接的過程,現在分析的是新連接接入,故只看入站handler。先知道入站事件流動的順序是從pipeline的頭部節點開始,途徑各個入站handler節點,一直流動到尾部節點結束,這里就是Head->ServerBootstrapAcceptor->Tail。如下:

還得知道tail節點本質是一個入站handler,head節點本質是一個出站handler,后續會詳細拆解,這里不知道為什么也無所謂。

前面說到,NioMessageUnsafe類的read()方法,最后會將讀到的客戶端新連接傳遞出去,如下:

具體來說是觸發后續的各個入站handler的ChannelRead事件(前面說了ChannelRead是一個入站事件),入站事件都是從pipeline的頭部節點——HeadContext開始傳播的,而觸發這個事件傳播的正是pipeline.fireChannelRead(xxx)方法。

還記得服務端啟動的時候,如下有一段代碼:serverBootstrap.handler(new ServerHandler())serverBootstrap.childHandler(new ServerHandler());

當時給了這樣一個結論:.handler方法添加的handler是添加到服務端Channel的pipeline上,是在服務端初始化的時候就添加的,而.childHandler方法添加的handler是添加到客戶端Channel的pipeline上,是在處理新連接接入的時候添加的。現在知道原因了,ServerBootstrap調用init時,先pipeline.addLast(handler),然后添加一個ServerBootstrapAccepter,這樣服務端的pipeline也可能是head-hander>serverBootStrapAccepter>tail這種組成結構,如下(很熟悉的結構):

這里一定要明白,兩個操作是分別把handler加到了服務端和客戶端的pipeline。

serverBootStrapAccepter本身也是一個入站的handler。根據前面的分析,入站事件的傳播順序是head->用戶定義的入站handler->ServerBootstrapAcceptor->tail,我的demo里沒有為服務器定義handler,故直接調用到ServerBootstrapAcceptor的channelRead方法,該方法是接入器的重點,需要重點學習,ServerBootstrapAcceptor的channelRead方法源碼如下;

ServerBootstrapAcceptor是ServerBootstrap的一個內部類。下面看debug過程,一上來就把msg強轉為了Channel,即這里接收到的msg變量本質是剛剛讀取到的客戶端新連接——被Netty封裝為了其自定義的Channel。后續的ServerBootstrapAcceptor主要做了三件事:

1、黃色1處,就是前面分析的,在接入器里添加用戶配置的客戶端Channel的handler:即將用戶在服務器代碼里通過.childHandler()自定義的ChannelHandler添加到客戶端的pipeline,后續詳解。

2、黃色2處,設置用戶配置的options和attrs,主要是設置客戶端Channel的childOptions和childAttrs,childOptions是channel底層為TCP協議配置的屬性,childAttrs是channel本身的一些屬性,它的本質是個map,比如可以存儲當前channel存活時間,密鑰等。

3、黃色3處,選擇worker線程池的一根NIO線程,並將其綁定到該客戶端Channel——即代碼里的child變量。這步是異步操作,並通過register方法實現,這個方法復用了服務端啟動時為服務端Channel注冊I/O多路復用器的代碼邏輯。這最后一步又分為兩小步:

  • worker線程池通過EventLoop的線程選擇器——Chooser的next()方法選擇一個NioEventLoop線程和新連接綁定,和服務端線程池一樣的邏輯

  • 注冊客戶端的新Channel到這個NioEventLoop的I/O多路復用器,並為其注冊OP_READ事件

下面詳細分析這兩小步,我通過debug跟進register,來到了MultithreadEventLoopGroup的register方法,如下源碼:

最后進入到父類io/netty/util/concurrent/MultithreadEventExecutorGroup類,看到這里就很熟悉了,會進入到前面分析過的NioEventLoopGroup的線程選擇器。

這里使用的優化方法——通過位運算選擇一個NioEventLoop線程。如下發現idx是0,即workerGroup線程池里的線程此時才剛剛選擇第一個,因為這是我當前運行的服務器接收到的第一條客戶端連接,所以后續再來新連接時,會順次啟動后續的線程與之綁定,如果綁定到最后一根,那么idx會重新從0開始,循環往復。。。注意此時NIO線程還沒有啟動。Netty做了優化,前面也說了,Netty的線程池都是延遲啟動的。

在MultithreadEventLoopGroup類的register方法里選擇NioEventLoop線程后,next()方法會返回一個NioEventLoop實例,然后繼續調用該實例的register方法,即下一步過會跳轉到NioEventLoop直接父類SingleThreadEventLoop的register方法,如下源碼:

調用到了第二個register方法里,里面的channel()方法返回的就是客戶端的NioSocketChannel,unsafe()方法就是NioByteUnsafe實例,即最后調用了客戶端channel的Unsafe的register方法。即AbstractChannel的內部類——AbstractUnsafe的register方法,源碼如下:

看到這個方法的代碼就應該很熟悉了,我在前面Netty服務端啟動的時候分析過,即給客戶端新連接注冊I/O多路復用器的邏輯復用了這一套代碼,這也得益於Netty良好的架構設計。

下面再分析一下,執行AbstractUnsafe的register方法的邏輯:

1、首先對當前客戶端的I/O線程以及Channel做校驗,然后在黃色1處,判斷當前線程是不是NIO線程,顯然這里是false,因為雖然此時已經選擇了一個客戶端NIO線程,但是該NIO線程還沒有啟動,整個注冊邏輯還是運行在用戶線程下,我的demo是main線程,如下佐證,故1這里判斷失敗,接下來執行else里的代碼,將真正的注冊邏輯委托給剛剛啟動的客戶端的NIO線程異步執行,這樣做也能保證線程安全。

2、看黃色2處,即else代碼里,會通過NioEventLoop的execute方法啟動之前選擇的NIO線程(當然,如果已經啟動了,那么會略過啟動步驟),同時驅動注冊的這個task,這里才真正啟動NIO線程,也能佐證Netty的線程池實現了延遲啟動,

3、最后看黃色3處,我進入到這個register0方法,看它的實現源碼,如下:

最關鍵的方法是其中的doRegister()方法,看紅色方框處。我進入該方法,發現其實現在了子類AbstractNioChannel里。這就非常熟悉了,還是和服務端注冊ServerSocketChannel流程一樣,如下:

正是Netty封裝的JDK注冊Channel的Selector的邏輯。在該方法里將客戶端Channel注冊到客戶端NioEventLoop線程的I/O多路復用器,並將NioSocketChannel對象附加到JDK Channel,不過此時注冊的感興趣的I/O事件還是0,即什么都不關注,即該客戶端Channel還處於初始化狀態,真正注冊I/O事件還在后面流程里。

注意該方法將注冊邏輯寫在了一個死循環里,學會這種用法,目的是為了保證一個事情必須完成,即使出現某些異常。

回到register0方法,再看一遍,注冊完成后,會先觸發處於掛起狀態的handlerAdded事件,即先執行黃色1處的代碼,這里對應了為該客戶端新連接添加用戶自定義的客戶端handler的邏輯。然后才執行黃色2處,觸發並傳播當前Channel已經注冊成功的事件。如果當前Channel依然存活,那么會繼續執行3處的代碼,即為首次注冊的新Channel傳播Channel成功連接(處於活躍狀態)的事件。

最后,如果當前Channel不是第一次注冊,那么會判斷是否配置的自動讀消息(Netty默認都是讀優先),如果是,那么會執行黃色4處的代碼,后續詳解。

為新連接分配NIO線程和對新連接注冊I/O多路復用器的核心——是理解ServerBootstrapAcceptor,並由此知道服務端Channel的pipeline最小構成:Head->ServerBootstrapAcceptor->Tail

理解ServerBootstrapAcceptor:

1.延遲添加childHandler——將自定義ChannelHandler添加到新連接的pipeline,必須等當前Channel注冊I/O多路復用器完畢后,才會添加

2.設置options和attrs——設置childOptions和childAttrs

3.選擇NioEventLoop並注冊到Selector,核心是調用worker線程池的Chooser的next()方法選擇一個NioEventLoop,通過其doRegister()方法,將新連接注冊到worker線程綁定的Selector上。這里的新連接和Selector是多對一的關系。

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM