網(wǎng)站建設(shè)設(shè)計(jì)設(shè)計(jì)公司哪家好軟文推廣是什么意思?
本文主要介紹服務(wù)端對(duì)于網(wǎng)絡(luò)并發(fā)模型以及Linux系統(tǒng)下常見(jiàn)的網(wǎng)絡(luò)IO復(fù)用并發(fā)模型。文章內(nèi)容一共分為兩個(gè)部分。
第一部分主要介紹網(wǎng)絡(luò)并發(fā)中的一些基本概念以及我們Linux下常見(jiàn)的原生IO復(fù)用系統(tǒng)調(diào)用(epoll/select)等。第二部分主要介紹并發(fā)場(chǎng)景下常見(jiàn)的網(wǎng)絡(luò)IO復(fù)用模型,以及各自的優(yōu)缺點(diǎn)。
一、網(wǎng)絡(luò)并發(fā)模型中的幾個(gè)基本概念
1 流
開(kāi)發(fā)過(guò)程中,一般給流的定義有很多種,這里面我們總結(jié)用三個(gè)特征來(lái)描述一個(gè)流的定義:
(1)可以進(jìn)行I/O操作的內(nèi)核對(duì)象。
(2)傳輸媒介可以是文件、管道、套接字等。
(3)數(shù)據(jù)的入口是通過(guò)文件描述符(fd)。
2 I/O操作
其實(shí)所有對(duì)流的讀寫操作,都可以稱之為IO操作,如圖1.1.2所示,是向一個(gè)已經(jīng)滿的流再去執(zhí)行寫入操作,那么這次的寫入實(shí)際上就是一個(gè)IO操作。當(dāng)然已經(jīng)將流寫滿了,那么這次寫入就會(huì)發(fā)生阻塞。
圖1.1 傳輸媒介已滿,再寫入的IO操作
那么如果流為空的情況,再執(zhí)行讀操作,那么這次讀取也是一個(gè)IO操作,也依然會(huì)發(fā)生阻塞的情況。
如圖1.1和圖1.2所示,他們都是對(duì)IO的操作,當(dāng)我們向一個(gè)容量已經(jīng)滿的傳輸媒介寫數(shù)據(jù)的時(shí)候,那么這個(gè)IO操作就會(huì)發(fā)生寫阻塞。同理,當(dāng)我們從一個(gè)容量為空的傳輸媒介讀數(shù)據(jù)的時(shí)候,這個(gè)IO操作就會(huì)發(fā)生讀阻塞。
3 阻塞等待
通過(guò)IO的讀寫過(guò)程,我們得到了阻塞的概念。那么我們?nèi)绾蝸?lái)形象地表示一個(gè)阻塞的現(xiàn)象呢?
圖1.3 阻塞等待
如圖1.3所示,假設(shè)您今天清閑在家無(wú)事可做,您家里有一部座機(jī),這個(gè)是您唯一可以和外界建立溝通的媒介。假設(shè)您今天準(zhǔn)備洗一雙襪子,但是缺少一塊肥皂,這個(gè)肥皂在等待快遞員給您送過(guò)來(lái),而您又是一個(gè)“單細(xì)胞動(dòng)物”,今天必須要先把襪子洗完才能做其他的事,否則無(wú)事可做。那么此時(shí)您的這種在一天的生活流程中因等待某個(gè)資源導(dǎo)致生活節(jié)奏暫停的狀態(tài),就是阻塞等待狀態(tài)。
4 非阻塞,忙輪詢
圖1.4 非阻塞,忙輪詢
與阻塞等待相對(duì)應(yīng)的狀態(tài)是非阻塞,忙輪詢狀態(tài)。假設(shè)您性子比較急躁,每分鐘必須要打電話詢問(wèn)快遞小哥一次,“到底有沒(méi)有到?”,那么快遞員每隔一段時(shí)間就會(huì)接聽(tīng)到你的電話詢問(wèn),并告訴你是否到了,這樣你就可以主動(dòng)地指導(dǎo)你所缺的肥皂資源是否已經(jīng)抵達(dá)。那么您此種不斷通過(guò)通訊媒介詢問(wèn)對(duì)方并且循環(huán)往復(fù)的狀態(tài)就是一種非阻塞忙輪詢的狀態(tài)。
5 阻塞與非阻塞對(duì)比
阻塞等待:空出大腦可以安心睡覺(jué),不影響快遞員工作(不占用CPU寶貴的時(shí)間片)。
非阻塞,忙輪詢:浪費(fèi)時(shí)間,浪費(fèi)電話費(fèi),占用快遞員時(shí)間(占用CPU,系統(tǒng)資源)。
很明顯,通過(guò)上述的場(chǎng)景作為比較,阻塞等待這種方式,對(duì)于通信上是有明顯優(yōu)勢(shì)的,阻塞等待也并非是沒(méi)有弊端的。
二、解決阻塞等待缺點(diǎn)的辦法
1 阻塞死等待的缺點(diǎn)
阻塞等待也是有非常明顯的缺點(diǎn)的,現(xiàn)在我們比如有如下場(chǎng)景,如圖1.5所示。
圖1.5 阻塞
同一時(shí)刻,你只能被動(dòng)地處理一個(gè)快遞員的簽收業(yè)務(wù),其他快遞員打電話打不進(jìn)來(lái),你的電話是座機(jī),在簽收的時(shí)候,接不到其他快遞員的電話。所以阻塞等待的問(wèn)題很明顯,我們無(wú)法在同一時(shí)刻解決多個(gè)IO的讀寫請(qǐng)求。
2 解決阻塞等待的辦法一:多線程/多進(jìn)程
那么解決這個(gè)問(wèn)題,即使家里多買N個(gè)座機(jī), 但是依然是你一個(gè)人接,也處理不過(guò)來(lái),所以就需要用“影分身術(shù)”創(chuàng)建都個(gè)自己來(lái)接電話(采用多線程或者多進(jìn)程)來(lái)處理,如圖1.6所示。
圖1.6 辦法一:提高資源(開(kāi)多線程/多進(jìn)程)
這種方式就是沒(méi)有多路IO復(fù)用的情況的解決方案,但是大量的開(kāi)辟線程和進(jìn)程也是非常浪費(fèi)資源的,我們知道一個(gè)操作系統(tǒng)能夠同時(shí)運(yùn)行的線程和進(jìn)程都是有上限的,尤其是進(jìn)程占用內(nèi)存的資源極高,這樣也就限定了能夠同時(shí)處理IO數(shù)量的瓶頸。
3 解決阻塞等待的辦法二:非阻塞、忙輪詢
那么如果我們不借助影分身的方式(多線程/多進(jìn)程),該如何解決阻塞死等待的方法呢?
如果我們是采用非阻塞的方式,那么可以用一個(gè)辦法來(lái)監(jiān)控多個(gè)IO的狀態(tài),我們可以采用粗暴的“非阻塞,忙輪詢”方式,如圖1.7所示。
圖1.7 辦法二:非阻塞忙輪詢
非阻塞忙輪詢的方式,可以讓用戶分別與每個(gè)快遞員取得聯(lián)系,宏觀上來(lái)看,是同時(shí)可以與多個(gè)快遞員溝通(并發(fā)效果)、 但是快遞員在于用戶溝通時(shí)耽誤前進(jìn)的速度(浪費(fèi)CPU)。
非阻塞忙輪詢的方式具體的實(shí)現(xiàn)邏輯偽代碼如下:
while true {for i in 流[] {if i has 數(shù)據(jù) {讀 或者 其他處理}}
}
一層循環(huán)下,不斷的遍歷流是否有數(shù)據(jù),如果沒(méi)有或者有則進(jìn)行邏輯處理,然后不停歇地進(jìn)入下一次遍歷,直到for循環(huán)出來(lái),又回到while true的無(wú)限重復(fù)次數(shù)中。所以非阻塞忙輪詢雖然能夠在短暫的時(shí)間內(nèi)監(jiān)控到每個(gè)IO的讀寫狀態(tài),但是付出的代價(jià)是無(wú)限不停歇的判斷,這樣往往會(huì)使CPU過(guò)于勞累,把CPU資源打滿,所以非阻塞忙輪詢并不是一個(gè)非常好的解決方案。
相關(guān)視頻推薦
手寫一個(gè)epoll組件,為tcp并發(fā)實(shí)現(xiàn)epoll
服務(wù)端的網(wǎng)絡(luò)并發(fā),詳解網(wǎng)絡(luò)io與線程進(jìn)程的關(guān)系
準(zhǔn)備好4臺(tái)虛擬機(jī),實(shí)現(xiàn)服務(wù)器的百萬(wàn)級(jí)并發(fā)
Linux C/C++開(kāi)發(fā)(后端/音視頻/游戲/嵌入式/高性能網(wǎng)絡(luò)/存儲(chǔ)/基礎(chǔ)架構(gòu)/安全)
需要C/C++ Linux服務(wù)器架構(gòu)師學(xué)習(xí)資料加qun812855908獲取(資料包括C/C++,Linux,golang技術(shù),Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等),免費(fèi)分享
4 解決阻塞等待的辦法三:select
我們可以開(kāi)設(shè)一個(gè)代收網(wǎng)點(diǎn),讓快遞員全部送到代收點(diǎn)。這個(gè)網(wǎng)店管理員叫select。這樣我們就可以在家休息了,麻煩的事交給select就好了。當(dāng)有快遞的時(shí)候,select負(fù)責(zé)給我們打電話,期間在家休息睡覺(jué)就好了。
圖1.8辦法三:select
但select 代收員比較懶,她記不住快遞員的單號(hào),還有快遞貨物的數(shù)量。她只會(huì)告訴你快遞到了,但是是誰(shuí)到的,你需要挨個(gè)快遞員問(wèn)一遍。實(shí)現(xiàn)的邏輯偽代碼如下:
while true {//阻塞select(流[]);//有消息抵達(dá)for i in 流[] {if i has 數(shù)據(jù) {讀 或者 其他處理}}
}
Select的實(shí)現(xiàn)邏輯在while true的外層循環(huán)下,會(huì)有一個(gè)阻塞的過(guò)程,這個(gè)阻塞并不是永久阻塞,而是當(dāng)select所監(jiān)聽(tīng)的流中(多個(gè)IO傳輸媒介),有一個(gè)流可以讀寫,那么select就會(huì)立刻返回,當(dāng)我們得知select已經(jīng)返回,說(shuō)目前的流一定是具備讀寫能力的,那么這時(shí)候就可以遍歷這個(gè)流中,如果流有數(shù)據(jù),我們就讀出來(lái)處理,如果沒(méi)有就看下一個(gè)流是否有。
用select并不會(huì)出現(xiàn)非阻塞忙輪詢的無(wú)限判斷情況的出現(xiàn),因?yàn)閟elect是可以阻塞的,阻塞的時(shí)候是不占用任何CPU資源的,但select有個(gè)明顯的缺點(diǎn),因?yàn)樗看味紩?huì)返回全量的流集合,并不會(huì)告訴開(kāi)發(fā)者哪個(gè)流可讀寫,哪個(gè)流不可讀寫,我們需要再循環(huán)全量的流集合,再進(jìn)行判斷是否讀寫,所以即使有一個(gè)流觸發(fā),我們依然for要全部流都掃描一遍,這顯然是一種低效的方式。
5 解決阻塞等待的辦法四:epoll
現(xiàn)在這個(gè)快遞代收驛站升級(jí)了,服務(wù)更加友善且能力更強(qiáng),與select一樣,你依然可以在家休息,被動(dòng)的接收epoll發(fā)來(lái)的通知,如圖1.9所示。
圖1.9 辦法四:epoll
epoll的服務(wù)態(tài)度要比select好很多,在通知我們的時(shí)候,不僅告訴我們有幾個(gè)快遞到了,還分別告訴我們是誰(shuí)誰(shuí)誰(shuí)。我們只需要按照epoll給的答復(fù),來(lái)詢問(wèn)快遞員取快遞即可。實(shí)現(xiàn)邏輯的偽代碼如下:
while true {//阻塞可處理的流[] = epoll_wait(epoll_fd);//有消息抵達(dá),全部放在 “可處理的流[]”中for i in 可處理的流[] {讀 或者 其他處理}
}
使用epoll來(lái)解決監(jiān)聽(tīng)多個(gè)IO的邏輯和select極為相似,在使用過(guò)程中,有個(gè)重大的區(qū)別在于epoll_wait發(fā)生阻塞的時(shí)候,如果監(jiān)控的流中有IO可以讀寫,那么epoll_wait會(huì)給我們返回一個(gè)可以讀寫流的集合,那么不可以讀寫的流epoll并不會(huì)返回給我們。這樣實(shí)際上會(huì)減少我們的無(wú)效的遍歷,這一點(diǎn)epoll要比select做得畢竟優(yōu)秀。另外一個(gè)地方在實(shí)現(xiàn)邏輯中看不出來(lái),select能夠最大監(jiān)聽(tīng)I(yíng)O的數(shù)量是一個(gè)固定的數(shù)(這個(gè)可以修改,但是畢竟困難,需要重新編譯操作系統(tǒng)),而且這個(gè)數(shù)量也不是很大,但是epoll能夠監(jiān)聽(tīng)的最大的IO數(shù)量是跟隨當(dāng)前操作系統(tǒng)的內(nèi)存大小成正比的。所以epoll在監(jiān)控IO數(shù)量這塊也要比select優(yōu)秀很多。
三、什么是epoll
epoll與select,poll一樣,是對(duì)I/O多路復(fù)用的技術(shù),它只關(guān)心“活躍”的鏈接,無(wú)須遍歷全部描述符集合,它能夠處理大量的鏈接請(qǐng)求(系統(tǒng)可以打開(kāi)的文件數(shù)目,取決于內(nèi)存大小)。
1 Linux提供的epoll的系統(tǒng)調(diào)用
epoll的開(kāi)發(fā)流程是屬于linux操作系統(tǒng)提供給用戶態(tài)開(kāi)發(fā)者的一系列系統(tǒng)調(diào)用函數(shù),這些函數(shù)的直接接口都是C語(yǔ)言實(shí)現(xiàn)。所以開(kāi)發(fā)者一般基于epoll進(jìn)行開(kāi)發(fā)的話,一般都是基于C語(yǔ)言進(jìn)行開(kāi)發(fā),這樣最接近操作系統(tǒng),性能上也是最優(yōu)的方法。
epoll的開(kāi)發(fā)流程基本分為三大步驟:
第一步:創(chuàng)建epoll;
第二步:控制epoll;
第三步:等待epoll。
接下來(lái)我們來(lái)看一下Linux給開(kāi)發(fā)者提供的epoll的原生接口是什么樣子的。
1)創(chuàng)建EPOLL
原型如下:
/*** @param size 告訴內(nèi)核監(jiān)聽(tīng)的數(shù)目* @returns 返回一個(gè)epoll句柄(即一個(gè)文件描述符)*/
int epoll_create(int size);
int epfd = epoll_create(1000);
當(dāng)我們執(zhí)行上述代碼時(shí),在內(nèi)核中實(shí)則是創(chuàng)建一顆紅黑樹(shù)(平衡二叉樹(shù))的根節(jié)點(diǎn)root,如圖1.10所示。
圖1.10 epoll系統(tǒng)調(diào)用1
這個(gè)根節(jié)點(diǎn)的關(guān)系與epfd相對(duì)應(yīng)。
2)控制EPOLL
原型如下:
/**
* @param epfd 用epoll_create所創(chuàng)建的epoll句柄
* @param op 表示對(duì)epoll監(jiān)控描述符控制的動(dòng)作
*
* EPOLL_CTL_ADD(注冊(cè)新的fd到epfd)
* EPOLL_CTL_MOD(修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件)
* EPOLL_CTL_DEL(epfd刪除一個(gè)fd)
*
* @param fd 需要監(jiān)聽(tīng)的文件描述符
* @param event 告訴內(nèi)核需要監(jiān)聽(tīng)的事件
*
* @returns 成功返回0,失敗返回-1, errno查看錯(cuò)誤信息
*/
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);struct epoll_event {__uint32_t events; /* epoll 事件 */epoll_data_t data; /* 用戶傳遞的數(shù)據(jù) */
}/** events : {EPOLLIN, EPOLLOUT, EPOLLPRI,EPOLLHUP, EPOLLET, EPOLLONESHOT}*/
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event new_event;new_event.events = EPOLLIN | EPOLLOUT;
new_event.data.fd = 5;epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &new_event);
創(chuàng)建一個(gè)用戶態(tài)的事件,綁定到某個(gè)fd上,然后添加到內(nèi)核中的epoll紅黑樹(shù)中,如圖1.11所示。
圖1.11 epoll系統(tǒng)調(diào)用2
3)等待EPOLL
原型如下:
/**
*
* @param epfd 用epoll_create所創(chuàng)建的epoll句柄
* @param event 從內(nèi)核得到的事件集合
* @param maxevents 告知內(nèi)核這個(gè)events有多大,
* 注意: 值 不能大于創(chuàng)建epoll_create()時(shí)的size.
* @param timeout 超時(shí)時(shí)間
* -1: 永久阻塞
* 0: 立即返回,非阻塞
* >0: 指定微秒
*
* @returns 成功: 有多少文件描述符就緒,時(shí)間到時(shí)返回0
* 失敗: -1, errno 查看錯(cuò)誤
*/
int epoll_wait(int epfd, struct epoll_event *event,int maxevents, int timeout);struct epoll_event my_event[1000];int event_cnt = epoll_wait(epfd, my_event, 1000, -1);
epoll_wait是一個(gè)阻塞的狀態(tài),如果內(nèi)核檢測(cè)到IO的讀寫響應(yīng),會(huì)拋給上層的epoll_wait, 返回給用戶態(tài)一個(gè)已經(jīng)觸發(fā)的事件隊(duì)列,同時(shí)阻塞返回。開(kāi)發(fā)者可以從隊(duì)列中取出事件來(lái)處理,其中事件里就有綁定的對(duì)應(yīng)fd是具體哪一個(gè)(之前添加epoll事件的時(shí)候已經(jīng)綁定),如圖1.12所示。
圖1.12 epoll系統(tǒng)調(diào)用3
比如這次epoll_wait返回的就是一個(gè)my_event集合,其中每一個(gè)元素均是一個(gè)event結(jié)構(gòu)體,結(jié)構(gòu)體里面有兩個(gè)重要的元素,第一個(gè)是當(dāng)前的事件類型(如:EPOLLIN或者EPOLLOUT,分別對(duì)應(yīng)讀寫事件),一個(gè)是當(dāng)前event所綁定的fd(也可以綁定任意指針)。如圖10.12所示,epoll_wait觸發(fā)了2個(gè)事件,那么開(kāi)發(fā)者只需要遍歷my_event依次處理每個(gè)event事件就可以了。
4)使用epoll編程主流程骨架
下面一段代碼是基于epoll開(kāi)發(fā)的一段主干代碼:
int epfd = epoll_create(1000);//將 listen_fd 添加進(jìn) epoll 中
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd,&listen_event);while (1) {//阻塞等待 epoll 中 的fd 觸發(fā)int active_cnt = epoll_wait(epfd, events, 1000, -1);for (i = 0 ; i < active_cnt; i++) {if (evnets[i].data.fd == listen_fd) {//accept. 并且將新accept 的fd 加進(jìn)epoll中.}else if (events[i].events & EPOLLIN) {//對(duì)此fd 進(jìn)行讀操作}else if (events[i].events & EPOLLOUT) {//對(duì)此fd 進(jìn)行寫操作}}
}
這里并沒(méi)有加數(shù)據(jù)處理和網(wǎng)絡(luò)處理實(shí)現(xiàn),只是說(shuō)明epoll的事件交互流程。我們一般寫一個(gè)服務(wù)端代碼,首先要對(duì)listen_fd(監(jiān)聽(tīng)端口的fd)進(jìn)行讀事件的監(jiān)聽(tīng),并且將這個(gè)事件放置在epoll堆里,當(dāng)listen_fd觸發(fā)可讀事件,那就說(shuō)明有新的客戶端鏈接創(chuàng)建過(guò)來(lái),那么epoll_wait的阻塞就會(huì)返回,我們通過(guò)判斷fd是否為listen_fd來(lái)做與客戶端建立鏈接的動(dòng)作, 那么將建立好的鏈接添加放置到epoll堆里,等待下次可讀可寫事件觸發(fā),這就是epoll開(kāi)發(fā)的基本流程。
四、epoll的觸發(fā)模式
本節(jié)作為附加小節(jié),實(shí)則介紹epoll的觸發(fā)模式種類,如果不想進(jìn)一步關(guān)心epoll的觸發(fā)方式的讀者可以越過(guò)本節(jié)。
epoll給開(kāi)發(fā)者提供了兩種觸發(fā)模式,他們分別是水平觸發(fā)與邊緣觸發(fā)。
1 水平觸發(fā)
水平觸發(fā)(Level Triggered,簡(jiǎn)稱:LT)的主要特點(diǎn)是,如果用戶在監(jiān)聽(tīng)epoll事件,當(dāng)內(nèi)核有事件的時(shí)候,會(huì)拷貝給用戶態(tài)事件,但是如果用戶只處理了一次,那么剩下沒(méi)有處理的會(huì)在下一次epoll_wait再次返回該事件,如圖1.13和1.14所示。
圖1.13 epoll系統(tǒng)調(diào)用4
圖1.14 epoll系統(tǒng)調(diào)用5
這樣如果用戶永遠(yuǎn)不處理這個(gè)事件,就導(dǎo)致每次都會(huì)有該事件從內(nèi)核到用戶的拷貝,如圖1.15所示,耗費(fèi)性能,但是水平觸發(fā)相對(duì)安全,最起碼事件不會(huì)丟掉,除非用戶處理完畢。
圖1.15 epoll系統(tǒng)調(diào)用6
2 邊緣觸發(fā)
邊緣觸發(fā)(Edge Triggered,簡(jiǎn)稱:ET)相對(duì)跟水平觸發(fā)相反,當(dāng)內(nèi)核有事件到達(dá), 只會(huì)通知用戶一次,至于用戶處理還是不處理,以后將不會(huì)再通知。這樣減少了拷貝過(guò)程,增加了性能,但是相對(duì)來(lái)說(shuō),如果用戶馬虎忘記處理,將會(huì)產(chǎn)生事件丟的情況,如圖1.16所示。
圖1.16 epoll系統(tǒng)調(diào)用7
五、簡(jiǎn)單的epoll服務(wù)器
1 服務(wù)端實(shí)現(xiàn)
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>#include <sys/epoll.h>#define SERVER_PORT (7778)
#define EPOLL_MAX_NUM (2048)
#define BUFFER_MAX_LEN (4096)char buffer[BUFFER_MAX_LEN];void str_toupper(char *str)
{int i;for (i = 0; i < strlen(str); i ++) {str[i] = toupper(str[i]);}
}int main(int argc, char **argv)
{int listen_fd = 0;int client_fd = 0;struct sockaddr_in server_addr;struct sockaddr_in client_addr;socklen_t client_len;int epfd = 0;struct epoll_event event, *my_events;/ socketlisten_fd = socket(AF_INET, SOCK_STREAM, 0);// bindserver_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(SERVER_PORT);bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));// listenlisten(listen_fd, 10);// epoll createepfd = epoll_create(EPOLL_MAX_NUM);if (epfd < 0) {perror("epoll create");goto END;}// listen_fd -> epollevent.events = EPOLLIN;event.data.fd = listen_fd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event) < 0) {perror("epoll ctl add listen_fd ");goto END;}my_events = malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM);while (1) {// epoll waitint active_fds_cnt = epoll_wait(epfd, my_events, EPOLL_MAX_NUM, -1);int i = 0;for (i = 0; i < active_fds_cnt; i++) {// if fd == listen_fdif (my_events[i].data.fd == listen_fd) {//acceptclient_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);if (client_fd < 0) {perror("accept");continue;}char ip[20];printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port));event.events = EPOLLIN | EPOLLET;event.data.fd = client_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);}else if (my_events[i].events & EPOLLIN) {printf("EPOLLIN\n");client_fd = my_events[i].data.fd;// do readbuffer[0] = '\0';int n = read(client_fd, buffer, 5);if (n < 0) {perror("read");continue;}else if (n == 0) {epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event);close(client_fd);}else {printf("[read]: %s\n", buffer);buffer[n] = '\0';str_toupper(buffer);write(client_fd, buffer, strlen(buffer));printf("[write]: %s\n", buffer);memset(buffer, 0, BUFFER_MAX_LEN);/*event.events = EPOLLOUT;event.data.fd = client_fd;epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);*/}}else if (my_events[i].events & EPOLLOUT) {printf("EPOLLOUT\n");/*client_fd = my_events[i].data.fd;str_toupper(buffer);write(client_fd, buffer, strlen(buffer));printf("[write]: %s\n", buffer);memset(buffer, 0, BUFFER_MAX_LEN);event.events = EPOLLIN;event.data.fd = client_fd;epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);*/}}}END:close(epfd);close(listen_fd);return 0;
}
2 客戶端實(shí)現(xiàn)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>#define MAX_LINE (1024)
#define SERVER_PORT (7778)void setnoblocking(int fd)
{int opts = 0;opts = fcntl(fd, F_GETFL);opts = opts | O_NONBLOCK;fcntl(fd, F_SETFL);
}int main(int argc, char **argv)
{int sockfd;char recvline[MAX_LINE + 1] = {0};struct sockaddr_in server_addr;if (argc != 2) {fprintf(stderr, "usage ./client <SERVER_IP>\n");exit(0);}// 創(chuàng)建socketif ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {fprintf(stderr, "socket error");exit(0);}// server addr 賦值bzero(&server_addr, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {fprintf(stderr, "inet_pton error for %s", argv[1]);exit(0);}// 鏈接服務(wù)端if (connect(sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {perror("connect");fprintf(stderr, "connect error\n");exit(0);}setnoblocking(sockfd);char input[100];int n = 0;int count = 0;// 不斷的從標(biāo)準(zhǔn)輸入字符串while (fgets(input, 100, stdin) != NULL){printf("[send] %s\n", input);n = 0;// 把輸入的字符串發(fā)送 到 服務(wù)器中去n = send(sockfd, input, strlen(input), 0);if (n < 0) {perror("send");}n = 0;count = 0;// 讀取 服務(wù)器返回的數(shù)據(jù)while (1){n = read(sockfd, recvline + count, MAX_LINE);if (n == MAX_LINE){count += n;continue;}else if (n < 0){perror("recv");break;}else {count += n;recvline[count] = '\0';printf("[recv] %s\n", recvline);break;}}}return 0;
}
六、Linux下常見(jiàn)的網(wǎng)絡(luò)IO復(fù)用并發(fā)模型
本節(jié)主要介紹常見(jiàn)的Server的并發(fā)模型,這些模型與編程語(yǔ)言本身無(wú)關(guān),有的編程語(yǔ)言可能在語(yǔ)法上直接透明了模型本質(zhì),所以開(kāi)發(fā)者沒(méi)必要一定要基于模型去編寫,只是需要知道和了解并發(fā)模型的構(gòu)成和特點(diǎn)即可,本節(jié)的一些使用模型需要讀者了解基本的多路IO復(fù)用知識(shí),如一~六章節(jié)介紹。
1 模型一:單線程Accept(無(wú)IO復(fù)用)
模型一是單線程的Server,并且不適用任何IO復(fù)用機(jī)制,來(lái)實(shí)現(xiàn)一個(gè)基本的網(wǎng)絡(luò)服務(wù)器。其結(jié)構(gòu)如圖1.17所示。
圖1.17 網(wǎng)絡(luò)并發(fā)模型一:單線程Accept
1) 流程
(1)我們首先啟動(dòng)一個(gè)Server服務(wù)端進(jìn)程,其中進(jìn)程包括主線程main thread。我們知道一個(gè)基本的服務(wù)端Socket編程需要的幾個(gè)關(guān)鍵步驟,創(chuàng)建一個(gè)ListenFd(服務(wù)端監(jiān)聽(tīng)套接字),將這個(gè)ListenFd綁定到需要服務(wù)的IP和端口上,然后執(zhí)行阻塞Accept被動(dòng)等待遠(yuǎn)程的客戶端建立鏈接,每次客戶端Connect鏈接過(guò)來(lái),main thread中accept響應(yīng)并建立連接。
(2)這里第一個(gè)鏈接過(guò)來(lái)的Client1請(qǐng)求服務(wù)端鏈接,服務(wù)端Server創(chuàng)建鏈接成功,得到Connfd1套接字后, 依然在main thread串行處理套接字讀寫,并處理業(yè)務(wù)。
(3) 在(2)處理業(yè)務(wù)中,如果有新客戶端Connect過(guò)來(lái),Server無(wú)響應(yīng),直到當(dāng)前套接字全部業(yè)務(wù)處理完畢。
(4)當(dāng)前客戶端處理完后,完畢鏈接,處理下一個(gè)客戶端請(qǐng)求。
以上是模型一的服務(wù)端整體執(zhí)行邏輯,我們來(lái)分析一下模型一的優(yōu)缺點(diǎn):
2)優(yōu)點(diǎn)
模型一的socket編程流程清晰且簡(jiǎn)單,適合學(xué)習(xí)使用,可以基于模型一很快地了解socket基本編程流程。
3)缺點(diǎn)
該模型并非并發(fā)模型,是串行的服務(wù)器,同一時(shí)刻,監(jiān)聽(tīng)并響應(yīng)最大的網(wǎng)絡(luò)請(qǐng)求量為1。即并發(fā)量為1。
所以綜上,僅適合學(xué)習(xí)基本 socket編程,不適合任何服務(wù)器Server構(gòu)建。
2 模型二:單線程Accept+多線程讀寫業(yè)務(wù)(無(wú)IO復(fù)用)
模型二是主進(jìn)程啟動(dòng)一個(gè)main thread線程,其中main thread在進(jìn)行socket初始化的過(guò)程和模型一是一樣的,那么對(duì)于如果有新的Client建立鏈接請(qǐng)求進(jìn)來(lái),就會(huì)出現(xiàn)和模型一不同的地方,如圖1.18所示。
圖1.18 網(wǎng)絡(luò)并發(fā)模型二:單線程Accept+多線程讀寫-1
1)流程
(1)主線程main thread執(zhí)行阻塞Accept,每次客戶端Connect鏈接過(guò)來(lái),main thread中accept響應(yīng)并建立連接。
(2)創(chuàng)建鏈接成功,得到Connfd1套接字后,創(chuàng)建一個(gè)新線程thread1用來(lái)處理客戶端的讀寫業(yè)務(wù)。main thead依然回到Accept阻塞等待新客戶端。
(3)thread1通過(guò)套接字Connfd1與客戶端進(jìn)行通信讀寫。
(4)server在(2)處理業(yè)務(wù)中,如果有新客戶端Connect過(guò)來(lái),main thread中Accept依然響應(yīng)并建立連接,重復(fù)(2)過(guò)程,如圖1.19所示。
圖1.19 網(wǎng)絡(luò)并發(fā)模型二:單線程Accept+多線程讀寫-2
以上是模型二的服務(wù)端整體執(zhí)行邏輯,我們來(lái)分析一下模型一的優(yōu)缺點(diǎn):
2)優(yōu)點(diǎn)
基于模型一:單線程Accept(無(wú)IO復(fù)用) 支持了并發(fā)的特性。使用靈活,一個(gè)客戶端對(duì)應(yīng)一個(gè)線程單獨(dú)處理,server處理業(yè)務(wù)內(nèi)聚程度高,客戶端無(wú)論如何寫,服務(wù)端均會(huì)有一個(gè)線程做資源響應(yīng)。
3)缺點(diǎn)
隨著客戶端的數(shù)量增多,需要開(kāi)辟的線程也增加,客戶端與server線程數(shù)量1:1正比關(guān)系,一次對(duì)于高并發(fā)場(chǎng)景,線程數(shù)量收到硬件上限瓶頸。對(duì)于長(zhǎng)鏈接,客戶端一旦無(wú)業(yè)務(wù)讀寫,只要不關(guān)閉,server的對(duì)應(yīng)線程依然需要保持連接(心跳、健康監(jiān)測(cè)等機(jī)制),占用連接資源和線程開(kāi)銷資源浪費(fèi)。僅適合客戶端數(shù)量不大,并且數(shù)量可控的場(chǎng)景使用。僅適合學(xué)習(xí)基本socket編程,不適合任何服務(wù)器Server構(gòu)建。
3 模型三、單線程多路IO復(fù)用
1)流程
模型三是在單線程的基礎(chǔ)上添加多路IO復(fù)用機(jī)制,這樣就減少了多開(kāi)銷線程的弊端,模型三的流程如下:
(1)主線程main thread創(chuàng)建 listenFd 之后,采用多路I/O復(fù)用機(jī)制(如:select、epoll)進(jìn)行IO狀態(tài)阻塞監(jiān)控。有Client1客戶端Connect請(qǐng)求,I/O復(fù)用機(jī)制檢測(cè)到ListenFd觸發(fā)讀事件,則進(jìn)行Accept建立連接,并將新生成的connFd1加入到監(jiān)聽(tīng)I(yíng)/O集合中,如圖1.20所示。
圖1.20 網(wǎng)絡(luò)并發(fā)模型三:單線程Accept多路IO復(fù)用-1
(2)Client1再次進(jìn)行正常讀寫業(yè)務(wù)請(qǐng)求,main thread的多路I/O復(fù)用機(jī)制阻塞返回,會(huì)觸該套接字的讀/寫事件等,如圖1.21所示。
圖1.21 網(wǎng)絡(luò)并發(fā)模型三:單線程Accept多路IO復(fù)用-2
(3)對(duì)于Client1的讀寫業(yè)務(wù),Server依然在main thread執(zhí)行流程提繼續(xù)執(zhí)行,此時(shí)如果有新的客戶端Connect鏈接請(qǐng)求過(guò)來(lái),Server將沒(méi)有及時(shí)響應(yīng),如圖1.22所示。
圖1.22 網(wǎng)絡(luò)并發(fā)模型三:單線程Accept多路IO復(fù)用-3
(4)等到Server處理完一個(gè)連接的Read+Write操作,繼續(xù)回到多路I/O復(fù)用機(jī)制阻塞,其他鏈接過(guò)來(lái)重復(fù)(2)、(3)流程。
以上是模型二的服務(wù)端整體執(zhí)行邏輯,我們來(lái)分析一下模型一的優(yōu)缺點(diǎn):
2)優(yōu)點(diǎn)
單流程解決了可以同時(shí)監(jiān)聽(tīng)多個(gè)客戶端讀寫狀態(tài)的模型,不需要1:1與客戶端的線程數(shù)量關(guān)系。多路I/O復(fù)用阻塞,非忙詢狀態(tài),不浪費(fèi)CPU資源, CPU利用率較高。
3)缺點(diǎn)
雖然可以監(jiān)聽(tīng)多個(gè)客戶端的讀寫狀態(tài),但是同一時(shí)間內(nèi),只能處理一個(gè)客戶端的讀寫操作,實(shí)際上讀寫的業(yè)務(wù)并發(fā)為1。多客戶端訪問(wèn)Server,業(yè)務(wù)為串行執(zhí)行,大量請(qǐng)求會(huì)有排隊(duì)延遲現(xiàn)象,當(dāng)Client3占據(jù)main thread流程時(shí),Client1,Client2流程卡在IO復(fù)用等待下次監(jiān)聽(tīng)觸發(fā)事件。
4 模型四-單線程多路IO復(fù)用+多線程讀寫業(yè)務(wù)(業(yè)務(wù)工作池)
模型四是基于模型三的一種改進(jìn)版,但是趕緊的地方是在處理應(yīng)用層消息業(yè)務(wù)本身,將這部分承擔(dān)的壓力交給了一個(gè)Worker Pool工作池來(lái)處理。
1)流程
(1)主線程main thread創(chuàng)建listenFd之后,采用多路I/O復(fù)用機(jī)制(如:select、epoll)進(jìn)行IO狀態(tài)阻塞監(jiān)控。有Client1客戶端Connect請(qǐng)求,I/O復(fù)用機(jī)制檢測(cè)到ListenFd觸發(fā)讀事件,則進(jìn)行Accept建立連接,并將新生成的connFd1加入到監(jiān)聽(tīng)I(yíng)/O集合中,如圖1.23所示。
圖1.23 網(wǎng)絡(luò)并發(fā)模型四:單線程多路IO復(fù)用+業(yè)務(wù)工作池-1
(2)當(dāng)connFd1有可讀消息,觸發(fā)讀事件,并且進(jìn)行讀寫消息。
(3)main thread按照固定的協(xié)議讀取消息,并且交給worker pool工作線程池, 工作線程池在server啟動(dòng)之前就已經(jīng)開(kāi)啟固定數(shù)量的thread,里面的線程只處理消息業(yè)務(wù),不進(jìn)行套接字讀寫操作,如圖1.24所示。
圖1.24 網(wǎng)絡(luò)并發(fā)模型四:單線程多路IO復(fù)用+業(yè)務(wù)工作池-2
(4)工作池處理完業(yè)務(wù),觸發(fā)connFd1寫事件,將回執(zhí)客戶端的消息通過(guò)main thead寫給對(duì)方,如圖1.25所示。
圖1.25 網(wǎng)絡(luò)并發(fā)模型四:單線程多路IO復(fù)用+業(yè)務(wù)工作池-3
那么接下來(lái)Client2的讀寫請(qǐng)求的邏輯就是重復(fù)上述(1)-(4)的過(guò)程,一般我們把這種基于消息事件的業(yè)務(wù)層處理的線程稱之為業(yè)務(wù)工作池,如圖1.26所示。
圖1.26 網(wǎng)絡(luò)并發(fā)模型四:單線程多路IO復(fù)用+業(yè)務(wù)工作池-4
以上是模型四的服務(wù)端整體執(zhí)行邏輯,我們來(lái)分析一下模型一的優(yōu)缺點(diǎn):
2)優(yōu)點(diǎn)
對(duì)于模型三, 將業(yè)務(wù)處理部分,通過(guò)工作池分離出來(lái),減少多客戶端訪問(wèn)Server,業(yè)務(wù)為串行執(zhí)行,大量請(qǐng)求會(huì)有排隊(duì)延遲時(shí)間。實(shí)際上讀寫的業(yè)務(wù)并發(fā)為1,但是業(yè)務(wù)流程并發(fā)為worker pool線程數(shù)量,加快了業(yè)務(wù)處理并行效率。
3)缺點(diǎn)
讀寫依然為main thread單獨(dú)處理,最高讀寫并行通道依然為1。雖然多個(gè)worker線程處理業(yè)務(wù),但是最后返回給客戶端,依舊需要排隊(duì),因?yàn)槌隹谶€是main thread的Read + Write。
5 模型五:單線程IO復(fù)用+多線程IO復(fù)用(鏈接線程池)
模型五是單線程IO復(fù)用機(jī)制上再加上多線程的IO復(fù)用機(jī)制,看上去很繁瑣,但是這種模型確實(shí)當(dāng)下最通用和高效的解決方案。
1)流程
(1)Server在啟動(dòng)監(jiān)聽(tīng)之前,開(kāi)辟固定數(shù)量(N)的線程,用Thead Pool線程池管理,如圖1.27所示。
圖1.27 網(wǎng)絡(luò)并發(fā)模型五:單線程多路IO復(fù)用+多線程IO復(fù)用-1
(2)主線程main thread創(chuàng)建listenFd之后,采用多路I/O復(fù)用機(jī)制(如:select、epoll)進(jìn)行IO狀態(tài)阻塞監(jiān)控。有Client1客戶端Connect請(qǐng)求,I/O復(fù)用機(jī)制檢測(cè)到ListenFd觸發(fā)讀事件,則進(jìn)行Accept建立連接,并將新生成的connFd1分發(fā)給Thread Pool中的某個(gè)線程進(jìn)行監(jiān)聽(tīng)。
(3) Thread Pool中的每個(gè)thread都啟動(dòng)多路I/O復(fù)用機(jī)制(select、epoll),用來(lái)監(jiān)聽(tīng)main thread建立成功并且分發(fā)下來(lái)的socket套接字。
圖1.28 網(wǎng)絡(luò)并發(fā)模型五:單線程多路IO復(fù)用+多線程IO復(fù)用-2
(4)如圖1.28所示, thread監(jiān)聽(tīng)ConnFd1、ConnFd2, thread2監(jiān)聽(tīng)ConnFd3,thread3監(jiān)聽(tīng)ConnFd4. 當(dāng)對(duì)應(yīng)的ConnFd有讀寫事件,對(duì)應(yīng)的線程處理該套接字的讀寫及業(yè)務(wù)。
所以我們將這些固定承擔(dān)epoll多路IO監(jiān)控的線程集合,稱之為線程池,如圖1.29所示。
圖1.29 網(wǎng)絡(luò)并發(fā)模型五:單線程多路IO復(fù)用+多線程IO復(fù)用-3
以上是模型五的服務(wù)端整體執(zhí)行邏輯,我們來(lái)分析一下模型一的優(yōu)缺點(diǎn)。
2)優(yōu)點(diǎn)
將main thread的單流程讀寫,分散到多線程完成,這樣增加了同一時(shí)刻的讀寫并行通道,并行通道數(shù)量N, N為線程池Thread數(shù)量。server同時(shí)監(jiān)聽(tīng)的ConnFd套接字?jǐn)?shù)量幾乎成倍增大,之前的全部監(jiān)控?cái)?shù)量取決于main thread的多路I/O復(fù)用機(jī)制的最大限制 (select 默認(rèn)為1024, epoll默認(rèn)與內(nèi)存大小相關(guān),約3~6w不等),所以理論單點(diǎn)Server最高響應(yīng)并發(fā)數(shù)量為N*(3~6W)(N為線程池Thread數(shù)量,建議與CPU核心成比例1:1)。如果良好的線程池?cái)?shù)量和CPU核心數(shù)適配,那么可以嘗試CPU核心與Thread進(jìn)行綁定,從而降低CPU的切換頻率,提升每個(gè)Thread處理合理業(yè)務(wù)的效率,降低CPU切換成本開(kāi)銷。
3)缺點(diǎn)
雖然監(jiān)聽(tīng)的并發(fā)數(shù)量提升,但是最高讀寫并行通道依然為N,而且多個(gè)身處同一個(gè)Thread的客戶端,會(huì)出現(xiàn)讀寫延遲現(xiàn)象,實(shí)際上每個(gè)Thread的模型特征與模型三:單線程多路IO復(fù)用一致。
6 模型五(進(jìn)程版):單進(jìn)程多路I/O復(fù)用+多進(jìn)程IO復(fù)用
模型五進(jìn)程版和模型五的流程大致一樣,這里的區(qū)別是由線程池更編程進(jìn)程池,如圖1.30所示。
圖1.30 網(wǎng)絡(luò)并發(fā)模型五-進(jìn)程池版本-1
在模型五進(jìn)程版要注意的是,進(jìn)程之間的資源都是獨(dú)立的,所以當(dāng)有客戶端(如:Client1)建立請(qǐng)求的時(shí)候,main process(主進(jìn)程)的IO復(fù)用會(huì)監(jiān)聽(tīng)到ListenFd的可讀事件,如果在線程模型中,可以直接Accept將鏈接創(chuàng)建,并且將新創(chuàng)建的ConnFd交給線程中的某個(gè)線程中的IO復(fù)用機(jī)制來(lái)監(jiān)控,因?yàn)榫€程與線程中資源是共享的。但是在多進(jìn)程中則不能這么做。Main Process如果進(jìn)行Accept得到的ConnFd并不能傳遞給子進(jìn)程,因?yàn)樗麄兌加懈髯缘奈募枋龇蛄?。所以在多進(jìn)程版本,主進(jìn)程listenFd觸發(fā)讀事件,應(yīng)該由主進(jìn)程發(fā)送信號(hào)告知子進(jìn)程目前有新的鏈接可以建立,最終應(yīng)該由某個(gè)子進(jìn)程來(lái)進(jìn)行Accept完成鏈接建立過(guò)程,同時(shí)得到與客戶端通信的套接字ConnFd。最終在用自己的多路IO復(fù)用機(jī)制來(lái)監(jiān)聽(tīng)當(dāng)前進(jìn)程創(chuàng)建的ConnFd。
圖1.31 網(wǎng)絡(luò)并發(fā)模型五-進(jìn)程池版本-2
如圖1.31所示,進(jìn)程版與“模型五-單線程IO復(fù)用+多線程IO復(fù)用(鏈接線程池)”無(wú)大差異。
1)不同
(1)進(jìn)程和線程的內(nèi)存布局不同導(dǎo)致,main process(主進(jìn)程)不再進(jìn)行Accept操作,而是將Accept過(guò)程分散到各個(gè)子進(jìn)程(process)中。
(2)進(jìn)程的特性,資源獨(dú)立,所以main process如果Accept成功的fd,其他進(jìn)程無(wú)法共享資源,所以需要各子進(jìn)程自行Accept創(chuàng)建鏈接。
(3)main process只是監(jiān)聽(tīng)ListenFd狀態(tài),一旦觸發(fā)讀事件(有新連接請(qǐng)求)。通過(guò)一些IPC(進(jìn)程間通信:如信號(hào)、共享內(nèi)存、管道)等, 讓各自子進(jìn)程Process競(jìng)爭(zhēng)Accept完成鏈接建立,并各自監(jiān)聽(tīng)。
2)優(yōu)缺點(diǎn)
與五、單線程IO復(fù)用+多線程IO復(fù)用(鏈接線程池)無(wú)大差異。多進(jìn)程內(nèi)存資源空間占用稍微大一些,多進(jìn)程模型安全穩(wěn)定型較強(qiáng),這也是因?yàn)楦髯赃M(jìn)程互不干擾的特點(diǎn)導(dǎo)致。
7 模型六:單線程多路I/O復(fù)用+多線程I/O復(fù)用+多線程
本小節(jié)介紹一個(gè)更加復(fù)雜的模型六,我們?cè)诨谀P臀迳显偌由弦粋€(gè)多線程處理讀寫,服務(wù)端邏輯如下。
1)流程
(1)Server在啟動(dòng)監(jiān)聽(tīng)之前,開(kāi)辟固定數(shù)量(N)的線程,用Thead Pool線程池管理,如圖1.32所示。
圖1.32 網(wǎng)絡(luò)并發(fā)模型六-1
(2)主線程main thread創(chuàng)建listenFd之后,采用多路I/O復(fù)用機(jī)制(如:select、epoll)進(jìn)行IO狀態(tài)阻塞監(jiān)控。有Client1客戶端Connect請(qǐng)求,I/O復(fù)用機(jī)制檢測(cè)到ListenFd觸發(fā)讀事件,則進(jìn)行Accept建立連接,并將新生成的connFd1分發(fā)給Thread Pool中的某個(gè)線程進(jìn)行監(jiān)聽(tīng),如圖1.33所示。
圖10.33 網(wǎng)絡(luò)并發(fā)模型六-2
(3)Thread Pool中的每個(gè)thread都啟動(dòng)多路I/O復(fù)用機(jī)制(select、epoll),用來(lái)監(jiān)聽(tīng)main thread建立成功并且分發(fā)下來(lái)的socket套接字。一旦其中某個(gè)被監(jiān)聽(tīng)的客戶端套接字觸發(fā)I/O讀寫事件,那么,會(huì)立刻開(kāi)辟一個(gè)新線程來(lái)處理I/O讀寫業(yè)務(wù),如圖1.34所示。
圖1.34 網(wǎng)絡(luò)并發(fā)模型六-3
(4)但某個(gè)讀寫線程完成當(dāng)前讀寫業(yè)務(wù),如果當(dāng)前套接字沒(méi)有被關(guān)閉,那么將當(dāng)前客戶端套接字(如:ConnFd3)重新加回線程池的監(jiān)控線程中,同時(shí)自身線程自我銷毀。
以上是模型六的處理邏輯,我們來(lái)分析一下他的優(yōu)缺點(diǎn):
2)優(yōu)點(diǎn)
在模型五、單線程IO復(fù)用+多線程IO復(fù)用(鏈接線程池)基礎(chǔ)上,除了能夠保證同時(shí)響應(yīng)的最高并發(fā)數(shù),又能解決讀寫并行通道局限的問(wèn)題。同一時(shí)刻的讀寫并行通道,達(dá)到最大化極限,一個(gè)客戶端可以對(duì)應(yīng)一個(gè)單獨(dú)執(zhí)行流程處理讀寫業(yè)務(wù),讀寫并行通道與客戶端數(shù)量1:1關(guān)系,如圖1.35所示。
圖1.35 網(wǎng)絡(luò)并發(fā)模型六-4
3)缺點(diǎn)
該模型過(guò)于理想化,因?yàn)橐驝PU核心數(shù)量足夠大。如果硬件CPU數(shù)量可數(shù)(目前的硬件情況),那么該模型將造成大量的CPU切換成本浪費(fèi)。因?yàn)闉榱吮WC讀寫并行通道與客戶端1:1的關(guān)系,那么Server需要開(kāi)辟的Thread數(shù)量就與客戶端一致,那么線程池中做多路I/O復(fù)用的監(jiān)聽(tīng)線程池綁定CPU數(shù)量將變得毫無(wú)意義。如果每個(gè)臨時(shí)的讀寫Thread都能夠綁定一個(gè)單獨(dú)的CPU,那么此模型將是最優(yōu)模型。但是目前CPU的數(shù)量無(wú)法與客戶端的數(shù)量達(dá)到一個(gè)量級(jí),目前甚至差的不是幾個(gè)量級(jí)的事。
七、小結(jié)
本章我們首先介紹了多路IO復(fù)用機(jī)制解決的問(wèn)題,以及Epoll的常用接口和基本的結(jié)構(gòu)分析,那么在基于IO復(fù)用機(jī)制的理論基礎(chǔ)上,我們整理了七種Server服務(wù)器并發(fā)處理結(jié)構(gòu)模型,每個(gè)模型都有各自的特點(diǎn)和優(yōu)勢(shì),那么對(duì)于多少應(yīng)付高并發(fā)和高CPU利用率的模型,目前多數(shù)采用的是模型五(或模型五進(jìn)程版,如Nginx就是類似模型五進(jìn)程版的改版)。
至于并發(fā)模型并非設(shè)計(jì)的越復(fù)雜越好,也不是線程開(kāi)辟得越多越好,我們要考慮硬件的利用與和切換成本的開(kāi)銷。模型六設(shè)計(jì)就極為復(fù)雜,線程較多,但以當(dāng)今的硬件能力無(wú)法支撐,反倒導(dǎo)致該模型性能極差。所以對(duì)于不同的業(yè)務(wù)場(chǎng)景也要選擇適合的模型構(gòu)建,并不是一定固定就要使用某個(gè)來(lái)應(yīng)用。