一、基礎(chǔ)概念
1. socket
??socket也稱作“套接字”,用于描述IP地址和端口,是一個(gè)通信鏈路的描述符。應(yīng)用程序通常通過(guò)“套接字”向?qū)Χ税l(fā)出請(qǐng)求或者應(yīng)答網(wǎng)絡(luò)請(qǐng)求。
??socket是連接運(yùn)行在網(wǎng)絡(luò)上的兩個(gè)程序之間的通信端點(diǎn)。通信的兩端都有socket,它是一個(gè)通道,數(shù)據(jù)在兩個(gè)socket之間進(jìn)行傳輸。socket把復(fù)雜的TCP/IP協(xié)議族或者UDP/IP協(xié)議族隱藏在socket接口后面,對(duì)程序員來(lái)說(shuō),只要用好socket相關(guān)的函數(shù),就可以完成數(shù)據(jù)通信。
2. FD:file descriptor**
? ??Linux 系統(tǒng)中,把一切都看做是文件(一切皆文件),當(dāng)進(jìn)程打開(kāi)現(xiàn)有文件或創(chuàng)建新文件時(shí),內(nèi)核向進(jìn)程 返回一個(gè)文件描述符,文件描述符就是內(nèi)核為了高效管理已被打開(kāi)的文件所創(chuàng)建的索引,用來(lái)指向被打開(kāi)的文 件,所有執(zhí)行I/O操作的系統(tǒng)調(diào)用都會(huì)通過(guò)文件描述符。
? ??一個(gè)進(jìn)程能夠同時(shí)打開(kāi)多個(gè)文件,對(duì)應(yīng)需要多個(gè)文件描述符,所以需要用一個(gè)文件描述符表對(duì)文件描述符 進(jìn)行管理;通常默認(rèn)大小為1024,也即能容納1024個(gè)文件描述符;
? ??在 soket 通信中,當(dāng)我們調(diào)用內(nèi)核函數(shù)創(chuàng)建 socket 后,內(nèi)核返回給我們的是 socket 對(duì)應(yīng)的文件描述符( fd),所以我們對(duì) socket 的操作基本都是通過(guò) fd 來(lái)進(jìn)行。這個(gè)文件描述符 fd 可能是就緒的狀態(tài)即大于 1 的整 數(shù),也可能是未就緒的狀態(tài)-1。就緒狀態(tài)表示客戶端或服務(wù)端發(fā)送的全部數(shù)據(jù)通過(guò)網(wǎng)卡進(jìn)入到內(nèi)核緩沖區(qū)buf, 這時(shí)調(diào)用 系統(tǒng)read( )就可以將數(shù)據(jù)從內(nèi)核區(qū)拷貝到用戶緩沖區(qū)。
??見(jiàn)阻塞 IO 圖。
3. 內(nèi)核態(tài)和用戶態(tài)
? ??線程是操作系統(tǒng)調(diào)度CPU的最小單元,在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的計(jì)數(shù)器、 堆棧和局部變量等屬性,并且能夠訪問(wèn)共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者感覺(jué)到這些 線程在同時(shí)執(zhí)行。線程的實(shí)現(xiàn)可以分為兩類:用戶級(jí)線程,內(nèi)核線線程。(java線程就是內(nèi)核級(jí)線程)
? ??虛擬內(nèi)存被操作系統(tǒng)劃分成兩塊:內(nèi)核空間和用戶空間,內(nèi)核空間是內(nèi)核代碼運(yùn)行的地方,用戶空間是用 戶程序代碼運(yùn)行的地方。當(dāng)進(jìn)程運(yùn)行在內(nèi)核空間時(shí)就處于內(nèi)核態(tài),當(dāng)進(jìn)程運(yùn)行在用戶空間時(shí)就處于用戶態(tài),為 了安全,它們是隔離的,即使用戶的程序崩潰了,內(nèi)核也不受影響。說(shuō)起這個(gè)概念就是因?yàn)榫€程上下文切換的 概念。雖然線程上下文切換比進(jìn)程切換成本要低但是,線程切換也是很影響性能的。線程上下文切換就涉及用 戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換。
二、 IO 模型分類
??服務(wù)器端編程經(jīng)常需要構(gòu)造高性能的IO模型,
1. 常見(jiàn)的IO模型
-
同步阻塞IO(Blocking IO):即傳統(tǒng)的IO模型。
-
同步非阻塞IO(Non-blocking IO):默認(rèn)創(chuàng)建的socket都是阻塞的,非阻塞IO要求socket被設(shè)置為NONBLOCK。
-
IO多路復(fù)用(IO Multiplexing):即經(jīng)典的Reactor模式(并非23種設(shè)計(jì)模式之一),有時(shí)也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。epoll有兩種工作方式,一種是LT模式(level trigger,水平觸發(fā)),事件驅(qū)動(dòng),內(nèi)核告訴你一個(gè)文件描述符是否就緒,然后你可以對(duì)就緒的文件描述符進(jìn)行IO操作。一種是ET模式(edge trigger,邊緣觸發(fā))。默認(rèn)情況下是LT模式。
-
異步IO(Asynchronous IO):即經(jīng)典的Proactor設(shè)計(jì)模式,也稱為異步非阻塞IO。
2. 阻塞和非阻塞
?? 阻塞和非阻塞的概念描述的是用戶線程調(diào)用內(nèi)核IO操作的方式,主體是IO:
? ??阻塞是指IO操作需要徹底完成后才返回到用戶空間;
? ??而非阻塞是指IO操作被調(diào)用后立即返回給用戶一個(gè)狀態(tài)值,無(wú)需等到IO操作徹底完成。
? ??阻塞非阻塞的主體是IO。
3. 同步和異步
??同步和異步的概念描述的是用戶線程與內(nèi)核的交互方式,主體是線程:
? ??同步是指用戶線程發(fā)起IO請(qǐng)求后需要等待或者輪詢內(nèi)核IO操作完成后才能繼續(xù)執(zhí)行;
? ??而異步是指用戶線程發(fā)起IO請(qǐng)求后仍繼續(xù)執(zhí)行,當(dāng)內(nèi)核IO操作完成后通知用戶線程,或者調(diào)用用戶線程注冊(cè)的回調(diào)函數(shù)。
? ??同步異步的主體是用戶線程。
三、 阻塞IO
??為了方便理解,以下所有代碼都是偽代碼,知道其表達(dá)的意思即可。
??服務(wù)端為了處理客戶端的連接和請(qǐng)求的數(shù)據(jù),寫了如下代碼。
服務(wù)端 客戶端
listenfd = socket(); 打開(kāi)一個(gè)網(wǎng)絡(luò)通信端口
bind(listenfd); 綁定
listen(listenfd); 監(jiān)聽(tīng)
while(1) { fd = socket();
connfd = accept(listenfd); 阻塞建立連接 connect(fd);
write(fd,buf);
int n = read(connfd, buf); 阻塞讀數(shù)據(jù) closed(fd);
doSomeThing(buf); 利用讀到的數(shù)據(jù)做些什么
close(connfd); 關(guān)閉連接,循環(huán)等待下一個(gè)連接
}
??服務(wù)端的線程阻塞在了兩個(gè)地方,一個(gè)是 accept 函數(shù),一個(gè)是 read 函數(shù)。
??read函數(shù)包括兩個(gè)階段:
??這就是傳統(tǒng)的阻塞 IO。如果這個(gè)連接的客戶端一直不發(fā)數(shù)據(jù),那么服務(wù)端線程將會(huì)一直阻塞在 read 函數(shù)上不返回,也無(wú)法接受其他客戶端連接。
四、非阻塞 IO
??為了解決上面的問(wèn)題,我們需要對(duì) accept函數(shù)和 read 函數(shù)進(jìn)行改造。
1、針對(duì) read 函數(shù)造成的阻塞
? ??這個(gè) read 函數(shù)的效果是,如果沒(méi)有數(shù)據(jù)到達(dá)內(nèi)核緩沖區(qū)時(shí),即第一階段未完成,立刻返回一個(gè)錯(cuò)誤值-1,而不是阻塞地等待。
? ??操作系統(tǒng)提供了這樣的功能,只需要在調(diào)用 read 前,將文件描述符設(shè)置為非阻塞即可。
服務(wù)端
listenfd = socket(); 打開(kāi)一個(gè)網(wǎng)絡(luò)通信端口
bind(listenfd); 綁定
listen(listenfd); 監(jiān)聽(tīng)
while(1) {
connfd = accept(listenfd); 阻塞建立連接
fcntl(connfd, F_SETFL, O_NONBLOCK); 將文件描述符設(shè)置為非阻塞
int n = read(connfd, buffer); 如果 fd 未就緒,調(diào)用 read 會(huì)立即返回-1,處理下 一個(gè)連接
doSomeThing(buf);
close(connfd);
}
??如果 fd 未就緒,調(diào)用 read 會(huì)立即返回-1,如果就緒,就會(huì)阻塞式的 read。
2、針對(duì) accept函數(shù)造成的阻塞(IO 多路復(fù)用)
? ??有一種聰明的辦法是,每次都創(chuàng)建一個(gè)新的進(jìn)程或線程,去調(diào)用 read 函數(shù),并做業(yè)務(wù)處理。
while(1) {
connfd = accept(listenfd); // 阻塞建立連接
pthread_create(doWork); // 創(chuàng)建一個(gè)新的線程
}
void doWork() {
int n = read(connfd, buf); // 阻塞讀數(shù)據(jù)
doSomeThing(buf); // 利用讀到的數(shù)據(jù)做些什么
close(connfd); // 關(guān)閉連接,循環(huán)等待下一個(gè)連接
}
??這樣,當(dāng)給一個(gè)客戶端建立好連接后,就可以立刻等待新的客戶端連接,而不用阻塞在原客戶端的 read 請(qǐng)求上。不過(guò),這不叫非阻塞 IO,只不過(guò)用了多線程的手段使得主線程沒(méi)有卡在 read 函數(shù)上不往下走罷了。操作系統(tǒng)為我們提供的 read 函數(shù)仍然是阻塞的。
??為每個(gè)客戶端創(chuàng)建一個(gè)線程,服務(wù)器端的線程資源很容易被耗光。
??當(dāng)然還有個(gè)聰明的辦法,我們可以每 accept 一個(gè)客戶端連接后,將這個(gè)文件描述符(connfd)放到一個(gè)數(shù)組里。然后弄一個(gè)新的線程去不斷遍歷這個(gè)數(shù)組,調(diào)用每一個(gè)元素的非阻塞 read 方法。
服務(wù)端
listenfd = socket(); 打開(kāi)一個(gè)網(wǎng)絡(luò)通信端口
bind(listenfd); 綁定
listen(listenfd); 監(jiān)聽(tīng)
fdlist;
while(1) {
connfd = accept(listenfd); 阻塞建立連接
fcntl(connfd, F_SETFL, O_NONBLOCK); 將文件描述符設(shè)置為非阻塞
fdlist.add(connfd);
}
新線程去處理
while(1) {
for(fd <-- fdlist) {
if(read(fd) != -1) {
doSomeThing();
}
close(fd);
//移除此 fd
}
}
??這樣,我們就成功用一個(gè)線程處理了多個(gè)客戶端連接。
? ??但這和我們用多線程去將阻塞 IO 改造,看起來(lái)是 一樣的,這種遍歷方式也是我們用戶層的小把戲,每次遍 歷遇到 read 返回 -1 時(shí)仍然是一次浪費(fèi)資源的系統(tǒng)調(diào)用。
??使用 while 循環(huán)不斷地做系統(tǒng)調(diào)用,是不合理的。每次只傳給 read 函數(shù)一個(gè)文件描述符,傳一次調(diào)用一次。
??我們每次傳給 read 函數(shù)一批文件描述符到內(nèi)核,由內(nèi)核層去遍歷,這個(gè)問(wèn)題才能真正解決。
2.1 select 模型
? ??select 是操作系統(tǒng)提供的系統(tǒng)調(diào)用函數(shù),通過(guò)它,我們可以把一個(gè)文件描述符的數(shù)組發(fā)給操作系統(tǒng), 讓 操作系統(tǒng)去遍歷,確定哪個(gè)文件描述符可以讀寫, 然后告訴我們?nèi)ヌ幚?。不過(guò),當(dāng) select 函數(shù)返回后,用 戶依然需要遍歷剛剛提交給操作系統(tǒng)的 list。只不過(guò),操作系統(tǒng)會(huì)將準(zhǔn)備就緒的文件描述符做上標(biāo)識(shí),用戶 層將不會(huì)再有無(wú)意義的系統(tǒng)調(diào)用開(kāi)銷。
服務(wù)端
listenfd = socket(); 打開(kāi)一個(gè)網(wǎng)絡(luò)通信端口
bind(listenfd); 綁定
listen(listenfd); 監(jiān)聽(tīng)
fdlist;
首先一個(gè)線程不斷接受客戶端連接,并把 socket 文件描述符放到一個(gè) list 里。
while(1) {
connfd = accept(listenfd); 阻塞建立連接
fcntl(connfd, F_SETFL, O_NONBLOCK); 將文件描述符設(shè)置為非阻塞
fdlist.add(connfd);
}
while(1) {
// 把一堆文件描述符 list 傳給 select 函數(shù)
// 有已就緒的文件描述符就返回,nready 表示有多少個(gè)就緒的
nready = select(list);
// 用戶層依然要遍歷,只不過(guò)少了很多無(wú)效的系統(tǒng)調(diào)用
for(fd <-- fdlist) {
if(fd != -1) {
// 只讀已就緒的文件描述符
read(fd, buf);
// 總共只有 nready 個(gè)已就緒描述符,不用過(guò)多遍歷
if(--nready == 0)
break;
}
}
}
??存在問(wèn)題:
-
select 調(diào)用需要傳入 fd 數(shù)組,需要拷貝一份到內(nèi)核,高并發(fā)場(chǎng)景下這樣的拷貝消耗的資源是驚人的。(可優(yōu)化為不復(fù)制)
-
select 在內(nèi)核層仍然是通過(guò)遍歷的方式檢查文件描述符的就緒狀態(tài),是個(gè)同步過(guò)程,只不過(guò)無(wú)系統(tǒng)調(diào)用切換上下文的開(kāi)銷。(內(nèi)核層可優(yōu)化為異步事件通知)
-
select 僅僅返回可讀文件描述符的個(gè)數(shù),具體哪個(gè)可讀還是要用戶自己遍歷。(可優(yōu)化為只返回給用戶就緒的文件描述符,無(wú)需用戶做無(wú)效的遍歷)
2.2 poll模型
? ??它和 select 的主要區(qū)別就是,去掉了 select 只能監(jiān)聽(tīng) 1024 個(gè)文件描述符的限制。
2.3 epoll模型
? ??針對(duì)select 模型的三個(gè)問(wèn)題進(jìn)行了改進(jìn)。
-
? 內(nèi)核中保存一份文件描述符集合,無(wú)需用戶每次都重新傳入,只需告訴內(nèi)核修改的部分即可。
-
? 內(nèi)核不再通過(guò)輪詢的方式找到就緒的文件描述符,而是通過(guò)異步 IO 事件喚醒。
-
? 內(nèi)核僅會(huì)將有 IO 事件的文件描述符返回給用戶,用戶也無(wú)需遍歷整個(gè)文件描述符集合。
?
? ??具體,操作系統(tǒng)提供了這三個(gè)函數(shù)。
//第一步,創(chuàng)建一個(gè) epoll 句柄
int epoll_create(int size);
//第二步,向內(nèi)核添加、修改或刪除要監(jiān)控的文件描述符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//第三步,類似發(fā)起了 select() 調(diào)用
int epoll_wait(int epfd, struct epoll_event *events, int max events, int timeout);
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-427817.html
參考文章https://www.dandelioncloud.cn/article/details/1615702819904651265文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-427817.html
到了這里,關(guān)于網(wǎng)絡(luò)模型與 IO 多路復(fù)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!