大家好,我是威哥,《RocketMQ技術內幕》一書作者,榮獲RocketMQ官方社區優秀布道師、CSDN2020博客執之星Top2等榮譽稱號,目前擔任中通快遞技術平臺部資深架構師,主要負責全鏈路壓測、訊息中間件、資料同步等產品的研發與落地,擁有千億級訊息集群的運維經驗,不僅實踐經驗豐富,而且對其源代碼有深入且系統的研究,歡迎大家關注我,一起抱團發展,
1、主從多Reactor模型
不知道大家有沒有發現面向網路編程的框架都非常突出執行緒模型,其實也不難理解,因為網路編程是IO密集型,合理使用執行緒對提高性能有著非常顯著的作用,
在網路編程模型中,有一個非常經典的執行緒模型:主從多Reactor模型,其示意圖如下所示:

我們首先簡單介紹一下上圖中涉及的幾個重要角色:
-
Acceptor
請求接收者,在實踐時其職責類似服務器,并不真正負責連接請求的建立,而只將其請求委托 Main Reactor 執行緒池來實作,起到一個轉發的作用,
-
Main Reactor
主 Reactor 執行緒組,主要負責連接事件,并將IO讀寫請求轉發到 SubReactor 執行緒池,當然在一些需要對客戶端進行權限控制等場景下,權限校驗的職責可以放到 Main Reactor 執行緒池,即 Main Reactor 也可以注冊通道的讀寫事件,讀取客戶端權限校驗相關的資料包,執行權限驗證,權限驗證通過后再將2通道注冊到IO執行緒, -
Sub Reactor
Main Reactor 通常監聽客戶端連接后會將通道的讀寫轉發到 Sub Reactor 執行緒池中一個執行緒(負載均衡),負責資料的讀寫,在 NIO 中 通常注冊通道的讀(OP_READ)、寫事件(OP_WRITE),
上面的圖非常經典,除了介紹了執行緒模型相關知識外,其實還羅列出了網路通信的核心步驟:
- 編碼/解碼(通信協議)
- 認證授權
- 網路讀、寫
- 業務邏輯處理執行緒池
上述的執行緒模型非常經典,那我們能否基于NIO,來模擬實作上面的執行緒模型呢?
2、基于NIO實作主從多Reactor模型
我聽一個技術圈大佬對我說過這樣一句話:NIO是一名程式員進階的第一個門檻,也是必需跨過的門檻,
筆者將嘗試用NIO實作Reactor模型,成為NIO界最強的"Hello Wold",
為了接下來的代碼進行展示與講解,首先先羅列出其核心類圖:

各個類的職責說明情況如下:
-
Acceptor
服務端接收器,主要系結埠,將ServerSocketChannel轉發到MainReactor,及主從多Reactor模型中的主Reactor中,如果服務端系結多個埠,則會創建多個ServerSocketChannel,但通常情況只會系結到一個埠,
-
MainReactor
主Reactor,主要承擔接受客戶端的連接請求,即主要負責OP_ACCEPT事件的處理,并將創建的連接(SocketChannel)轉發到從Reactor執行緒組,由其處理讀寫事件, -
SubReactorThreadGroup
從Reactor執行緒組,維護多個Reactor執行緒,并提供負載均衡, -
SubReactorThread
從Reactor,主要負責IO讀寫,俗稱IO執行緒,
在上原始碼之前我們再來看一下整個示例到時序圖:

核心的關鍵點如下:
- Acceptor主要是創建ServerSocketChannel,然后轉發給MainReactor
- MainReactor中維護一個NIO Selector事件選擇器,注冊OP_ACCEPT事件,每處理一個OP_ACCEPT,表示一個新的客戶端連接,在服務端會創建一個SocketChannel物件,接著將該物件轉發到從Reactor,
- SubReactorThreadGroup
主從多Reactor模型中的從Reactor組,包含多個從Reactor執行緒,新的連接會進行負載均衡,選擇其中一個Reactor來處理特定的SocketChannel, - SubReactorThread
從Reactor執行緒,會維護一個Nio Selector,會將SocketChannel注冊到該事件選擇器,處理讀、寫請求,一個Selector中會包含多個Channel(SocketChannel),但一個SocketChannel在其生命周期中只會注冊到一個Selector,這樣可以簡化處理模型,
程式員希望不服就干,接下來我將展示基于NIO實作多Reactor的代碼,應該是全網目前第一個直接用代碼實作,
溫馨提示:為了便于排版,本文中的代碼統一用截圖進行展示,代碼獲取方式:私信回復 reactorcode 即可獲取,
2.1 Acceptor代碼實作
Acceptor代碼實作如下:

