Linux中的幾種IO模型詳解
一、五種IO模型
1.1 高效IO的初步理解
IO其實就是“input”和“output”尤其在網(wǎng)絡(luò)部分,IO的特性非常明顯!!
如果是在本地文件,本質(zhì)上就是將數(shù)據(jù)寫到內(nèi)核文件緩沖區(qū),具體什么時候刷到磁盤上,是由OS決定的??!而在網(wǎng)絡(luò)中,本質(zhì)上也是將數(shù)據(jù)寫到發(fā)送緩沖區(qū),但是具體什么時候發(fā)送,也是由OS決定的?。?/p>
所以應(yīng)用層進行read或write的時候,本質(zhì)上是把數(shù)據(jù)從用戶層寫給OS!這也是IO的本質(zhì)!read和write函數(shù)的本質(zhì)其實就是拷貝函數(shù)!!
但是拷貝并不是一定能立馬執(zhí)行的!比如說read的時候,如果我的接收緩沖區(qū)沒有數(shù)據(jù),我得阻塞,而write的時候,我的發(fā)送緩沖區(qū)滿了,那么我也得阻塞!!
所以要進行拷貝!必須要先判斷讀寫事件是否就緒!!
IO=等+拷貝
問題1:什么是讀寫事件呢??
——>你想讀就得等讀事件就緒,就是接收緩沖區(qū)有數(shù)據(jù),想寫就得等寫事件就緒,就是等發(fā)送緩沖區(qū)要有足夠多的空間,想讀就是得讀事件就緒,以上統(tǒng)稱讀寫事件就緒!
問題2:什么是高效的IO呢??
——>任何IO過程中, 都包含兩個步驟. 第一是等待, 第二是拷貝. 而且在實際的應(yīng)用場景中, 等待消耗的時間往 往都遠遠高于拷貝的時間. 讓IO更高效, 最核心的辦法就是單位時間內(nèi)等待時間的比重減少!
問題3:怎么理解等的比重減少呢?
——>比如說你當前是單進程,如果讀寫時間沒有就緒就會阻塞住,只會等一個文件描述符,而如果是多線程,他可以等待多個文件描述符,此時的IO等待時間不是串型的而是并行的!!
1.2 用“釣魚”理解五種IO模型
接下來我們就要介紹五種IO模型,什么叫模型呢??其實就是規(guī)律,未來不管是讀文件還是寫文件都離不開其中一種!!
釣魚=等+釣(可以比喻IO)
1、張三(新手) 拿著自己的魚漂(用來主動檢測讀寫事件是否就緒) 魚竿(相當于文件描述符) 魚鉤坐在椅子上,然后一下鉤就死死盯著魚漂, 魚漂不動張三也不動,誰找他喊他他都不回應(yīng) 直到魚上鉤 ----這是阻塞式IO(策略是在內(nèi)核將數(shù)據(jù)準備好之前, 系統(tǒng)調(diào)用會一直等待所有的套接字, 默認都是阻塞方式.)

