您當前的位置:首頁 > 收藏

啟程,從IO出發...

作者:由 毛奇志 發表于 收藏時間:2020-12-05

一、前言

在學習Reactor模式之前,我們需要對“I/O的四種模型”以及“I/O多路複用”進行簡單的介紹(即本文第二部分),因為

Reactor本質上是一個使用了同步非阻塞的I/O多路複用機制的模式

金手指:Reactor本質上是一個使用了同步非阻塞的I/O多路複用機制的模式。

二、前奏:I/O的四種模型與I/O多路複用

2。1 I/O的四種模型

要搞懂I/O四種模型,先看懂阻塞與非阻塞、同步與非同步。

首先,任何一個 I/O 操作底層主要分成兩部分:

① 準備資料,將資料載入到核心快取;

② 複製資料,將核心快取中的資料載入到使用者快取。

堵塞、非堵塞的區別是在於第一階段,即資料準備階段。無論是堵塞還是非堵塞,都是用應用向核心發出請求,而read資料的過程是‘堵塞’的,直到資料讀取完。

同步、非同步的區別在於第二階段,即資料複製階段。若由請求者主動的去獲取資料,則為同步操作;若資料的read都由kernel核心完成了,這就是非同步操作。

好了,現在正是來看I/O的四種模型:同步阻塞IO模型、同步非阻塞IO模型、非同步阻塞IO模型、非同步非阻塞IO模型,如下:

第一種,Synchronous blocking I/O 同步阻塞IO

啟程,從IO出發...

第二種,Synchronous non-blocking I/O 同步非阻塞IO

啟程,從IO出發...

第三種,Asynchronous blocking I/0 非同步阻塞IO

啟程,從IO出發...

第四種,Asynchronous non-blocking I/0 非同步非阻塞IO

啟程,從IO出發...

看懂了四個圖之後,附加兩個問題,如下:

問題1:為什麼請求者主要獲取資料是同步操作?

回答1:因為read/write操作是‘堵塞’的,直到資料讀取完。

問題2:為什麼資料read都由核心完成是非同步操作?

回答2:因為在核心read資料的過程中,應用程序依舊可以執行其他的任務。

2。2 I/O多路複用

I/O 複用機制一定離不開事件分發器(Reactor模型中的Reactor/Dispatcher,Netty中的)。 事件分發器的作用,就是將那些讀寫事件源分發給各讀寫事件的處理者。

涉及到事件分發器的兩種模式稱為:Reactor和Proactor。 Reactor模式是基於同步I/O的,而Proactor模式是和非同步I/O相關的。因為本文介紹的就是 Reactor模式相關的知識,所以我們這裡看基於同步I/O的多路複用,如下圖:

啟程,從IO出發...

上面這就是經典的每連線對應一個執行緒的同步阻塞I/O模式。

流程:

① 伺服器端的Server是一個執行緒,執行緒中執行一個死迴圈來阻塞的監聽客戶端的連線請求和通訊。

② 當客戶端向伺服器端傳送一個連線請求後,伺服器端的Server會接受客戶端的請求,ServerSocket。accept()從阻塞中返回,得到一個與客戶端連線相對於的Socket。

③ 構建一個handler,將Socket傳入該handler。建立一個執行緒並啟動該執行緒,線上程中執行handler,這樣與客戶端的所有的通訊以及資料處理都在該執行緒中執行。當該客戶端和伺服器端完成通訊關閉連線後,執行緒就會被銷燬。

④ 然後Server繼續執行accept()操作等待新的連線請求。

優點:

① 使用簡單,容易程式設計

② 在多核系統下,能夠充分利用了多核CPU的資源。即當I/O阻塞系統,但CPU空閒的時候,可以利用多執行緒使用CPU資源。

缺點:

