前言的前言
服務(wù)器模型涉及到線程模式和IO模式,搞清楚這些就能針對各種場景有的放矢。該系列分成三部分:
單線程/多線程阻塞I/O模型
單線程非阻塞I/O模型
多線程非阻塞I/O模型,Reactor及其改進(jìn)
前言
這里探討的服務(wù)器模型主要指的是服務(wù)器端對I/O的處理模型。從不同維度可以有不同的分類,這里從I/O的阻塞與非阻塞、I/O處理的單線程與多線程角度探討服務(wù)器模型。
對于I/O,可以分成阻塞I/O與非阻塞I/O兩大類型。阻塞I/O在做I/O讀寫操作時(shí)會使當(dāng)前線程進(jìn)入阻塞狀態(tài),而非阻塞I/O則不進(jìn)入阻塞狀態(tài)。
對于線程,單線程情況下由一條線程負(fù)責(zé)所有客戶端連接的I/O操作,而多線程情況下則由若干線程共同處理所有客戶端連接的I/O操作。
單線程阻塞I/O模型
單線程阻塞I/O模型是最簡單的一種服務(wù)器模型,幾乎所有程序員在剛開始接觸網(wǎng)絡(luò)編程時(shí)都從這個(gè)簡單的模型開始。這種模型只能同時(shí)處理一個(gè)客戶端訪問,并且在I/O操作上是阻塞的,線程會一直在等待,而不會做其他事情。對于多個(gè)客戶端訪問,必須要等到前一個(gè)客戶端訪問結(jié)束才能進(jìn)行下一個(gè)訪問的處理,請求一個(gè)一個(gè)排隊(duì),只提供一問一答服務(wù)。
首先,服務(wù)器必須初始化一個(gè)套接字服務(wù)器,并綁定某個(gè)端口號并使之監(jiān)聽客戶端的訪問。接著,客戶端1調(diào)用服務(wù)器的服務(wù),服務(wù)器接收到請求后對其進(jìn)行處理,處理完后寫數(shù)據(jù)回客戶端1,整個(gè)過程都是在一個(gè)線程里面完成的。最后,處理客戶端2的請求并寫數(shù)據(jù)回客戶端2,期間就算客戶端2在服務(wù)器處理完客戶端1之前就進(jìn)行請求,也要等服務(wù)器對客戶端1響應(yīng)完后才會對客戶端2進(jìn)行響應(yīng)處理。
這種模型的特點(diǎn)在于單線程和阻塞I/O。單線程即服務(wù)器端只有一個(gè)線程處理客戶端的所有請求,客戶端連接與服務(wù)器端的處理線程比是n:1,它無法同時(shí)處理多個(gè)連接,只能串行處理連接。而阻塞I/O是指服務(wù)器在讀寫數(shù)據(jù)時(shí)是阻塞的,讀取客戶端數(shù)據(jù)時(shí)要等待客戶端發(fā)送數(shù)據(jù)并且把操作系統(tǒng)內(nèi)核復(fù)制到用戶進(jìn)程中,這時(shí)才解除阻塞狀態(tài)。寫數(shù)據(jù)回客戶端時(shí)要等待用戶進(jìn)程將數(shù)據(jù)寫入內(nèi)核并發(fā)送到客戶端后才解除阻塞狀態(tài)。這種阻塞給網(wǎng)絡(luò)編程帶來了一個(gè)問題,服務(wù)器必須要等到客戶端成功接收才能繼續(xù)往下處理另外一個(gè)客戶端的請求,在此期間線程將無法響應(yīng)任何客戶端請求。
該模型的特點(diǎn):它是最簡單的服務(wù)器模型,整個(gè)運(yùn)行過程都只有一個(gè)線程,只能支持同時(shí)處理一個(gè)客戶端的請求(如果有多個(gè)客戶端訪問,就必須排隊(duì)等待),服務(wù)器系統(tǒng)資源消耗較小,但并發(fā)能力低,容錯(cuò)能力差。
多線程阻塞I/O模型
針對單線程阻塞I/O模型的缺點(diǎn),我們可以使用多線程對其進(jìn)行改進(jìn),使之能并發(fā)地對多個(gè)客戶端同時(shí)進(jìn)行響應(yīng)。多線程模型的核心就是利用多線程機(jī)制為每個(gè)客戶端分配一個(gè)線程。服務(wù)器端開始監(jiān)聽客戶端的訪問,假如有兩個(gè)客戶端發(fā)送請求過來,服務(wù)器端在接收到客戶端請求后分別創(chuàng)建兩個(gè)線程對它們進(jìn)行處理,每條線程負(fù)責(zé)一個(gè)客戶端連接,直到響應(yīng)完成。期間兩個(gè)線程并發(fā)地為各自對應(yīng)的客戶端處理請求,包括讀取客戶端數(shù)據(jù)、處理客戶端數(shù)據(jù)、寫數(shù)據(jù)回客戶端等操作。
這種模型的I/O操作也是阻塞的,因?yàn)槊總€(gè)線程執(zhí)行到讀取或?qū)懭氩僮鲿r(shí)都將進(jìn)入阻塞狀態(tài),直到讀取到客戶端的數(shù)據(jù)或數(shù)據(jù)成功寫入客戶端后才解除阻塞狀態(tài)。盡管I/O操作阻塞,但這種模式比單線程處理的性能明顯高了,它不用等到第一個(gè)請求處理完才處理第二個(gè),而是并發(fā)地處理客戶端請求,客戶端連接與服務(wù)器端處理線程的比例是1:1。
多線程阻塞I/O模型的特點(diǎn):支持對多個(gè)客戶端并發(fā)響應(yīng),處理能力得到大幅提高,有較大的并發(fā)量,但服務(wù)器系統(tǒng)資源消耗量較大,而且多線程之間會產(chǎn)生線程切換成本,同時(shí)擁有較復(fù)雜的結(jié)構(gòu)。
單線程非阻塞I/O模型
多線程阻塞I/O模型通過引入多線程確實(shí)提高了服務(wù)器端的并發(fā)處理能力,但每個(gè)連接都需要一個(gè)線程負(fù)責(zé)I/O操作。當(dāng)連接數(shù)量較多時(shí)可能導(dǎo)致機(jī)器線程數(shù)量太多,而這些線程大多數(shù)時(shí)間卻處于等待狀態(tài),造成極大的資源浪費(fèi)。鑒于多線程阻塞I/O模型的缺點(diǎn),有沒有可能用一個(gè)線程就可以維護(hù)多個(gè)客戶端連接并且不會阻塞在讀寫操作呢?下面介紹單線程非阻塞I/O模型。
單線程非阻塞I/O模型最重要的一個(gè)特點(diǎn)是,在調(diào)用讀取或?qū)懭?a target="_blank">接口后立即返回,而不會進(jìn)入阻塞狀態(tài)。在探討單線程非阻塞I/O模型前必須要先了解非阻塞情況下套接字事件的檢測機(jī)制,因?yàn)閷τ趩尉€程非阻塞模型最重要的事情是檢測哪些連接有感興趣的事件發(fā)生。一般會有如下三種檢測方式。
應(yīng)用程序遍歷套接字的事件檢測
當(dāng)多個(gè)客戶端向服務(wù)器請求時(shí),服務(wù)器端會保存一個(gè)套接字連接列表中,應(yīng)用層線程對套接字列表輪詢嘗試讀取或?qū)懭搿τ谧x取操作,如果成功讀取到若干數(shù)據(jù),則對讀取到的數(shù)據(jù)進(jìn)行處理;如果讀取失敗,則下一個(gè)循環(huán)再繼續(xù)嘗試。對于寫入操作,先嘗試將數(shù)據(jù)寫入指定的某個(gè)套接字,寫入失敗則下一個(gè)循環(huán)再繼續(xù)嘗試。
這樣看來,不管有多少個(gè)套接字連接,它們都可以被一個(gè)線程管理,一個(gè)線程負(fù)責(zé)遍歷這些套接字列表,不斷地嘗試讀取或?qū)懭霐?shù)據(jù)。這很好地利用了阻塞的時(shí)間,處理能力得到提升。但這種模型需要在應(yīng)用程序中遍歷所有的套接字列表,同時(shí)需要處理數(shù)據(jù)的拼接,連接空閑時(shí)可能也會占用較多CPU資源,不適合實(shí)際使用。對此改進(jìn)的方法是使用事件驅(qū)動的非阻塞方式。
內(nèi)核遍歷套接字的事件檢測
這種方式將套接字的遍歷工作交給了操作系統(tǒng)內(nèi)核,把對套接字遍歷的結(jié)果組織成一系列的事件列表并返回應(yīng)用層處理。對于應(yīng)用層,它們需要處理的對象就是這些事件,這就是其中一種事件驅(qū)動的非阻塞方式的實(shí)現(xiàn)。
服務(wù)器端有多個(gè)客戶端連接,應(yīng)用層向內(nèi)核請求讀寫事件列表。內(nèi)核遍歷所有套接字并生成對應(yīng)的可讀列表readList和可寫列表writeList。readList標(biāo)明了每個(gè)套接字是否可讀,例如套接字1的值為1,表示可讀,socket2的值為0,表示不可讀。writeList則標(biāo)明了每個(gè)套接字是否可寫。應(yīng)用層遍歷讀寫事件列表readList和writeList,做相應(yīng)的讀寫操作。
內(nèi)核遍歷套接字時(shí)已經(jīng)不用在應(yīng)用層對所有套接字進(jìn)行遍歷,將遍歷工作下移到內(nèi)核層,這種方式有助于提高檢測效率。然而,它需要將所有連接的可讀事件列表和可寫事件列表傳到應(yīng)用層,假如套接字連接數(shù)量變大,列表從內(nèi)核復(fù)制到應(yīng)用層也是不小的開銷。另外,當(dāng)活躍連接較少時(shí),內(nèi)核與應(yīng)用層之間存在很多無效的數(shù)據(jù)副本,因?yàn)樗鼘⒒钴S和不活躍的連接狀態(tài)都復(fù)制到應(yīng)用層中。
內(nèi)核基于回調(diào)的事件檢測
通過遍歷的方式檢測套接字是否可讀可寫是一種效率比較低的方式,不管是在應(yīng)用層中遍歷還是在內(nèi)核中遍歷。所以需要另外一種機(jī)制來優(yōu)化遍歷的方式,那就是回調(diào)函數(shù)。內(nèi)核中的套接字都對應(yīng)一個(gè)回調(diào)函數(shù),當(dāng)客戶端往套接字發(fā)送數(shù)據(jù)時(shí),內(nèi)核從網(wǎng)卡接收數(shù)據(jù)后就會調(diào)用回調(diào)函數(shù),在回調(diào)函數(shù)中維護(hù)事件列表,應(yīng)用層獲取此事件列表即可得到所有感興趣的事件。
內(nèi)核基于回調(diào)的事件檢測方式有兩種。第一種是用可讀列表readList和可寫列表writeList標(biāo)記讀寫事件,套接字的數(shù)量與readList和writeList兩個(gè)列表的長度一樣,readList第一個(gè)元素標(biāo)為1則表示套接字1可讀,同理,writeList第二個(gè)元素標(biāo)為1則表示套接字2可寫。如圖所示,多個(gè)客戶端連接服務(wù)器端,當(dāng)客戶端發(fā)送數(shù)據(jù)過來時(shí),內(nèi)核從網(wǎng)卡復(fù)制數(shù)據(jù)成功后調(diào)用回調(diào)函數(shù)將readList第一個(gè)元素置為1,應(yīng)用層發(fā)送請求讀、寫事件列表,返回內(nèi)核包含了事件標(biāo)識的readList和writeList事件列表,進(jìn)而分表遍歷讀事件列表readList和寫事件列表writeList,對置為1的元素對應(yīng)的套接字進(jìn)行讀或?qū)懖僮鳌_@樣就避免了遍歷套接字的操作,但仍然有大量無用的數(shù)據(jù)(狀態(tài)為0的元素)從內(nèi)核復(fù)制到應(yīng)用層中。于是就有了第二種事件檢測方式。
內(nèi)核基于回調(diào)的事件檢測方式二如圖所示。服務(wù)器端有多個(gè)客戶端套接字連接。首先,應(yīng)用層告訴內(nèi)核每個(gè)套接字感興趣的事件。接著,當(dāng)客戶端發(fā)送數(shù)據(jù)過來時(shí),對應(yīng)會有一個(gè)回調(diào)函數(shù),內(nèi)核從網(wǎng)卡復(fù)制數(shù)據(jù)成功后即調(diào)回調(diào)函數(shù)將套接字1作為可讀事件event1加入到事件列表。同樣地,內(nèi)核發(fā)現(xiàn)網(wǎng)卡可寫時(shí)就將套接字2作為可寫事件event2添加到事件列表中。最后,應(yīng)用層向內(nèi)核請求讀、寫事件列表,內(nèi)核將包含了event1和event2的事件列表返回應(yīng)用層,應(yīng)用層通過遍歷事件列表得知套接字1有數(shù)據(jù)待讀取,于是進(jìn)行讀操作,而套接字2則可以寫入數(shù)據(jù)。
上面兩種方式由操作系統(tǒng)內(nèi)核維護(hù)客戶端的所有連接并通過回調(diào)函數(shù)不斷更新事件列表,而應(yīng)用層線程只要遍歷這些事件列表即可知道可讀取或可寫入的連接,進(jìn)而對這些連接進(jìn)行讀寫操作,極大提高了檢測效率,自然處理能力也更強(qiáng)。
對于Java來說,非阻塞I/O的實(shí)現(xiàn)完全是基于操作系統(tǒng)內(nèi)核的非阻塞I/O,它將操作系統(tǒng)的非阻塞I/O的差異屏蔽并提供統(tǒng)一的API,讓我們不必關(guān)心操作系統(tǒng)。JDK會幫我們選擇非阻塞I/O的實(shí)現(xiàn)方式,例如對于Linux系統(tǒng),在支持epoll的情況下JDK會優(yōu)先選擇用epoll實(shí)現(xiàn)Java的非阻塞I/O。這種非阻塞方式的事件檢測機(jī)制就是效率最高的“內(nèi)核基于回調(diào)的事件檢測”中的第二種方式。
在了解了非阻塞模式下的事件檢測方式后,重新回到對單線程非阻塞I/O模型的討論。雖然只有一個(gè)線程,但是它通過把非阻塞讀寫操作與上面幾種檢測機(jī)制配合就可以實(shí)現(xiàn)對多個(gè)連接的及時(shí)處理,而不會因?yàn)槟硞€(gè)連接的阻塞操作導(dǎo)致其他連接無法處理。在客戶端連接大多數(shù)都保持活躍的情況下,這個(gè)線程會一直循環(huán)處理這些連接,它很好地利用了阻塞的時(shí)間,大大提高了這個(gè)線程的執(zhí)行效率。
單線程非阻塞I/O模型的主要優(yōu)勢體現(xiàn)在對多個(gè)連接的管理,一般在同時(shí)需要處理多個(gè)連接的發(fā)場景中會使用非阻塞NIO模式,此模型下只通過一個(gè)線程去維護(hù)和處理連接,這樣大大提高了機(jī)器的效率。一般服務(wù)器端才會使用NIO模式,而對于客戶端,出于方便及習(xí)慣,可使用阻塞模式的套接字進(jìn)行通信。
多線程非阻塞I/O模型
單線程非阻塞I/O模型已經(jīng)大大提高了機(jī)器的效率,而在多核的機(jī)器上可以通過多線程繼續(xù)提高機(jī)器效率。最樸實(shí)、最自然的做法就是將客戶端連接按組分配給若干線程,每個(gè)線程負(fù)責(zé)處理對應(yīng)組內(nèi)的連接。如圖所示,有4個(gè)客戶端訪問服務(wù)器,服務(wù)器將套接字1和套接字2交由線程1管理,而線程2則管理套接字3和套接字4,通過事件檢測及非阻塞讀寫就可以讓每個(gè)線程都能高效處理。
最經(jīng)典的多線程非阻塞I/O模型方式是Reactor模式。首先看單線程下的Reactor,Reactor將服務(wù)器端的整個(gè)處理過程分成若干個(gè)事件,例如分為接收事件、讀事件、寫事件、執(zhí)行事件等。Reactor通過事件檢測機(jī)制將這些事件分發(fā)給不同處理器去處理。如圖所示,若干客戶端連接訪問服務(wù)器端,Reactor負(fù)責(zé)檢測各種事件并分發(fā)到處理器,這些處理器包括接收連接的accept處理器、讀數(shù)據(jù)的read處理器、寫數(shù)據(jù)的write處理器以及執(zhí)行邏輯的process處理器。在整個(gè)過程中只要有待處理的事件存在,即可以讓Reactor線程不斷往下執(zhí)行,而不會阻塞在某處,所以處理效率很高。
基于單線程Reactor模型,根據(jù)實(shí)際使用場景,把它改進(jìn)成多線程模式。常見的有兩種方式:一種是在耗時(shí)的process處理器中引入多線程,如使用線程池;另一種是直接使用多個(gè)Reactor實(shí)例,每個(gè)Reactor實(shí)例對應(yīng)一個(gè)線程。
Reactor模式的一種改進(jìn)方式如圖所示。其整體結(jié)構(gòu)基本上與單線程的Reactor類似,只是引入了一個(gè)線程池。由于對連接的接收、對數(shù)據(jù)的讀取和對數(shù)據(jù)的寫入等操作基本上都耗時(shí)較少,因此把它們都放到Reactor線程中處理。然而,對于邏輯處理可能比較耗時(shí)的工作,可以在process處理器中引入線程池,process處理器自己不執(zhí)行任務(wù),而是交給線程池,從而在Reactor線程中避免了耗時(shí)的操作。將耗時(shí)的操作轉(zhuǎn)移到線程池中后,盡管Reactor只有一個(gè)線程,它也能保證Reactor的高效。
Reactor模式的另一種改進(jìn)方式如圖所示。其中有多個(gè)Reactor實(shí)例,每個(gè)Reactor實(shí)例對應(yīng)一個(gè)線程。因?yàn)榻邮帐录窍鄬τ诜?wù)器端而言的,所以客戶端的連接接收工作統(tǒng)一由一個(gè)accept處理器負(fù)責(zé),accept處理器會將接收的客戶端連接均勻分配給所有Reactor實(shí)例,每個(gè)Reactor實(shí)例負(fù)責(zé)處理分配到該Reactor上的客戶端連接,包括連接的讀數(shù)據(jù)、寫數(shù)據(jù)和邏輯處理。這就是多Reactor實(shí)例的原理。
多線程非阻塞I/O模式讓服務(wù)器端處理能力得到很大提高,它充分利用機(jī)器的CPU,適合用于處理高并發(fā)的場景,但它也讓程序更復(fù)雜,更容易出現(xiàn)問題。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9029瀏覽量
85205 -
多線程
+關(guān)注
關(guān)注
0文章
277瀏覽量
19923 -
阻塞
+關(guān)注
關(guān)注
0文章
24瀏覽量
8093 -
非阻塞
+關(guān)注
關(guān)注
0文章
13瀏覽量
2165 -
單線程
+關(guān)注
關(guān)注
0文章
17瀏覽量
1769
原文標(biāo)題:最全服務(wù)器模型詳解——從單線程阻塞到多線程非阻塞
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論