2、李四(有兩三年釣魚經(jīng)驗,坐不?。┖皬埲?,張三不理他 他也就坐在那釣魚了 但是他比較坐不住,他會每隔一段時間檢查一下魚漂,不會一直死死盯著,其他時間他會把視線轉(zhuǎn)移到自己的手機上刷抖音,所以他檢測的時候如果檢測不到就會立刻做自己的事情 不會一直死盯 檢測條件就緒了才釣魚 ——這是非阻塞等待IO(策略是如果內(nèi)核還未將數(shù)據(jù)準備好, 系統(tǒng)調(diào)用仍然會直接返回, 并且返回EWOULDBLOCK錯誤碼.)

非阻塞IO往往需要程序員循環(huán)的方式反復(fù)嘗試讀寫文件描述符, 這個過程稱為輪詢. 這對CPU來說是較大的浪費, 一 般只有特定場景下才使用.
3、王五 (有五年釣魚經(jīng)驗) 他看張三和李四一個一直動,一個一動不動,覺得他們是菜鳥,他也跟著釣魚了,然后他在魚竿上綁了一個鈴鐺 然后他就把魚竿插起來不管了 直接躺在旁邊玩手機 基本不關(guān)注魚竿,直接等鈴鐺響 他才會去把魚釣上來。 我們會發(fā)現(xiàn)張三和李四是主動去檢測的 而王五的方式就是我不會主動檢測,就是魚上鉤了會自己通知我 ——信號驅(qū)動式IO(策略是內(nèi)核將數(shù)據(jù)準備好的時候, 使用SIGIO信號通知應(yīng)用程序進行IO操作. )

4、趙六(富豪、好勝) 所以他拉了一卡車的魚竿 把所有的魚竿都插起來 然后他會來回走動檢測(周期性遍歷)哪邊有魚上鉤 ——這就是多路轉(zhuǎn)接(策略最核心在于IO多路轉(zhuǎn)接能夠同時等待多個文件描述符的就緒狀態(tài))

5、田七(世界首富 但是不是很專業(yè)) 司機開車帶著他經(jīng)過河邊的時候,他發(fā)現(xiàn)河邊有4個非常奇怪的人 釣魚的姿勢形態(tài)各異 于是他就很好奇 也想去釣魚 然后突然公司打電話要開緊急會議 可是他又想吃魚 于是他就把司機小王叫了過來 說我要去開會 你幫我釣魚 等你釣滿一桶了打電話給我 我再讓人來接你
田七并不是喜歡釣魚 他是釣魚行為的發(fā)起者 他要的是魚(數(shù)據(jù)) 田七這種方式叫做——異步IO (由內(nèi)核在數(shù)據(jù)拷貝完成時, 通知應(yīng)用程序)

因為小王在釣魚的時候 他正在開會 此時的小王就相當于是OS 桶就相當于是一段緩沖區(qū),電話就相當于是一種通知方式 他將IO工作交給了OS 由OS自動去檢測然后將數(shù)據(jù)放在緩沖區(qū)里 等緩沖區(qū)滿了就通知你來取 田七在應(yīng)用層用就可以了,田七并不參與具體的IO過程 而前四種方式就叫做同步IO
問題1:為什么趙六效率最高呢??拿到魚竿多效率及高么??
——>假設(shè)你是一條魚 你看到旁邊這么多魚竿 你會咬哪一個呢??顯然趙六釣到魚的機會最大,因為多個魚竿可以讓我們每一個等待的過程在時間上是并行重疊的??!所以整體上等的比重就減少了??!!
問題2:阻塞IOvs非阻塞IO
——>阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時的狀態(tài).
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當前線程會被掛起. 調(diào)用線程只有在得到結(jié)果之后才會返回.
非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當前線程.
在效率方面沒有任何區(qū)別(因為IO=等+拷貝 大家的區(qū)別只是等的方式不同),我們一般說非阻塞效率會高一點不是IO效率高 而是他在非阻塞輪詢的時候可以做其他的事情
問題3:王五有等嗎??
——>王五也算一種等??!要不然他為什么不直接回家呢??就算我們說他沒等,魚咬鉤的時候他也要參與釣魚的過程(IO) 只要有參與IO,就一定有同步的過程,所以也是同步IO
問題4:同步IOVS 異步IO
——>同步IO就是有參與O的過程,而異步IO就只是發(fā)起IO,但是并不參與IO的過程,OS完成IO后會通知上層拿結(jié)果,然后直接用就行了!!
問題5:同步通信vs異步通信
——>同步和異步關(guān)注的是消息通信機制.
所謂同步,就是在發(fā)出一個調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用就不返回. 但是一旦調(diào)用返回,就得到返回值了; 換句話說,就是由調(diào)用者主動等待這個調(diào)用的結(jié)果;
異步則是相反,調(diào)用在發(fā)出之后,這個調(diào)用就直接返回了,所以沒有返回結(jié)果; 換句話說,當一個異步過程調(diào)用發(fā)出后,調(diào)用者不會立刻得到結(jié)果; 而是在調(diào)用發(fā)出后,被調(diào)用者通過狀態(tài)、通知來通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用.
問題6:同步IOVS 線程同步
——>他倆就是老婆和老婆餅的關(guān)系(毫無關(guān)聯(lián)?。絀O是IO層面的概念,而線程同步是兩個線程誰先誰后的問題?。∷砸院笤诳吹?"同步" 這個詞, 一定要先搞清楚大背景是什么. 這個同步, 是同步通信異步通信的同步, 還是同步 與互斥的同步.
問題7:異步IO效率不高呢??為什么實際場景多路轉(zhuǎn)接用的多?
——>田七再厲害也只有一套裝備 而且異步IO寫出來的服務(wù)邏輯比較混亂 所以現(xiàn)在已經(jīng)有很多方法(比如協(xié)程)在逐步取代異步IO了 所以這里最值得我們學習的是多路轉(zhuǎn)接和非阻塞??!
問題8:異步IOvs信號驅(qū)動
——>異步IO是由OS完成拷貝的過程然后通知上層,而信號驅(qū)動是告訴上層可以進行拷貝了!
問題9:其他高級IO
——>非阻塞IO,紀錄鎖,系統(tǒng)V流機制,I/O多路轉(zhuǎn)接(也叫I/O多路復(fù)用),readv和writev函數(shù)以及存儲映射IO(mmap),這些統(tǒng)稱為高級IO.
二、非阻塞輪詢


我們會發(fā)現(xiàn)以上接口有一個flag參數(shù),我們可以通過設(shè)置來讓該事件以非阻塞輪詢的方式來訪問套接字,但是這種方法太麻煩了?。?/p>
因為我們讀寫本質(zhì)就是讀寫文件描述符指向的文件緩沖區(qū),而文件描述符本質(zhì)上是下標,所以更通用的做法就是把文件描述符屬性設(shè)置成非阻塞(其實就是他指向的文件對象struct file里面的一個標志位)告訴內(nèi)核這個文件描述符我們要以非阻塞的方式來操作!

2.1fcntl
一個文件描述符, 默認都是阻塞IO.
int fcntl(int fd, int cmd, ... /* arg */ );
傳入的cmd的值不同, 后面追加的參數(shù)也不相同. fcntl函數(shù)有5種功能:
- 復(fù)制一個現(xiàn)有的描述符(cmd=F_DUPFD).
- 獲得/設(shè)置文件描述符標記(cmd=F_GETFD或F_SETFD).
- 獲得/設(shè)置文件狀態(tài)標記(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)標記, 就可以將一個文件描述符設(shè)置為非阻塞.
2.2 實現(xiàn)函數(shù)SetNoBlock
基于fcntl, 我們實現(xiàn)一個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將當前的文件描述符的屬性取出來(這是一個位圖).
然后再使用F_SETFL將文件描述符設(shè)置回去. 設(shè)置回去的同時, 加上一個O_NONBLOCK參數(shù).
2.3 輪詢方式讀取標準輸入
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ù)沒有就緒,recv/read/write/send, 返回值會以出錯的形式返回
// 2. a. 真的出錯 b. 底層沒有就緒
// 3. 我怎么區(qū)分呢?通過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 信號中斷IO?
}
}
return 0;
}問題:如果將文件描述符設(shè)置為非阻塞了,如果底層fd數(shù)據(jù)沒有就緒,recv/read/write/send,返回值會以出錯(-1)的返回,為什么呢??
——>因為他實在沒辦法了??!>0表示成功,=0表示關(guān)閉,那么只能是<0了
所以此時<0有兩種情況(1)真的出錯了 (2)底層讀寫事件沒有就緒
那我怎么區(qū)分呢??所以規(guī)定在返回-1的時候會設(shè)置錯誤碼,我們可以通過錯誤碼去判斷!