① 執行緒生命週期的開銷非常高。該模式的本質問題在於嚴重依賴執行緒,但執行緒Java虛擬機器非常寶貴的資源。隨著客戶端併發訪問量的急劇增加,執行緒數量的不斷膨脹將伺服器端的效能將急劇下降。執行緒的建立與銷燬並不是沒有代價的。在Linux這樣的作業系統中,執行緒本質上就是一個程序,建立和銷燬都是重量級的系統函式。

② 資源消耗。記憶體:大量空閒的執行緒會佔用許多記憶體,給垃圾回收器帶來壓力。;CPU:如果你已經擁有足夠多的執行緒使所有CPU保持忙碌狀態,那麼再建立更過的執行緒反而會降低效能。

③ 穩定性。在可建立執行緒的數量上存在一個限制。這個限制值將隨著平臺的不同而不同,並且受多個因素制約:a)JVM的啟動引數、b)Threa的建構函式中請求的棧大小、c)底層作業系統對執行緒的限制 等。如果破壞了這些限制,那麼很可能丟擲OutOfMemoryError異常。

④ 執行緒的切換成本是很高的。作業系統發生執行緒切換的時候,需要保留執行緒的上下文,然後執行系統呼叫。如果執行緒數過高,不僅會帶來許多無用的上下文切換,還可能導致執行執行緒切換的時間甚至會大於執行緒執行的時間,這時候帶來的表現往往是系統負載偏高、CPU sy(系統CPU)使用率特別高,導致系統幾乎陷入不可用的狀態。

⑤ 容易造成鋸齒狀的系統負載。一旦執行緒數量高但外部網路環境不是很穩定,就很容易造成大量請求的結果同時返回,啟用大量阻塞執行緒從而使系統負載壓力過大。

⑥ 若是長連線的情況下並且客戶端與伺服器端互動並不頻繁的,那麼客戶端和伺服器端的連線會一直保留著,對應的執行緒也就一直存在在,但因為不頻繁的通訊,導致大量執行緒在大量時間內都處於空置狀態。

適用場景:如果你有少量的連線使用非常高的頻寬,一次傳送大量的資料,也許典型的IO伺服器實現可能非常契合。

三、目標:Reactor模式

3。1 從NIO到Reactor模式

定義:Reactor模式(反應器模式)是一種處理一個或多個客戶端併發交付服務請求的事件設計模式。當請求抵達後,服務處理程式使用I/O多路複用策略,然後同步地派發這些請求至相關的請求處理程式。

我們知道,Java中有三種IO,分別是BIO、NIO、AIO:

BIO是同步阻塞,裡使用者最關心“我要讀”;

NIO是同步非阻塞,裡使用者最關心“我可以讀了”;

AIO是非同步非阻塞,模型裡使用者更需要關注的是“讀完了”。

要講Reactor,就要看NIO,因為Reactor本質上是一個使用了同步非阻塞的I/O多路複用機制的模式。

NIO一個重要的特點是:socket主要的讀、寫、註冊和接收函式,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但效能非常高)。NIO是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎。

問題1:NIO相對於BIO的效能最佳化?

回答1:BIO對於每一個連線都需要開闢一個執行緒,而NIO對BIO最佳化,對於N個連線只需要M個執行緒,M遠遠小於N。

問題2:NIO如何實現對於BIO的效能最佳化?

回答2:三元件 Selector Channel Buffer。

問題3:為什麼Reactor要使用NIO?

回答3:

第一,複製資料雖然需要消耗CPU但效能非常高:NIO是同步非阻塞IO,非阻塞指的時候準備資料的時候應用程式不用阻塞,但是複製資料的時候應用程式是需要阻塞的,複製資料雖然需要消耗CPU但效能非常高;

第二,NIO是I/O多路複用的基礎:Reactor模式(又稱反應器模式)是一種處理一個或多個客戶端併發交付服務請求的事件設計模式。當請求抵達後,服務處理程式使用I/O多路複用策略,然後同步地派發這些請求至相關的請求處理程式。

3。2 Reactor五個角色

啟程,從IO出發...

上圖流程解釋,Reactor模式的角色構成(Reactor模式一共有5中角色構成):

