Linux中的幾種IO模型詳解
一、五種IO模型
1.1 高效IO的初步理解
IO其實(shí)就是“input”和“output”尤其在網(wǎng)絡(luò)部分,IO的特性非常明顯!!
如果是在本地文件,本質(zhì)上就是將數(shù)據(jù)寫(xiě)到內(nèi)核文件緩沖區(qū),具體什么時(shí)候刷到磁盤(pán)上,是由OS決定的??!而在網(wǎng)絡(luò)中,本質(zhì)上也是將數(shù)據(jù)寫(xiě)到發(fā)送緩沖區(qū),但是具體什么時(shí)候發(fā)送,也是由OS決定的!!
所以應(yīng)用層進(jìn)行read或write的時(shí)候,本質(zhì)上是把數(shù)據(jù)從用戶層寫(xiě)給OS!這也是IO的本質(zhì)!read和write函數(shù)的本質(zhì)其實(shí)就是拷貝函數(shù)!!
但是拷貝并不是一定能立馬執(zhí)行的!比如說(shuō)read的時(shí)候,如果我的接收緩沖區(qū)沒(méi)有數(shù)據(jù),我得阻塞,而write的時(shí)候,我的發(fā)送緩沖區(qū)滿了,那么我也得阻塞!!
所以要進(jìn)行拷貝!必須要先判斷讀寫(xiě)事件是否就緒!!
IO=等+拷貝
問(wèn)題1:什么是讀寫(xiě)事件呢??
——>你想讀就得等讀事件就緒,就是接收緩沖區(qū)有數(shù)據(jù),想寫(xiě)就得等寫(xiě)事件就緒,就是等發(fā)送緩沖區(qū)要有足夠多的空間,想讀就是得讀事件就緒,以上統(tǒng)稱讀寫(xiě)事件就緒!
問(wèn)題2:什么是高效的IO呢??
——>任何IO過(guò)程中, 都包含兩個(gè)步驟. 第一是等待, 第二是拷貝. 而且在實(shí)際的應(yīng)用場(chǎng)景中, 等待消耗的時(shí)間往 往都遠(yuǎn)遠(yuǎn)高于拷貝的時(shí)間. 讓IO更高效, 最核心的辦法就是單位時(shí)間內(nèi)等待時(shí)間的比重減少!
問(wèn)題3:怎么理解等的比重減少呢?
——>比如說(shuō)你當(dāng)前是單進(jìn)程,如果讀寫(xiě)時(shí)間沒(méi)有就緒就會(huì)阻塞住,只會(huì)等一個(gè)文件描述符,而如果是多線程,他可以等待多個(gè)文件描述符,此時(shí)的IO等待時(shí)間不是串型的而是并行的!!
1.2 用“釣魚(yú)”理解五種IO模型
接下來(lái)我們就要介紹五種IO模型,什么叫模型呢??其實(shí)就是規(guī)律,未來(lái)不管是讀文件還是寫(xiě)文件都離不開(kāi)其中一種!!
釣魚(yú)=等+釣(可以比喻IO)
1、張三(新手) 拿著自己的魚(yú)漂(用來(lái)主動(dòng)檢測(cè)讀寫(xiě)事件是否就緒) 魚(yú)竿(相當(dāng)于文件描述符) 魚(yú)鉤坐在椅子上,然后一下鉤就死死盯著魚(yú)漂, 魚(yú)漂不動(dòng)張三也不動(dòng),誰(shuí)找他喊他他都不回應(yīng) 直到魚(yú)上鉤 ----這是阻塞式IO(策略是在內(nèi)核將數(shù)據(jù)準(zhǔn)備好之前, 系統(tǒng)調(diào)用會(huì)一直等待所有的套接字, 默認(rèn)都是阻塞方式.)
2、李四(有兩三年釣魚(yú)經(jīng)驗(yàn),坐不?。┖皬埲?,張三不理他 他也就坐在那釣魚(yú)了 但是他比較坐不住,他會(huì)每隔一段時(shí)間檢查一下魚(yú)漂,不會(huì)一直死死盯著,其他時(shí)間他會(huì)把視線轉(zhuǎn)移到自己的手機(jī)上刷抖音,所以他檢測(cè)的時(shí)候如果檢測(cè)不到就會(huì)立刻做自己的事情 不會(huì)一直死盯 檢測(cè)條件就緒了才釣魚(yú) ——這是非阻塞等待IO(策略是如果內(nèi)核還未將數(shù)據(jù)準(zhǔn)備好, 系統(tǒng)調(diào)用仍然會(huì)直接返回, 并且返回EWOULDBLOCK錯(cuò)誤碼.)
非阻塞IO往往需要程序員循環(huán)的方式反復(fù)嘗試讀寫(xiě)文件描述符, 這個(gè)過(guò)程稱為輪詢. 這對(duì)CPU來(lái)說(shuō)是較大的浪費(fèi), 一 般只有特定場(chǎng)景下才使用.
3、王五 (有五年釣魚(yú)經(jīng)驗(yàn)) 他看張三和李四一個(gè)一直動(dòng),一個(gè)一動(dòng)不動(dòng),覺(jué)得他們是菜鳥(niǎo),他也跟著釣魚(yú)了,然后他在魚(yú)竿上綁了一個(gè)鈴鐺 然后他就把魚(yú)竿插起來(lái)不管了 直接躺在旁邊玩手機(jī) 基本不關(guān)注魚(yú)竿,直接等鈴鐺響 他才會(huì)去把魚(yú)釣上來(lái)。 我們會(huì)發(fā)現(xiàn)張三和李四是主動(dòng)去檢測(cè)的 而王五的方式就是我不會(huì)主動(dòng)檢測(cè),就是魚(yú)上鉤了會(huì)自己通知我 ——信號(hào)驅(qū)動(dòng)式IO(策略是內(nèi)核將數(shù)據(jù)準(zhǔn)備好的時(shí)候, 使用SIGIO信號(hào)通知應(yīng)用程序進(jìn)行IO操作. )
4、趙六(富豪、好勝) 所以他拉了一卡車(chē)的魚(yú)竿 把所有的魚(yú)竿都插起來(lái) 然后他會(huì)來(lái)回走動(dòng)檢測(cè)(周期性遍歷)哪邊有魚(yú)上鉤 ——這就是多路轉(zhuǎn)接(策略最核心在于IO多路轉(zhuǎn)接能夠同時(shí)等待多個(gè)文件描述符的就緒狀態(tài))
5、田七(世界首富 但是不是很專業(yè)) 司機(jī)開(kāi)車(chē)帶著他經(jīng)過(guò)河邊的時(shí)候,他發(fā)現(xiàn)河邊有4個(gè)非常奇怪的人 釣魚(yú)的姿勢(shì)形態(tài)各異 于是他就很好奇 也想去釣魚(yú) 然后突然公司打電話要開(kāi)緊急會(huì)議 可是他又想吃魚(yú) 于是他就把司機(jī)小王叫了過(guò)來(lái) 說(shuō)我要去開(kāi)會(huì) 你幫我釣魚(yú) 等你釣滿一桶了打電話給我 我再讓人來(lái)接你
田七并不是喜歡釣魚(yú) 他是釣魚(yú)行為的發(fā)起者 他要的是魚(yú)(數(shù)據(jù)) 田七這種方式叫做——異步IO (由內(nèi)核在數(shù)據(jù)拷貝完成時(shí), 通知應(yīng)用程序)
因?yàn)樾⊥踉卺烎~(yú)的時(shí)候 他正在開(kāi)會(huì) 此時(shí)的小王就相當(dāng)于是OS 桶就相當(dāng)于是一段緩沖區(qū),電話就相當(dāng)于是一種通知方式 他將IO工作交給了OS 由OS自動(dòng)去檢測(cè)然后將數(shù)據(jù)放在緩沖區(qū)里 等緩沖區(qū)滿了就通知你來(lái)取 田七在應(yīng)用層用就可以了,田七并不參與具體的IO過(guò)程 而前四種方式就叫做同步IO
問(wèn)題1:為什么趙六效率最高呢??拿到魚(yú)竿多效率及高么??
——>假設(shè)你是一條魚(yú) 你看到旁邊這么多魚(yú)竿 你會(huì)咬哪一個(gè)呢??顯然趙六釣到魚(yú)的機(jī)會(huì)最大,因?yàn)槎鄠€(gè)魚(yú)竿可以讓我們每一個(gè)等待的過(guò)程在時(shí)間上是并行重疊的??!所以整體上等的比重就減少了!!!
問(wèn)題2:阻塞IOvs非阻塞IO
——>阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時(shí)的狀態(tài).
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起. 調(diào)用線程只有在得到結(jié)果之后才會(huì)返回.
非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會(huì)阻塞當(dāng)前線程.
在效率方面沒(méi)有任何區(qū)別(因?yàn)镮O=等+拷貝 大家的區(qū)別只是等的方式不同),我們一般說(shuō)非阻塞效率會(huì)高一點(diǎn)不是IO效率高 而是他在非阻塞輪詢的時(shí)候可以做其他的事情
問(wèn)題3:王五有等嗎??
——>王五也算一種等?。∫蝗凰麨槭裁床恢苯踊丶夷???就算我們說(shuō)他沒(méi)等,魚(yú)咬鉤的時(shí)候他也要參與釣魚(yú)的過(guò)程(IO) 只要有參與IO,就一定有同步的過(guò)程,所以也是同步IO
問(wèn)題4:同步IOVS 異步IO
——>同步IO就是有參與O的過(guò)程,而異步IO就只是發(fā)起IO,但是并不參與IO的過(guò)程,OS完成IO后會(huì)通知上層拿結(jié)果,然后直接用就行了!!
問(wèn)題5:同步通信vs異步通信
——>同步和異步關(guān)注的是消息通信機(jī)制.
所謂同步,就是在發(fā)出一個(gè)調(diào)用時(shí),在沒(méi)有得到結(jié)果之前,該調(diào)用就不返回. 但是一旦調(diào)用返回,就得到返回值了; 換句話說(shuō),就是由調(diào)用者主動(dòng)等待這個(gè)調(diào)用的結(jié)果;
異步則是相反,調(diào)用在發(fā)出之后,這個(gè)調(diào)用就直接返回了,所以沒(méi)有返回結(jié)果; 換句話說(shuō),當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不會(huì)立刻得到結(jié)果; 而是在調(diào)用發(fā)出后,被調(diào)用者通過(guò)狀態(tài)、通知來(lái)通知調(diào)用者,或通過(guò)回調(diào)函數(shù)處理這個(gè)調(diào)用.
問(wèn)題6:同步IOVS 線程同步
——>他倆就是老婆和老婆餅的關(guān)系(毫無(wú)關(guān)聯(lián)?。絀O是IO層面的概念,而線程同步是兩個(gè)線程誰(shuí)先誰(shuí)后的問(wèn)題!!所以以后在看到 "同步" 這個(gè)詞, 一定要先搞清楚大背景是什么. 這個(gè)同步, 是同步通信異步通信的同步, 還是同步 與互斥的同步.
問(wèn)題7:異步IO效率不高呢??為什么實(shí)際場(chǎng)景多路轉(zhuǎn)接用的多?
——>田七再厲害也只有一套裝備 而且異步IO寫(xiě)出來(lái)的服務(wù)邏輯比較混亂 所以現(xiàn)在已經(jīng)有很多方法(比如協(xié)程)在逐步取代異步IO了 所以這里最值得我們學(xué)習(xí)的是多路轉(zhuǎn)接和非阻塞!!
問(wèn)題8:異步IOvs信號(hào)驅(qū)動(dòng)
——>異步IO是由OS完成拷貝的過(guò)程然后通知上層,而信號(hào)驅(qū)動(dòng)是告訴上層可以進(jìn)行拷貝了!
問(wèn)題9:其他高級(jí)IO
——>非阻塞IO,紀(jì)錄鎖,系統(tǒng)V流機(jī)制,I/O多路轉(zhuǎn)接(也叫I/O多路復(fù)用),readv和writev函數(shù)以及存儲(chǔ)映射IO(mmap),這些統(tǒng)稱為高級(jí)IO.
二、非阻塞輪詢
我們會(huì)發(fā)現(xiàn)以上接口有一個(gè)flag參數(shù),我們可以通過(guò)設(shè)置來(lái)讓該事件以非阻塞輪詢的方式來(lái)訪問(wèn)套接字,但是這種方法太麻煩了!!
因?yàn)槲覀冏x寫(xiě)本質(zhì)就是讀寫(xiě)文件描述符指向的文件緩沖區(qū),而文件描述符本質(zhì)上是下標(biāo),所以更通用的做法就是把文件描述符屬性設(shè)置成非阻塞(其實(shí)就是他指向的文件對(duì)象struct file里面的一個(gè)標(biāo)志位)告訴內(nèi)核這個(gè)文件描述符我們要以非阻塞的方式來(lái)操作!
2.1fcntl
一個(gè)文件描述符, 默認(rèn)都是阻塞IO.
int fcntl(int fd, int cmd, ... /* arg */ );
傳入的cmd的值不同, 后面追加的參數(shù)也不相同. fcntl函數(shù)有5種功能:
- 復(fù)制一個(gè)現(xiàn)有的描述符(cmd=F_DUPFD).
- 獲得/設(shè)置文件描述符標(biāo)記(cmd=F_GETFD或F_SETFD).
- 獲得/設(shè)置文件狀態(tài)標(biāo)記(cmd=F_GETFL或F_SETFL).
- 獲得/設(shè)置異步I/O所有權(quán)(cmd=F_GETOWN或F_SETOWN).
- 獲得/設(shè)置記錄鎖(cmd=F_GETLK,F_SETLK或F_SETLKW).
我們此處只是用第三種功能, 獲取/設(shè)置文件狀態(tài)標(biāo)記, 就可以將一個(gè)文件描述符設(shè)置為非阻塞.
2.2 實(shí)現(xiàn)函數(shù)SetNoBlock
基于fcntl, 我們實(shí)現(xiàn)一個(gè)SetNoBlock函數(shù), 將文件描述符設(shè)置為非阻塞.
void SetNonBlock(int fd) { int fl = fcntl(fd, F_GETFL); if (fl < 0) { perror("fcntl"); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK); cout << " set " << fd << " nonblock done" << endl; }
使用F_GETFL將當(dāng)前的文件描述符的屬性取出來(lái)(這是一個(gè)位圖).
然后再使用F_SETFL將文件描述符設(shè)置回去. 設(shè)置回去的同時(shí), 加上一個(gè)O_NONBLOCK參數(shù).
2.3 輪詢方式讀取標(biāo)準(zhǔn)輸入
int main() { char buffer[1024]; SetNonBlock(0); sleep(1); while (true) { // printf("Please Enter# "); // fflush(stdout); ssize_t n = read(0, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n - 1] = 0; cout << "echo : " << buffer << endl; } else if (n == 0) { cout << "read done" << endl; break; } else { // 1. 設(shè)置成為非阻塞,如果底層fd數(shù)據(jù)沒(méi)有就緒,recv/read/write/send, 返回值會(huì)以出錯(cuò)的形式返回 // 2. a. 真的出錯(cuò) b. 底層沒(méi)有就緒 // 3. 我怎么區(qū)分呢?通過(guò)errno區(qū)分!?。? if (errno == EWOULDBLOCK) { cout << "0 fd data not ready, try again!" << endl; // do_other_thing(); sleep(1); } else { cerr << "read error, n = " << n << "errno code: " << errno << ", error str: " << strerror(errno) << endl; } // TODO 信號(hào)中斷IO? } } return 0; }
問(wèn)題:如果將文件描述符設(shè)置為非阻塞了,如果底層fd數(shù)據(jù)沒(méi)有就緒,recv/read/write/send,返回值會(huì)以出錯(cuò)(-1)的返回,為什么呢??
——>因?yàn)樗麑?shí)在沒(méi)辦法了!!>0表示成功,=0表示關(guān)閉,那么只能是<0了
所以此時(shí)<0有兩種情況(1)真的出錯(cuò)了 (2)底層讀寫(xiě)事件沒(méi)有就緒
那我怎么區(qū)分呢??所以規(guī)定在返回-1的時(shí)候會(huì)設(shè)置錯(cuò)誤碼,我們可以通過(guò)錯(cuò)誤碼去判斷!
因此一旦被設(shè)置為非阻塞了,那么返回-1情況在分類(lèi)討論的時(shí)候還需要根據(jù)錯(cuò)誤碼加一層判斷,不能直接break。
當(dāng)然我們也可以寫(xiě)一個(gè)函數(shù)讓他在輪詢的期間去做點(diǎn)別的事情!
三、select-多路轉(zhuǎn)接
以前我們學(xué)到的大多數(shù)接口是既等又IO,而現(xiàn)在我們可以用一個(gè)select專門(mén)用來(lái)等,并且他一次可以等待多個(gè)文件描述符,從而在等的時(shí)間上實(shí)現(xiàn)并行??!
3.1 select介紹
系統(tǒng)提供select函數(shù)來(lái)實(shí)現(xiàn)多路復(fù)用輸入/輸出模型
select系統(tǒng)調(diào)用是用來(lái)讓我們的程序監(jiān)視多個(gè)文件描述符的狀態(tài)變化的;
程序會(huì)停在select這里等待,直到被監(jiān)視的文件描述符有一個(gè)或多個(gè)發(fā)生了狀態(tài)改變;
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3.1.1 參數(shù)解釋
- nfds:是需要監(jiān)視的最大的文件描述符值+1;
因?yàn)槲募枋龇窍聵?biāo),所以可以理解為監(jiān)聽(tīng)的文件描述符的范圍。
- rdset,wrset,exset:分別對(duì)應(yīng)于需要檢測(cè)的 可讀文件描述符的集合,可寫(xiě)文件描述符的集合及異常文件描述符的集合;
輸入輸出型參數(shù)!設(shè)置時(shí)表示要監(jiān)聽(tīng)的文件描述符,返回時(shí)由內(nèi)核設(shè)置,表示已經(jīng)就緒的文件描述符
- timeout:結(jié)構(gòu)timeval,用來(lái)設(shè)置select()的等待時(shí)間
輸入輸出型參數(shù)! 比如等待時(shí)間是5s,如果2秒就有文件描述符就緒了,那么就會(huì)返回3秒
3.1.2 關(guān)于timeval結(jié)構(gòu)體
timeval結(jié)構(gòu)用于描述一段時(shí)間長(zhǎng)度,如果在這個(gè)時(shí)間內(nèi),需要監(jiān)視的描述符沒(méi)有事件發(fā)生則函數(shù)返回,返回值為0。
關(guān)于取值:
- NULL:則表示select()沒(méi)有timeout,select將一直被阻塞,直到某個(gè)文件描述符上發(fā)生了事件;
- 0:僅檢測(cè)描述符集合的狀態(tài),然后立即返回,并不等待外部事件的發(fā)生。
- 特定的時(shí)間值:如果在指定的時(shí)間段里沒(méi)有事件發(fā)生,select將超時(shí)返回。(第一個(gè)為單位s,第二個(gè)單位為ms)
3.1.3 關(guān)于fd_set結(jié)構(gòu)體
這個(gè)結(jié)構(gòu)是由內(nèi)核提供的一種數(shù)據(jù)類(lèi)型,其實(shí)就是一個(gè)整數(shù)數(shù)組, 更嚴(yán)格的說(shuō), 是一個(gè) "位圖". 使用位圖中對(duì)應(yīng)的位來(lái)表示要監(jiān)視的文件描述符. (用來(lái)給用戶和內(nèi)核做溝通)
他是一個(gè)輸入輸出型參數(shù)??!
- 輸入時(shí),由用戶告訴內(nèi)核:我給你的一個(gè)或者多個(gè)fd,你要幫我關(guān)心上面的事件哦!如果就緒了你一定要告訴我哈??!
- 輸出時(shí),由內(nèi)核告訴用戶:你讓我關(guān)心的多個(gè)fd中,有一些已經(jīng)就緒了哦,用戶你趕緊讀取吧
所以使用select注定一定有大量位圖操作!
用戶:這個(gè)位圖由我自己來(lái)操作嗎??
OS說(shuō):你還是別直接操作了吧,你連他的結(jié)構(gòu)都沒(méi)搞清楚,還是讓我來(lái)給你提供一批操作位圖的接口吧??!所以提供了一組操作fd_set的接口, 來(lái)比較方便的操作位圖,
void FD_CLR(int fd, fd_set *set); // 用來(lái)清除描述詞組set中相關(guān)fd的位 int FD_ISSET(int fd, fd_set *set); // 用來(lái)測(cè)試描述詞組set中相關(guān)fd的位是否為真 void FD_SET(int fd, fd_set *set); // 用來(lái)設(shè)置描述詞組set中相關(guān)fd的位 void FD_ZERO(fd_set *set); // 用來(lái)清除描述詞組set的全部位
3.1.4 函數(shù)返回值
執(zhí)行成功則返回文件描述詞狀態(tài)已改變的個(gè)數(shù)
如果返回0代表在描述詞狀態(tài)改變前已超過(guò)timeout時(shí)間,沒(méi)有返回
當(dāng)有錯(cuò)誤發(fā)生時(shí)則返回-1,錯(cuò)誤原因存于errno,此時(shí)參數(shù)readfds,writefds, exceptfds和timeout的 值變成不可預(yù)測(cè)。
錯(cuò)誤值可能為:
- EBADF 文件描述詞為無(wú)效的或該文件已關(guān)閉
- EINTR 此調(diào)用被信號(hào)所中斷
- EINVAL 參數(shù)n 為負(fù)值。
- ENOMEM 核心內(nèi)存不足
3.2 理解select執(zhí)行過(guò)程
理解select模型的關(guān)鍵在于理解fd_set,為說(shuō)明方便,取fd_set長(zhǎng)度為1字節(jié),fd_set中的每一bit可以對(duì)應(yīng)一個(gè)文件描述符fd。則1字節(jié)長(zhǎng)的fd_set最大可以對(duì)應(yīng)8個(gè)fd.
(1)執(zhí)行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執(zhí)行FD_SET(fd,&set);
后set變?yōu)?001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變?yōu)?001,0011
(4)執(zhí)行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發(fā)生可讀事件,則select返回,此時(shí)set變?yōu)?000,0011。注意:沒(méi)有事件發(fā)生的fd=5被清空。
3.3socket就緒條件
讀就緒
- socket內(nèi)核中, 接收緩沖區(qū)中的字節(jié)數(shù), 大于等于低水位標(biāo)記SO_RCVLOWAT. 此時(shí)可以無(wú)阻塞的讀該文件描述符, 并且返回值大于0;
- socket TCP通信中, 對(duì)端關(guān)閉連接, 此時(shí)對(duì)該socket讀, 則返回0;
- 監(jiān)聽(tīng)的socket上有新的連接請(qǐng)求;
- socket上有未處理的錯(cuò)誤;
寫(xiě)就緒
- socket內(nèi)核中, 發(fā)送緩沖區(qū)中的可用字節(jié)數(shù)(發(fā)送緩沖區(qū)的空閑位置大小), 大于等于低水位標(biāo)記SO_SNDLOWAT, 此時(shí)可以無(wú)阻塞的寫(xiě), 并且返回值大于0;
- socket的寫(xiě)操作被關(guān)閉(close或者shutdown). 對(duì)一個(gè)寫(xiě)操作被關(guān)閉的socket進(jìn)行寫(xiě)操作, 會(huì)觸發(fā)SIGPIPE信號(hào);
- socket使用非阻塞connect連接成功或失敗之后;
- socket上有未讀取的錯(cuò)誤;
異常就緒
socket上收到帶外數(shù)據(jù). 關(guān)于帶外數(shù)據(jù), 和TCP緊急模式相關(guān)(回憶TCP協(xié)議頭中, 有一個(gè)緊急指針的字段),
3.4 通過(guò)編碼深入理解
Socket.hpp
#pragma once #include <iostream> #include <string> #include <unistd.h> #include <cstring> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include "Log.hpp" enum { SocketErr = 2, BindErr, ListenErr, }; // TODO const int backlog = 10; class Sock { public: Sock() { } ~Sock() { } public: void Socket() { sockfd_ = socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ < 0) { lg(Fatal, "socker error, %s: %d", strerror(errno), errno); exit(SocketErr); } int opt = 1; setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); } void Bind(uint16_t port) { struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) { lg(Fatal, "bind error, %s: %d", strerror(errno), errno); exit(BindErr); } } void Listen() { if (listen(sockfd_, backlog) < 0) { lg(Fatal, "listen error, %s: %d", strerror(errno), errno); exit(ListenErr); } } int Accept(std::string *clientip, uint16_t *clientport) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len); if(newfd < 0) { lg(Warning, "accept error, %s: %d", strerror(errno), errno); return -1; } char ipstr[64]; inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); *clientip = ipstr; *clientport = ntohs(peer.sin_port); return newfd; } bool Connect(const std::string &ip, const uint16_t &port) { struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr)); int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer)); if(n == -1) { std::cerr << "connect to " << ip << ":" << port << " error" << std::endl; return false; } return true; } void Close() { close(sockfd_); } int Fd() { return sockfd_; } private: int sockfd_; };
log.hpp
#pragma once #include <iostream> #include <time.h> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile "log.txt" class Log { public: Log() { printMethod = Screen; path = "./log/"; } void Enable(int method) { printMethod = method; } std::string levelToString(int level) { switch (level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } // void logmessage(int level, const char *format, ...) // { // time_t t = time(nullptr); // struct tm *ctime = localtime(&t); // char leftbuffer[SIZE]; // snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), // ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, // ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // // va_list s; // // va_start(s, format); // char rightbuffer[SIZE]; // vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // // va_end(s); // // 格式:默認(rèn)部分+自定義部分 // char logtxt[SIZE * 2]; // snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer); // // printf("%s", logtxt); // 暫時(shí)打印 // printLog(level, logtxt); // } void printLog(int level, const std::string &logtxt) { switch (printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } void printOneFile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt" if (fd < 0) return; write(fd, logtxt.c_str(), logtxt.size()); close(fd); } void printClassFile(int level, const std::string &logtxt) { std::string filename = LogFile; filename += "."; filename += levelToString(level); // "log.txt.Debug/Warning/Fatal" printOneFile(filename, logtxt); } ~Log() { } void operator()(int level, const char *format, ...) { time_t t = time(nullptr); struct tm *ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); // 格式:默認(rèn)部分+自定義部分 char logtxt[SIZE * 2]; snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); // printf("%s", logtxt); // 暫時(shí)打印 printLog(level, logtxt); } private: int printMethod; std::string path; }; Log lg; // int sum(int n, ...) // { // va_list s; // char* // va_start(s, n); // int sum = 0; // while(n) // { // sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123); // n--; // } // va_end(s); //s = NULL // return sum; // }
Makefile:
select_server:Main.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f select_server
SelectServer.hpp:
#pragma once #include <iostream> #include <sys/select.h> #include <sys/time.h> #include "Socket.hpp" using namespace std; static const uint16_t defaultport = 8888; static const int fd_num_max = (sizeof(fd_set) * 8); int defaultfd = -1; class SelectServer { public: SelectServer(uint16_t port = defaultport) : _port(port) { for (int i = 0; i < fd_num_max; i++) { fd_array[i] = defaultfd; // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl; } } bool Init() { _listensock.Socket(); _listensock.Bind(_port); _listensock.Listen(); return true; } void Accepter() { // 我們的連接事件就緒了 std::string clientip; uint16_t clientport = 0; int sock = _listensock.Accept(&clientip, &clientport); // 會(huì)不會(huì)阻塞在這里?不會(huì) if (sock < 0) return; lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock); // sock -> fd_array[] int pos = 1; for (; pos < fd_num_max; pos++) // 第二個(gè)循環(huán) { if (fd_array[pos] != defaultfd) continue; else break; } if (pos == fd_num_max) { lg(Warning, "server is full, close %d now!", sock); close(sock); } else { fd_array[pos] = sock; PrintFd(); // TODO } } void Recver(int fd, int pos) { // demo char buffer[1024]; ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug? if (n > 0) { buffer[n] = 0; cout << "get a messge: " << buffer << endl; } else if (n == 0) { lg(Info, "client quit, me too, close fd is : %d", fd); close(fd); fd_array[pos] = defaultfd; // 這里本質(zhì)是從select中移除 } else { lg(Warning, "recv error: fd is : %d", fd); close(fd); fd_array[pos] = defaultfd; // 這里本質(zhì)是從select中移除 } } void Dispatcher(fd_set &rfds) { for (int i = 0; i < fd_num_max; i++) // 這是第三個(gè)循環(huán) { int fd = fd_array[i]; if (fd == defaultfd) continue; if (FD_ISSET(fd, &rfds)) { if (fd == _listensock.Fd()) { Accepter(); // 連接管理器 } else // non listenfd { Recver(fd, i); } } } } void Start() { int listensock = _listensock.Fd(); fd_array[0] = listensock; for (;;) { fd_set rfds; FD_ZERO(&rfds); int maxfd = fd_array[0]; for (int i = 0; i < fd_num_max; i++) // 第一次循環(huán) { if (fd_array[i] == defaultfd) continue; FD_SET(fd_array[i], &rfds); if (maxfd < fd_array[i]) { maxfd = fd_array[i]; lg(Info, "max fd update, max fd is: %d", maxfd); } } // accept?不能直接accept!檢測(cè)并獲取listensock上面的事件,新連接到來(lái),等價(jià)于讀事件就緒 // struct timeval timeout = {1, 0}; // 輸入輸出,可能要進(jìn)行周期的重復(fù)設(shè)置 struct timeval timeout = {0, 0}; // 輸入輸出,可能要進(jìn)行周期的重復(fù)設(shè)置 // 如果事件就緒,上層不處理,select會(huì)一直通知你! // select告訴你就緒了,接下來(lái)的一次讀取,我們讀取fd的時(shí)候,不會(huì)被阻塞 // rfds: 輸入輸出型參數(shù)。 1111 1111 -> 0000 0000 int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr); switch (n) { case 0: cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl; break; case -1: cerr << "select error" << endl; break; default: // 有事件就緒了,TODO cout << "get a new link!!!!!" << endl; Dispatcher(rfds); // 就緒的事件和fd你怎么知道只有一個(gè)呢??? break; } } } void PrintFd() { cout << "online fd list: "; for (int i = 0; i < fd_num_max; i++) { if (fd_array[i] == defaultfd) continue; cout << fd_array[i] << " "; } cout << endl; } ~SelectServer() { _listensock.Close(); } private: Sock _listensock; uint16_t _port; int fd_array[fd_num_max]; // 數(shù)組, 用戶維護(hù)的! // int wfd_array[fd_num_max]; };
Main.cc
#include "SelectServer.hpp" #include <memory> int main() { // std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl; std::unique_ptr<SelectServer> svr(new SelectServer()); svr->Init(); svr->Start(); return 0; }
注意事項(xiàng):
1、不能直接aceept,因?yàn)樗蟛糠謺r(shí)間都在等,一次只能等一個(gè)文件描述符?。。╨istensock上面的時(shí)間是新鏈接到來(lái),就是三次握手完成,鏈接投遞到全連接隊(duì)列里,然后你再通過(guò)accept把鏈接從底層拿上來(lái)),所以新鏈接來(lái)了相當(dāng)于是讀事件就緒!!
2、 定義fd_set類(lèi)型變量如果是在棧上定義,可能會(huì)出現(xiàn)亂碼,所以在使用前要記得先清空!!
3、因?yàn)閠imeout是輸入輸出型參數(shù)!!所以返回之后可能已經(jīng)修改過(guò)了??!所以為了維持他的效果我們就必須周期性重復(fù)設(shè)置!!
4、因?yàn)椋?)rfds是一個(gè)輸入輸出型參數(shù),每次都會(huì)被重新設(shè)置,且隨著不斷獲取新鏈接,套接字的數(shù)量會(huì)越來(lái)越多!不能寫(xiě)死,應(yīng)是動(dòng)態(tài)計(jì)算 (2)select不僅僅要等lisentsock,也要等讀的sock
因此需要有一個(gè)輔助數(shù)組arrry來(lái)監(jiān)控select中的fd,他不僅可以方便我們
(1)將文件描述符信息在不同函數(shù)之間的傳遞
(2)用于在select 返回后,array作為源數(shù)據(jù)和fd_set進(jìn)行FD_ISSET判斷。。
(3)select返回后會(huì)把以前加入的但并無(wú)事件發(fā)生的fd清空,則每次開(kāi)始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時(shí)取得fd最大值maxfd,用于select的第一個(gè)參數(shù)。
(4)讓左側(cè)是監(jiān)聽(tīng)套接字,右側(cè)是讀套接字
5、輔助數(shù)組里有鏈接就緒和讀就緒,我們?cè)趺磪^(qū)分呢??——>確認(rèn)就緒之后,再加一層判斷。證明自己是否是監(jiān)聽(tīng)套接字。
6、關(guān)于Dispatcher(事件派發(fā)器),就是收到了多個(gè)就緒的文件描述符,然后跟array進(jìn)行判斷并派發(fā),如果是連接就緒就交給連接事件處理,如果是讀就緒就交給讀事件處理。
因?yàn)榫途w的時(shí)間不一定只有一個(gè),所以必須要循環(huán)去遍歷!
7、關(guān)于recver,讀的時(shí)候不能直接讀,因?yàn)樽x的時(shí)候內(nèi)容可能不完整,這就涉及到了協(xié)議的內(nèi)容!
3.5select缺點(diǎn)
1、等待的fd是有上限的!!
可監(jiān)控的文件描述符個(gè)數(shù)取決與sizeof(fd_set)的值. 我這邊服務(wù)器上sizeof(fd_set)=512,每bit表示一個(gè)文件 描述符,則我服務(wù)器上支持的最大文件描述符是512*8=4096.
備注: fd_set的大小可以調(diào)整,可能涉及到重新編譯內(nèi)核
2、輸入輸出型參數(shù)比較多,數(shù)據(jù)拷貝的頻率很高,且每次都需要對(duì)關(guān)心的fd進(jìn)行重置
3、用戶層是,使用第三方數(shù)組管理用戶的fd,用戶層需要多次遍歷,內(nèi)核中檢測(cè)fd時(shí)間就緒也要遍歷。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Linux deepin 刪除多余內(nèi)核的實(shí)現(xiàn)方法
這篇文章主要介紹了Linux deepin 刪除多余內(nèi)核的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中
這篇文章主要介紹了linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中的相關(guān)資料,需要的朋友可以參考下2016-12-12logrotate實(shí)現(xiàn)日志切割方式(轉(zhuǎn)儲(chǔ))
這篇文章主要介紹了logrotate實(shí)現(xiàn)日志切割方式(轉(zhuǎn)儲(chǔ)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05linux中通過(guò)文件描述符獲取文件絕對(duì)路徑的方法
下面小編就為大家?guī)?lái)一篇linux中通過(guò)文件描述符獲取文件絕對(duì)路徑的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12ubuntu系統(tǒng)下apache配置虛擬主機(jī)及反向代理詳解
這篇文章主要介紹了ubuntu系統(tǒng)下apache配置虛擬主機(jī)及反向代理的相關(guān)資料,文中通過(guò)實(shí)例給大家演示的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-06-06Linux系統(tǒng)中獲取時(shí)間的方法總結(jié)
在Linux操作系統(tǒng)中,獲取時(shí)間是一個(gè)基本且重要的功能,本文旨在全面總結(jié)Linux系統(tǒng)中獲取時(shí)間的方法,包括命令行工具和編程接口,幫助讀者深入理解Linux時(shí)間管理的機(jī)制,需要的朋友可以參考下2025-03-03詳解Supervisor安裝與配置(Linux/Unix進(jìn)程管理工具)
這篇文章主要介紹了詳解Supervisor安裝與配置(Linux/Unix進(jìn)程管理工具),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06linux系統(tǒng)報(bào)tcp_mark_head_lost錯(cuò)誤的處理方法
這篇文章主要給大家介紹了關(guān)于linux系統(tǒng)報(bào)tcp_mark_head_lost錯(cuò)誤的處理方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用linux系統(tǒng)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07