因此一旦被設(shè)置為非阻塞了,那么返回-1情況在分類討論的時候還需要根據(jù)錯誤碼加一層判斷,不能直接break。
當然我們也可以寫一個函數(shù)讓他在輪詢的期間去做點別的事情!
三、select-多路轉(zhuǎn)接
以前我們學到的大多數(shù)接口是既等又IO,而現(xiàn)在我們可以用一個select專門用來等,并且他一次可以等待多個文件描述符,從而在等的時間上實現(xiàn)并行??!
3.1 select介紹
系統(tǒng)提供select函數(shù)來實現(xiàn)多路復(fù)用輸入/輸出模型
select系統(tǒng)調(diào)用是用來讓我們的程序監(jiān)視多個文件描述符的狀態(tài)變化的;
程序會停在select這里等待,直到被監(jiān)視的文件描述符有一個或多個發(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;
因為文件描述符是下標,所以可以理解為監(jiān)聽的文件描述符的范圍。
- rdset,wrset,exset:分別對應(yīng)于需要檢測的 可讀文件描述符的集合,可寫文件描述符的集合及異常文件描述符的集合;
輸入輸出型參數(shù)!設(shè)置時表示要監(jiān)聽的文件描述符,返回時由內(nèi)核設(shè)置,表示已經(jīng)就緒的文件描述符
- timeout:結(jié)構(gòu)timeval,用來設(shè)置select()的等待時間
輸入輸出型參數(shù)! 比如等待時間是5s,如果2秒就有文件描述符就緒了,那么就會返回3秒
3.1.2 關(guān)于timeval結(jié)構(gòu)體
timeval結(jié)構(gòu)用于描述一段時間長度,如果在這個時間內(nèi),需要監(jiān)視的描述符沒有事件發(fā)生則函數(shù)返回,返回值為0。

關(guān)于取值:
- NULL:則表示select()沒有timeout,select將一直被阻塞,直到某個文件描述符上發(fā)生了事件;
- 0:僅檢測描述符集合的狀態(tài),然后立即返回,并不等待外部事件的發(fā)生。
- 特定的時間值:如果在指定的時間段里沒有事件發(fā)生,select將超時返回。(第一個為單位s,第二個單位為ms)
3.1.3 關(guān)于fd_set結(jié)構(gòu)體
這個結(jié)構(gòu)是由內(nèi)核提供的一種數(shù)據(jù)類型,其實就是一個整數(shù)數(shù)組, 更嚴格的說, 是一個 "位圖". 使用位圖中對應(yīng)的位來表示要監(jiān)視的文件描述符. (用來給用戶和內(nèi)核做溝通)


他是一個輸入輸出型參數(shù)?。?/strong>
- 輸入時,由用戶告訴內(nèi)核:我給你的一個或者多個fd,你要幫我關(guān)心上面的事件哦!如果就緒了你一定要告訴我哈?。?/li>
- 輸出時,由內(nèi)核告訴用戶:你讓我關(guān)心的多個fd中,有一些已經(jīng)就緒了哦,用戶你趕緊讀取吧
所以使用select注定一定有大量位圖操作!

用戶:這個位圖由我自己來操作嗎??
OS說:你還是別直接操作了吧,你連他的結(jié)構(gòu)都沒搞清楚,還是讓我來給你提供一批操作位圖的接口吧??!所以提供了一組操作fd_set的接口, 來比較方便的操作位圖,
void FD_CLR(int fd, fd_set *set); // 用來清除描述詞組set中相關(guān)fd的位 int FD_ISSET(int fd, fd_set *set); // 用來測試描述詞組set中相關(guān)fd的位是否為真 void FD_SET(int fd, fd_set *set); // 用來設(shè)置描述詞組set中相關(guān)fd的位 void FD_ZERO(fd_set *set); // 用來清除描述詞組set的全部位
3.1.4 函數(shù)返回值
執(zhí)行成功則返回文件描述詞狀態(tài)已改變的個數(shù)
如果返回0代表在描述詞狀態(tài)改變前已超過timeout時間,沒有返回
當有錯誤發(fā)生時則返回-1,錯誤原因存于errno,此時參數(shù)readfds,writefds, exceptfds和timeout的 值變成不可預(yù)測。
錯誤值可能為:
- EBADF 文件描述詞為無效的或該文件已關(guān)閉
- EINTR 此調(diào)用被信號所中斷
- EINVAL 參數(shù)n 為負值。
- ENOMEM 核心內(nèi)存不足
3.2 理解select執(zhí)行過程
理解select模型的關(guān)鍵在于理解fd_set,為說明方便,取fd_set長度為1字節(jié),fd_set中的每一bit可以對應(yīng)一個文件描述符fd。則1字節(jié)長的fd_set最大可以對應(yīng)8個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返回,此時set變?yōu)?000,0011。注意:沒有事件發(fā)生的fd=5被清空。
3.3socket就緒條件
讀就緒
- socket內(nèi)核中, 接收緩沖區(qū)中的字節(jié)數(shù), 大于等于低水位標記SO_RCVLOWAT. 此時可以無阻塞的讀該文件描述符, 并且返回值大于0;
- socket TCP通信中, 對端關(guān)閉連接, 此時對該socket讀, 則返回0;
- 監(jiān)聽的socket上有新的連接請求;
- socket上有未處理的錯誤;
寫就緒
- socket內(nèi)核中, 發(fā)送緩沖區(qū)中的可用字節(jié)數(shù)(發(fā)送緩沖區(qū)的空閑位置大小), 大于等于低水位標記SO_SNDLOWAT, 此時可以無阻塞的寫, 并且返回值大于0;
- socket的寫操作被關(guān)閉(close或者shutdown). 對一個寫操作被關(guān)閉的socket進行寫操作, 會觸發(fā)SIGPIPE信號;
- socket使用非阻塞connect連接成功或失敗之后;
- socket上有未讀取的錯誤;
異常就緒
socket上收到帶外數(shù)據(jù). 關(guān)于帶外數(shù)據(jù), 和TCP緊急模式相關(guān)(回憶TCP協(xié)議頭中, 有一個緊急指針的字段),
3.4 通過編碼深入理解
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);
// // 格式:默認部分+自定義部分
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// // printf("%s", logtxt); // 暫時打印
// 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);
// 格式:默認部分+自定義部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暫時打印
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); // 會不會阻塞在這里?不會
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++) // 第二個循環(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++) // 這是第三個循環(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!檢測并獲取listensock上面的事件,新連接到來,等價于讀事件就緒
// struct timeval timeout = {1, 0}; // 輸入輸出,可能要進行周期的重復(fù)設(shè)置
struct timeval timeout = {0, 0}; // 輸入輸出,可能要進行周期的重復(fù)設(shè)置
// 如果事件就緒,上層不處理,select會一直通知你!
// select告訴你就緒了,接下來的一次讀取,我們讀取fd的時候,不會被阻塞
// 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你怎么知道只有一個呢???
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ù)組, 用戶維護的!
// 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;
}注意事項:
1、不能直接aceept,因為他大部分時間都在等,一次只能等一個文件描述符?。。╨istensock上面的時間是新鏈接到來,就是三次握手完成,鏈接投遞到全連接隊列里,然后你再通過accept把鏈接從底層拿上來),所以新鏈接來了相當于是讀事件就緒??!
2、 定義fd_set類型變量如果是在棧上定義,可能會出現(xiàn)亂碼,所以在使用前要記得先清空!!