Handle:譯為控制代碼或描述符,在Windows下稱為控制代碼,在Linux下稱為描述符;

Synchronous Event Demultiplexer:譯為同步事件分離器,地位等同於Selector;

Event Handler:事件處理器,地位等同於Handler;

Concrete Event Handler:具體事件處理器,地位等同於Handler;

Initiation Dispatcher:初始分發器,地位等同於Reactor。

概括來看,核心類是Initiation Dispatcher ,同步事件分離器notifies通知Handler處於ready狀態,用作分發(register_handler(h) remove_hadler(h) handler_events() 先select()阻塞,透過阻塞後,按照型別 for迴圈呼叫h。handler_event(type) )。其中,同步事件分離器中有一個select()方法,將同步事件分離器物件注入到初始分發器裡面,這樣初始分發器的handler_events() 就呼叫select()阻塞。

3。2。1 Handle

Handle譯為控制代碼或描述符,在Windows下稱為控制代碼,在Linux下稱為描述符:

(1)本質上表示一種資源(比如說檔案描述符,或是針對網路程式設計中的socket描述符),是由作業系統提供的,在Windows下稱為控制代碼,在Linux下稱為描述符;

(2)該資源用於表示一個個的事件,事件既可以來自於外部,也可以來自於內部;外部事件比如說客戶端的連線請求,客戶端傳送過來的資料等;內部事件比如說作業系統產生的定時事件等。

(3)它本質上就是一個檔案描述符,Handle是事件產生的發源地。

3。2。2 Synchronous Event Demultiplexer(同步事件分離器):Selector

它本身是一個系統呼叫,用於等待事件的發生(事件可能是一個,也可能是多個)。呼叫方在呼叫它的時候會被阻塞,一直阻塞到同步事件分離器上有事件產生為止。對於Linux來說,同步事件分離器指的就是常用的I/O多路複用機制,比如說select、poll、epoll等。在Java NIO領域中,同步事件分離器對應的元件就是Selector;對應的阻塞方法就是select方法。

金手指:在Java NIO領域中,同步事件分離器對應的元件就是Selector;對應的阻塞方法就是select方法(select()方法)。

3。2。3 Event Handler(事件處理器):Handler

本身由多個回撥方法構成,這些回撥方法構成了與應用相關的對於某個事件的反饋機制。在Java NIO領域中並沒有提供事件處理器機制讓我們呼叫或去進行回撥,是由我們自己編寫程式碼完成的。Netty相比於Java NIO來說,在事件處理器這個角色上進行了一個升級,它為我們開發者提供了大量的回撥方法,供我們在特定事件產生時實現相應的回撥方法進行業務邏輯的處理,即ChannelHandler。ChannelHandler中的方法對應的都是一個個事件的回撥。

3。2。4 Concrete Event Handler(具體事件處理器):Handler

是事件處理器的實現。它本身實現了事件處理器所提供的各種回撥方法,從而實現了特定於業務的邏輯。它本質上就是我們所編寫的一個個的處理器實現。

3。2。5 Initiation Dispatcher(初始分發器):Reactor

實際上就是Reactor角色。它本身定義了一些規範,這些規範用於控制事件的排程方式,同時又提供了應用進行事件處理器的註冊、刪除等設施。它本身是整個事件處理器的核心所在,Initiation Dispatcher會透過Synchronous Event Demultiplexer來等待事件的發生。一旦事件發生,Initiation Dispatcher首先會分離出每一個事件,然後呼叫事件處理器,最後呼叫相關的回撥方法來處理這些事件。Netty中ChannelHandler裡的一個個回撥方法都是由bossGroup或workGroup中的某個EventLoop來呼叫的。

定義了一些規範,這些規範用於控制事件的排程方式,同時又提供了應用進行事件處理器的註冊、刪除等設施(register_handler(h) remove_handler(h))

3。3 Reactor模式流程(Reactor Acceptor Handler)