Acceptor代碼實作比較簡單,就是利用NIO提供的API創建ServerSocketChannel,并設定為非阻塞模式,并呼叫bind方法系結埠,然后轉發到MainReactor中,
2.2 MainReactor代碼實作
MainReactor代碼實作如下圖所示:

代碼實作要點:
- 創建Selector選擇器物件,并注冊OP_ACCEPT事件,用于監聽客戶端的鏈接
- 創建從Reactor執行緒組
- NIO事件選擇經典使用方法,通過回圈呼叫selector的select方法,此方法會回傳當前就緒的事件,例如讀事件就緒,表明此時呼叫該通道的網路讀API,一定會回傳有效資料
- 如果當前事件是OP_ACCEPT,則呼叫ServerSocketChannel的accept方法,創建一個SocketChannel物件,并轉發到從Reactor執行緒組中,
2.3 SubReactorThreadGroup代碼實作
SubReactorThreadGroup的角色是充當從Reactor執行緒組,其代碼實作如下圖所示:

主要的關鍵點如下:
- 內部持有一個業務執行緒池,用于執行業務邏輯,可類比Dubbo中的用于執行服務提供這各個Service的業務代碼
- 通過next()進行負載均衡,選擇一個從Reactor,
- 這里使用了NioTask,主要是因為Selector雖然是執行緒安全的,但其內部持有的Key Set是執行緒不安全的,故這里為保證Selector的現執行緒安全性,selector的任何方法只會在從Reactor執行緒中執行,這里只是添加到任務佇列中,最侄訓由從Reactor執行緒去執行,
2.4 SubReactorThread代碼實作
該類是從Reactor到核心邏輯,接下來將詳細介紹其實作,

主要是提供增加IO讀寫任務(NioTask)添加到任務佇列中,并在執行業務邏輯時轉發到業務執行緒池,
接下來詳細介紹SubReactorThread到run方法實作,重點關注讀寫事件的處理流程,

Step1:NIO事件處理的核心要點:
-
事件選擇器的常用套路,呼叫select進行事件就緒選擇,然后遍歷就緒集合,
-
如果事件型別為寫事件,直接將資料通過呼叫通道的write方法將資料寫入到網路,這里的核心關鍵點:
呼叫SocketChannel的write方法會回傳本次寫入的位元組數,如果等于0,并且資料還未全部寫入,則說明寫入快取區已滿,需要再次注冊寫事件,
-
如果事件型別為讀事件,呼叫SocketChannel的read方法將從網路中讀取資料,這里有幾個側重點需要說明:
- 一次read讀取有可能并沒有完整奪取一個請求,該方法可以呼叫多次,當回傳0時可以停止繼續讀取,
- read方法將資料讀取到ByteBuffer(快取區),如何從快取區中讀取一個完整的請求是關鍵,因為該快取區中可能包含一個請求,也可以包含多個,這里的解決方案就是:編碼、解碼,關于這部分內容,大家可以參考文章:https://mp.weixin.qq.com/s/GDyG7wWLU5YijKvhFwljXg
-
經過解碼,從請求流中解碼一個請求后,將請求轉發到業務執行緒池,

處理完事件選擇相關的任務后,開始執行應用程式的任務,例如讀事件注冊、寫操作,這里的關鍵點是呼叫SocketChannel的write方法會回傳本次寫入的位元組數,如果等于0,并且資料還未全部寫入,則說明寫入快取區已滿,需要再次注冊寫事件,否則應該取消寫事件,
2.5 代碼獲取
代碼不利于排版,故本文中的代碼全部使用截圖,原始碼的獲取方式:私信回復:reactorcode 即可獲取,
好了,本文就介紹到這里了,一鍵三連(關注、點贊、留言)是對我最大的鼓勵,
掌握一到兩門java主流中間件,是敲開BAT等大廠必備的技能,送給大家一個Java中間件學習路線,助力大家早日進入互聯網大廠,
Java進階之梯,成長路線與學習資料,助力突破中間件領域
最后分享筆者一個硬核的Netty電子書,您將獲得通過提問方式學習Netty電子書,

獲取方式:私信回復nettypdf即可獲取,
個人網站:https://www.codingw.net
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/289351.html
標籤:其他
上一篇:漫畫:世界的本源是什么?