3、因為timeout是輸入輸出型參數(shù)!!所以返回之后可能已經(jīng)修改過了?。∷詾榱司S持他的效果我們就必須周期性重復(fù)設(shè)置!!

4、因為(1)rfds是一個輸入輸出型參數(shù),每次都會被重新設(shè)置,且隨著不斷獲取新鏈接,套接字的數(shù)量會越來越多!不能寫死,應(yīng)是動態(tài)計算 (2)select不僅僅要等lisentsock,也要等讀的sock
因此需要有一個輔助數(shù)組arrry來監(jiān)控select中的fd,他不僅可以方便我們
(1)將文件描述符信息在不同函數(shù)之間的傳遞
(2)用于在select 返回后,array作為源數(shù)據(jù)和fd_set進行FD_ISSET判斷。。
(3)select返回后會把以前加入的但并無事件發(fā)生的fd清空,則每次開始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用于select的第一個參數(shù)。
(4)讓左側(cè)是監(jiān)聽套接字,右側(cè)是讀套接字
5、輔助數(shù)組里有鏈接就緒和讀就緒,我們怎么區(qū)分呢??——>確認就緒之后,再加一層判斷。證明自己是否是監(jiān)聽套接字。

6、關(guān)于Dispatcher(事件派發(fā)器),就是收到了多個就緒的文件描述符,然后跟array進行判斷并派發(fā),如果是連接就緒就交給連接事件處理,如果是讀就緒就交給讀事件處理。
因為就緒的時間不一定只有一個,所以必須要循環(huán)去遍歷!
7、關(guān)于recver,讀的時候不能直接讀,因為讀的時候內(nèi)容可能不完整,這就涉及到了協(xié)議的內(nèi)容!
3.5select缺點
1、等待的fd是有上限的!!
可監(jiān)控的文件描述符個數(shù)取決與sizeof(fd_set)的值. 我這邊服務(wù)器上sizeof(fd_set)=512,每bit表示一個文件 描述符,則我服務(wù)器上支持的最大文件描述符是512*8=4096.
備注: fd_set的大小可以調(diào)整,可能涉及到重新編譯內(nèi)核
2、輸入輸出型參數(shù)比較多,數(shù)據(jù)拷貝的頻率很高,且每次都需要對關(guān)心的fd進行重置
3、用戶層是,使用第三方數(shù)組管理用戶的fd,用戶層需要多次遍歷,內(nèi)核中檢測fd時間就緒也要遍歷。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Linux deepin 刪除多余內(nèi)核的實現(xiàn)方法
這篇文章主要介紹了Linux deepin 刪除多余內(nèi)核的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12
linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中
這篇文章主要介紹了linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中的相關(guān)資料,需要的朋友可以參考下2016-12-12
logrotate實現(xiàn)日志切割方式(轉(zhuǎn)儲)
這篇文章主要介紹了logrotate實現(xiàn)日志切割方式(轉(zhuǎn)儲),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05
ubuntu系統(tǒng)下apache配置虛擬主機及反向代理詳解
這篇文章主要介紹了ubuntu系統(tǒng)下apache配置虛擬主機及反向代理的相關(guān)資料,文中通過實例給大家演示的非常詳細,對大家具有一定的參考學習價值,需要的朋友們下來一起學習學習吧。2017-06-06
詳解Supervisor安裝與配置(Linux/Unix進程管理工具)
這篇文章主要介紹了詳解Supervisor安裝與配置(Linux/Unix進程管理工具),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
linux系統(tǒng)報tcp_mark_head_lost錯誤的處理方法
這篇文章主要給大家介紹了關(guān)于linux系統(tǒng)報tcp_mark_head_lost錯誤的處理方法,文中通過示例代碼介紹的非常詳細,對大家學習或者使用linux系統(tǒng)具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-07-07