① 初始化Initiation Dispatcher(初始化初始分發器),然後將若干個Concrete Event Handler註冊到Initiation Dispatcher中(透過呼叫初始分發器的register_handler(h)方法)。當應用向Initiation Dispatcher註冊Concrete Event Handler時,會在註冊的同時指定感興趣的事件(金手指:就是手寫reactor的時候的accept – read -write 三種類型),即,應用會標識出該事件處理器希望Initiation Dispatcher在某些事件發生時向其發出通知,事件透過Handle來標識,而Concrete Event Handler又持有該Handle。這樣,事件 ——> Handle ——> Concrete Event Handler 就關聯起來了。

② Initiation Dispatcher 會要求每個事件處理器向其傳遞內部的Handle。該Handle向作業系統標識了事件處理器。

③ 當所有的Concrete Event Handler都註冊完畢後,應用會呼叫handle_events方法來啟動Initiation Dispatcher的事件迴圈(金手指:應用程式呼叫初始分發器中的handle_events()放棄啟動初始分發器中的事件迴圈,阻塞在select()方法的地方)。這是,Initiation Dispatcher會將每個註冊的Concrete Event Handler的Handle合併起來,並使用Synchronous Event Demultiplexer(同步事件分離器)同步阻塞的等待事件的發生。

比如說,TCP協議層會使用select同步事件分離器操作來等待客戶端傳送的資料到達連線的socket handler上。

比如,在Java中透過Selector的select()方法來實現這個同步阻塞等待事件發生的操作。

在Linux作業系統下,select()的實現中

a)會將已經註冊到Initiation Dispatcher的事件呼叫epollCtl(epfd, opcode, fd, events)註冊到linux系統中,這裡fd表示Handle,events表示我們所感興趣的Handle的事件;

b)透過呼叫epollWait方法同步阻塞的等待已經註冊的事件的發生。不同事件源上的事件可能同時發生,一旦有事件被觸發了,epollWait方法就會返回;

c)最後透過發生的事件找到相關聯的SelectorKeyImpl物件,並設定其發生的事件為就緒狀態,然後將SelectorKeyImpl放入selectedSet中。這樣一來我們就可以透過Selector。selectedKeys()方法得到事件就緒的SelectorKeyImpl集合了。

④ 當與某個事件源對應的Handle變為ready狀態時(比如說,TCP socket變為等待讀狀態時),Synchronous Event Demultiplexer就會通知Initiation Dispatcher。(就是上圖中的同步事件分離器到handler的notifies)

⑤ 接第四步,Initiation Dispatcher收到通知後,會觸發這個Handler對應的事件處理器的回撥方法,從而響應這個處於ready狀態的Handle。當事件發生時,Initiation Dispatcher會將被事件源啟用的Handle作為『key』來尋找並分發恰當的事件處理器回撥方法。

⑥ 接上面,呼叫對應事件的回撥方法,Initiation Dispatcher會回撥事件處理器的handle_event(type)回撥方法來執行特定於應用的功能(開發者自己所編寫的功能),從而相應這個事件。所發生的事件型別可以作為該方法引數並被該方法內部使用來執行額外的特定於服務的分離與分發。(金手指:就是在初始分發器中的handle_events()方法的for迴圈的中,呼叫h。handle_event(type)方法,這是事件處理器/具體事件處理器的方法)

四、進階:Reactor三種實現模式

4。1 Reactor單執行緒模式

4。1。1 Reactor單執行緒模式架構

啟程,從IO出發...

上圖理解:Reactor單執行緒模型包括三個元件:Dispacher、Accepter、Handler,其中,Reactor/Dispacher進行請求分發,連線請求到Acceptor地方,讀寫請求一共五個步驟:讀取 解碼 計算 編碼 傳送。

4。1。2 Reactor單執行緒模式流程

流程:

① 伺服器端的Reactor是一個執行緒物件,該執行緒會啟動事件迴圈,並使用Selector來實現IO的多路複用。註冊一個Acceptor事件處理器到Reactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣Reactor會監聽客戶端向伺服器端發起的連線請求事件(ACCEPT事件)。

② 客戶端向伺服器端發起一個連線請求,Reactor監聽到了該ACCEPT事件的發生並將該ACCEPT事件派發給相應的Acceptor處理器來進行處理。Acceptor處理器透過accept()方法得到與這個客戶端對應的連線(SocketChannel)(對應程式碼:SocketChannel socketChannel = serverSocketChannel。accept(); ),然後將該連線所關注的READ事件以及對應的READ事件處理器註冊到Reactor中(對應程式碼: SelectionKey selectionKey = socketChannel。register(selector, SelectionKey。OP_READ);),這樣一來Reactor就會監聽該連線的READ事件了。或者當你需要向客戶端傳送資料時,就向Reactor註冊該連線的WRITE事件和其處理器。

③ 當Reactor監聽到有讀或者寫事件發生時,將相關的事件派發給對應的處理器進行處理。比如,讀處理器會透過SocketChannel的read()方法讀取資料,此時read()操作可以直接讀取到資料,而不會堵塞與等待可讀的資料到來。

④ 每當處理完所有就緒的感興趣的I/O事件後,Reactor執行緒會再次執行select()阻塞等待新的事件就緒並將其分派給對應處理器進行處理。

注意,Reactor的單執行緒模式的單執行緒主要是針對於I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一個執行緒上完成的。但在目前的單執行緒Reactor模式中,不僅I/O操作在該Reactor執行緒上,連非I/O的業務操作也在該執行緒上進行處理了(所以我們要將非IO放到worker執行緒組裡面,且看Reactor執行緒池模型),這可能會大大延遲I/O請求的響應。所以我們應該將非I/O的業務邏輯操作從Reactor執行緒上解除安裝,以此來加速Reactor執行緒對I/O請求的響應。

4。2 Reactor執行緒池模式

4。2。1 Reactor執行緒池模式架構

啟程,從IO出發...

上圖理解:Reactor單執行緒模型包括三個元件:Dispacher、Accepter、Handler,其中,Reactor/Dispacher進行請求分發,連線請求到Acceptor地方,讀寫請求一共五個步驟:讀取 解碼 計算 編碼 傳送。

此外,添加了一個工作者worker執行緒池,並將非I/O操作(即解碼、計算、編碼)從Reactor執行緒中移出轉交給工作者執行緒池來執行。Reactor執行緒只需要accept read write 三個操作,其他的解碼 計算 編碼交給新建的工作者執行緒池執行,提高Reactor執行緒的I/O響應,不至於因為一些耗時的業務邏輯而延遲對後面I/O請求的處理(耗時操作都到工作者執行緒池中去了)

與單執行緒Reactor模式不同的是,添加了一個工作者執行緒池,並將非I/O操作從Reactor執行緒中移出轉交給工作者執行緒池來執行。這樣能夠提高Reactor執行緒的I/O響應,不至於因為一些耗時的業務邏輯而延遲對後面I/O請求的處理。

4。2。2 worker執行緒池處理非IO操作的優點

使用執行緒池的優勢(新建工作者執行緒池的優勢):

① 透過重用現有的執行緒而不是建立新執行緒,可以在處理多個請求時分攤線上程建立和銷燬過程產生的巨大開銷。(N個請求只需要M個執行緒,M遠遠小於N)

② 另一個額外的好處是,當請求到達時,工作執行緒通常已經存在,因此不會由於等待建立執行緒而延遲任務的執行,從而提高了響應性。

③ 透過適當調整執行緒池的大小,可以建立足夠多的執行緒以便使處理器保持忙碌狀態。同時還可以防止過多執行緒相互競爭資源而使應用程式耗盡記憶體或失敗。

注意,在上圖的改進的版本中,所以的I/O操作依舊由一個Reactor來完成,包括I/O的accept()、read()、write()以及connect()操作。

4。2。3 worker執行緒池處理非IO操作的侷限

對於一些小容量應用場景,可以使用單執行緒模型。但是對於高負載、大併發或大資料量的應用場景卻不合適,主要原因如下:

① 一個NIO執行緒(就是Reactor)同時處理成百上千的鏈路,效能上無法支撐,即便NIO執行緒的CPU負荷達到100%,也無法滿足海量訊息的讀取和傳送;

② 當NIO執行緒負載過重之後,處理速度將變慢,這會導致大量客戶端連線超時,超時之後往往會進行重發,這更加重了NIO執行緒的負載,最終會導致大量訊息積壓和處理超時,成為系統的效能瓶頸;

4。3 Reactor主從執行緒池模式

4。3。1 Reactor主從執行緒池模式架構

啟程,從IO出發...

上圖理解:Reactor單執行緒模型包括三個元件:Dispacher、Accepter、Handler,其中,Reactor/Dispacher進行請求分發,連線請求到Acceptor地方,讀寫請求一共五個步驟:讀取 解碼 計算 編碼 傳送。

創新點1

:從一個Reactor變成了兩個,mainReactor和subReactor,mainReactor用來分發accept請求,對於已經accept的請求,交個subReactor,讓subReactor來分發read write請求,mainReactor可以只有一個,但subReactor一般會有多個。mainReactor執行緒主要負責接收客戶端的連線請求,然後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通訊。

創新點2

:添加了一個工作者worker執行緒池,並將非I/O操作(即解碼、計算、編碼)從Reactor執行緒中移出轉交給工作者執行緒池來執行。Reactor執行緒只需要accept、read、write 三個操作,其他的解碼、計算、編碼交給新建的工作者執行緒池執行,提高Reactor執行緒的I/O響應,不至於因為一些耗時的業務邏輯而延遲對後面I/O請求的處理(耗時操作都到工作者執行緒池中去了)。

Reactor執行緒池中的每一Reactor執行緒都會有自己的Selector、執行緒和分發的事件迴圈邏輯。

mainReactor可以只有一個,但subReactor一般會有多個。mainReactor執行緒主要負責接收客戶端的連線請求,然後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通訊。

4。3。2 Reactor主從多執行緒模式執行流程

① 註冊一個Acceptor事件處理器到mainReactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣mainReactor會監聽客戶端向伺服器端發起的連線請求事件(ACCEPT事件)。啟動mainReactor的事件迴圈EventLoop。

② 客戶端向伺服器端發起一個連線請求,mainReactor監聽到了該ACCEPT事件並將該ACCEPT事件派發給Acceptor處理器來進行處理。Acceptor處理器透過accept()方法得到與這個客戶端對應的連線(SocketChannel,SocketChannel表示一個客戶端連線),然後將這個SocketChannel傳遞給subReactor執行緒池。

③ subReactor執行緒池分配一個subReactor執行緒給這個SocketChannel,即,將SocketChannel關注的READ事件以及對應的READ事件處理器註冊到subReactor執行緒中。當然你也註冊WRITE事件以及WRITE事件處理器到subReactor執行緒中以完成I/O寫操作。subReactor執行緒池中的每一subReactor執行緒都會有自己的Selector、執行緒和分發的迴圈邏輯。

④ 當有I/O事件就緒時,相關的subReactor就將事件派發給響應的處理器處理。注意,這裡subReactor執行緒只負責完成I/O的read()操作,在讀取到資料後將業務邏輯的處理放入到執行緒池中完成,若完成業務邏輯後需要返回資料給客戶端,則相關的I/O的write操作還是會被提交回subReactor執行緒來完成。

金手指:所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依舊還是在Reactor執行緒(mainReactor執行緒或 subReactor執行緒)中完成的。Thread Pool(執行緒池)僅用來處理非I/O操作的邏輯。

多Reactor執行緒模式將“接受客戶端的連線請求”和“與該客戶端的通訊”分在了兩個Reactor執行緒來完成——mainReactor accept,subReactor read/write。mainReactor完成接收客戶端連線請求的操作,它不負責與客戶端的通訊,而是將建立好的連線轉交給subReactor執行緒來完成與客戶端的通訊,這樣一來就不會因為read()資料量太大而導致後面的客戶端連線請求得不到即時處理的情況。並且多Reactor執行緒模式在海量的客戶端併發請求的情況下,還可以透過實現subReactor執行緒池來將海量的連線分發給多個subReactor執行緒,在多核的作業系統中這能大大提升應用的負載和吞吐量。

五、提升:從Reactor到Netty

5。1 Netty與Reactor對應關係

Netty的執行緒模式就是一個實現了Reactor模式的經典模式。

結構對應:

啟程,從IO出發...

模式對應(Netty服務端使用了“多Reactor執行緒模式”):

啟程,從IO出發...

5。2 Netty執行流程

① 當伺服器程式啟動時,會配置ChannelPipeline,ChannelPipeline中是一個ChannelHandler鏈,所有的事件發生時都會觸發Channelhandler中的某個方法,這個事件會在ChannelPipeline中的ChannelHandler鏈裡傳播。

然後,從bossGroup事件迴圈池中獲取一個NioEventLoop來現實服務端程式繫結本地埠的操作,將對應的ServerSocketChannel註冊到該NioEventLoop中的Selector上,並註冊ACCEPT事件為ServerSocketChannel所感興趣的事件。

② NioEventLoop事件迴圈啟動,此時開始監聽客戶端的連線請求。

③ 當有客戶端向伺服器端發起連線請求時,NioEventLoop的事件迴圈監聽到該ACCEPT事件,Netty底層會接收這個連線,透過accept()方法得到與這個客戶端的連線(SocketChannel),然後觸發ChannelRead事件(即,ChannelHandler中的channelRead方法會得到回撥),該事件會在ChannelPipeline中的ChannelHandler鏈中執行、傳播。

④ ServerBootstrapAcceptor的readChannel方法會該SocketChannel(客戶端的連線)註冊到workerGroup(NioEventLoopGroup) 中的某個NioEventLoop的Selector上,並註冊READ事件為SocketChannel所感興趣的事件。啟動SocketChannel所在NioEventLoop的事件迴圈,接下來就可以開始客戶端和伺服器端的通訊了。

六、面試金手指

金手指:

Reactor是一個使用了同步非阻塞的I/O多路複用機制的模式。

6。1 阻塞、非阻塞、同步、非同步

金手指:IO操作的兩個階段

I/0 操作 主要分成兩部分

① 資料準備,將資料載入到核心快取

② 將核心快取中的資料載入到使用者快取

金手指:阻塞、非阻塞、同步、非同步

堵塞、非堵塞的區別是在於第一階段,即資料準備階段。無論是堵塞還是非堵塞,都是用應用向核心發出請求

同步、非同步的區別在於第二階段,若由請求者主動的去獲取資料,則為同步操作;若資料的read都由kernel核心完成了,這就是非同步操作。

問題1:為什麼是請求者主要獲取資料是同步?

回答1:因為read/write操作是‘堵塞’的,直到資料讀取完。

問題2:為什麼資料read都由核心完成,是非同步操作?

回答2:因為在核心read資料的過程中,應用程序依舊可以執行其他的任務。

金手指:Linux五種IO 與 Java三種IO 對應關係

Java三種IO中,

BIO是同步阻塞,對應Linux中的同步阻塞IO,使用者最關心“我要讀”;

NIO是同步非阻塞,對於Linux中的訊號驅動機制IO, 使用者最關心“我可以讀了”;

AIO是非同步非阻塞,對應Linux中的非同步IO,使用者更需要關注的是“讀完了”。

6。2 總算搞懂NIO相對於BIO的效能優勢

問題1:NIO相對於BIO的效能最佳化?

回答1:BIO對於每一個連線都需要開闢一個執行緒,NIO對BIO最佳化,對於N個連線只需要M個執行緒,M遠遠小於N。

問題2:NIO如何實現對於BIO的效能最佳化?

回答2:三元件 Selector Channel Buffer。

6。3 Reactor三種模式

6。3。1 Reactor單執行緒模式

單執行緒模式:一個Reactor執行緒(處理三種請求accept read write + 業務邏輯操作 解碼 計算 編碼)

啟程,從IO出發...

上圖理解:Reactor單執行緒模型包括三個元件:Dispacher、Accepter、Handler,其中,Reactor/Dispacher進行請求分發,連線請求到Acceptor地方,讀寫請求一共五個步驟:讀取 解碼 計算 編碼 傳送。

6。3。2 Reactor執行緒池模式

多執行緒模式:一個Reactor執行緒(處理三種請求 accept read write) + 一個工作者worker執行緒池(處理非IO的具體業務邏輯操作)

啟程,從IO出發...

上圖理解:Reactor單執行緒模型包括三個元件:Dispacher、Accepter、Handler,其中,Reactor/Dispacher進行請求分發,連線請求到Acceptor地方,讀寫請求一共五個步驟:讀取 解碼 計算 編碼 傳送。

創新點:添加了一個工作者worker執行緒池,並將非I/O操作(解碼 計算 編碼)從Reactor執行緒中移出轉交給工作者執行緒池來執行。Reactor執行緒只需要accept read write 三個操作,其他的解碼 計算 編碼交給新建的工作者執行緒池執行,提高Reactor執行緒的I/O響應,不至於因為一些耗時的業務邏輯而延遲對後面I/O請求的處理(耗時操作都到工作者執行緒池中去了)

金手指:

注意,在上圖的改進的版本中,所以的I/O操作依舊由一個Reactor來完成,包括I/O的accept()、read()、write()以及connect()操作。

6。3。3 Reactor主從執行緒池模式

主從多執行緒模式:一個mainReactor執行緒(處理三種請求 accept)+ 一個subReactor執行緒池(處理請求 read write) + 一個工作者worker執行緒池(處理非IO的具體業務邏輯操作)

啟程,從IO出發...

上圖理解:Reactor單執行緒模型包括三個元件:Dispacher、Accepter、Handler,其中,Reactor/Dispacher進行請求分發,連線請求到Acceptor地方,讀寫請求一共五個步驟:讀取 解碼 計算 編碼 傳送。

創新點1

:從一個Reactor變成了兩個,mainReactor和subReactor,mainReactor用來分發accept請求,對於已經accept的請求,交個subReactor,讓subReactor來分發read write請求,mainReactor可以只有一個,但subReactor一般會有多個。mainReactor執行緒主要負責接收客戶端的連線請求,然後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通訊。

創新點2

:添加了一個工作者worker執行緒池,並將非I/O操作(即解碼、計算、編碼)從Reactor執行緒中移出轉交給工作者執行緒池來執行。Reactor執行緒只需要accept read write 三個操作,其他的解碼、計算、編碼交給新建的工作者執行緒池執行,提高Reactor執行緒的I/O響應,不至於因為一些耗時的業務邏輯而延遲對後面I/O請求的處理(耗時操作都到工作者執行緒池中去了)。

金手指:所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依舊還是在Reactor執行緒(mainReactor執行緒或 subReactor執行緒)中完成的。Thread Pool(執行緒池)僅用來處理非I/O操作的邏輯。

七、尾聲

本文主要對Reactor模式進行詳細的解析,Netty中正是應用Reactor模式來實現非同步事件驅動網路應用框架的(Linux底層事件驅動IO–>Java NIO同步非阻塞–>Reactor主從多執行緒模式–>Netty主從多執行緒模式),所以對於Reactor模式的掌握在Netty的學習是至關重要的。

天天打碼,天天進步!!!

標簽: 執行緒  Reactor  事件  客戶端  請求