高級IO—select
文章目錄
- 高級IO—select
- IO的概念
- 五種IO模型
- 阻塞IO
- 非阻塞IO
- 信號驅動IO
- IO多路轉接
- 異步IO
- I/O多路轉接之select
IO的概念
通常指數據在內部存儲器和外部存儲器或其他周邊設備之間的輸入和輸出。輸入是系統接收的信號或數據,輸出則是從其發送的信號或數據。也可把輸入輸出認為是信息處理系統(例如計算器)與外部世界(人類或另一信息處理系統)之間的通信。
IO分為IO設備和IO接口
- IO設備
IO設備是硬件中由人使用并與計算機進行通信的設備。例如鍵盤或鼠標是計算機的輸入設備,監視器和打印機是輸出設備。計算機之間的通信設備進行的通常是運行輸入輸出操作。
- IO接口
I/O接口的功能是負責實現CPU通過系統總線把I/O電路和外圍設備聯系在一起。IO函數的底層是系統提供的系統調用,供用戶通過調用來實現從用戶態到內核態或內核態到用戶態的數據拷貝。
實際上在網絡通信中,調用write
并不是直接將數據寫到網絡中,而是將數據從應用層拷貝到傳輸層的發送緩沖區當中,然后由OS自主決定什么時候將數據向下交付,發送到網絡中。同理調用read
并不是直接從網絡中讀取數據,而是將傳輸層的接收緩沖區的數據讀到應用層中。這意味調用read
的時候,傳輸層的接收緩沖區并沒有數據,那么read
函數就會阻塞住,直到緩沖區有數據,才能將數據讀到應用層。
因此IO本質不僅僅只有讀取/寫入,還有等待資源就緒的過程,即等+拷貝。
提高IO的效率本質是每次IO中減少等待的時間,讓IO過程盡可能都是拷貝。因此為了提高IO的效率,衍生出多種IO模型。
五種IO模型
阻塞IO
在內核將數據準備好之前,系統調用會一直等待,所有的套接字默認的是阻塞方式。
常見的阻塞IO模型
用戶調用recvfrom函數,嘗試讀取數據,即調用系統調用,由用戶態切換到內核態,由于數據沒有準備好導致阻塞等待,數據準備好了立刻拷貝數據報并返回用戶態。
代碼以使用read為例,讀取文件描述符為0即stdin的數據,默認以阻塞式方式讀取
#include"until.hpp"
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{char buffer[1024];while(true){printf(">>>>");fflush(stdout);ssize_t i=read(0,buffer,sizeof(buffer)-1);if(i>0){buffer[i-1]=0;cout<<"echo# "<<buffer<<endl;}else if(i==0){cout<<"read end"<<endl;break;}else{//...}}return 0;
}
非阻塞IO
如果內核還未將數據準備好, 系統調用不會阻塞等待,會直接返回, 并且返回EWOULDBLOCK錯誤碼。
非阻塞IO往往需要程序員以循環的方式反復嘗試讀寫文件描述符, 這個過程稱為輪詢。這意味著輪詢的過程需要一直占用CPU資源,對CPU來說是較大的浪費,一 般只有特定場景下才使用。
常見的非阻塞IO模型
用戶調用recvfrom函數,這次該函數是以非阻塞的方式進行調用,嘗試讀取數據,由用戶態切換到內核態,由于數據沒有準備好,直接返回
EWOULDBLOCK
。因此程序員需要以輪詢的方式調用recvfrom
函數,數據準備好了立刻拷貝數據報并返回用戶態。輪詢的過程中一是需要占用CPU的資源,二是需要多次進行用戶態與內核態之間的轉換,資源浪費較為嚴重,該方式一般在特定場景才使用。
需要將文件描述符設置為非阻塞狀態,那么讀取該文件描述符就以非阻塞方式讀取。
fcntl
用于控制文件描述符屬性的系統調用,它可以用于執行各種操作,包括設置文件狀態標志、獲取文件狀態標志、鎖定文件等。
函數原型
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* struct flock *flockptr */);
fd
:表示要操作的文件描述符。cmd
:表示操作類型,可以是以下值之一:F_GETFL
:獲取文件狀態標志,F_SETFL
:設置文件狀態標志,F_GETLK
:獲取文件鎖定信息,F_SETLK
:設置文件鎖定等。- 使用不同的cmd,會有不同的返回值。使用
F_GETFL
時,返回值是文件狀態標志flag
。可以通過文件狀態標志將文件設置為非阻塞狀態。
until.hpp
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
void Setnonblock(int sock)
{int flag=fcntl(sock,F_GETFL,0);if(flag<0){perror("fcntl");return;}fcntl(sock,F_SETFL,flag|O_NONBLOCK);//把文件描述符狀態設置為非阻塞O_NONBLOCK
}
#include"until.hpp"
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{char buffer[1024];Setnonblock(0);while(true){printf(">>>>");fflush(stdout);ssize_t i=read(0,buffer,sizeof(buffer)-1);if(i>0){buffer[i-1]=0;cout<<"echo# "<<buffer<<endl;}else if(i==0){cout<<"read end"<<endl;break;}else{//...}sleep(1);}return 0;
}
- 非阻塞的返回值
對于非阻塞來說,底層沒有數據直接返回,返回值為-1,但這并不是發生錯誤,原因由錯誤碼來標記。錯誤碼為EAGAIN
或EWOULDBLOCK
表示沒有讀取到數據。相同的還有EINTER
表示因為信號中斷導致返回,需要繼續讀取。
#include"until.hpp"
using namespace std;fd_set readset;
int main()
{setNonBlock(0);//將輸入緩沖區的IO行為設置為非阻塞char buffer[1024];//設置緩沖區while(true){ssize_t i= read(0,buffer,sizeof(buffer)-1);//從文件描述符為0(鍵盤)開始讀,讀到buffer緩沖區中if(i>0){buffer[i-1]=0;cout<<"echo# "<<buffer<<endl;}else if(i==0){cout<<"read end"<<endl;break;}else{cout<<"i: "<<i<<endl;cout<<"EAGAIN: "<<EAGAIN<<endl;cout<<"EWOULDBLOCK: "<<EWOULDBLOCK<<endl;}sleep(1);}return 0;
}
非阻塞沒有讀取到數據直接返回的錯誤碼是11,EAGAIN
和EWOULDBLOCK
的錯誤碼也是11。
信號驅動IO
內核將數據準備好的時候, 使用SIGIO
信號通知應用程序進行IO操作。
常見的信號驅動IO模型
先前建立好
SIGIO
信號處理程序,進程將等待資源就緒的過程托管給sigaction
函數,讓該函數去等待數據,數據準備好后,以信號通知的方式返回,通知進程,此時進程直接調用recvfrom
函數,拷貝數據報并返回。
IO多路轉接
IO多路轉接能夠同時等待多個文件描述符的就緒狀態。
常見的IO多路轉接模型
進程將等待資源的過程托管給
select
函數,讓select
去等待數據,資源準備好后,select
函數返回可讀條件,通知進程,此時進程直接調用recvfrom
函數,拷貝數據報并返回。這意味著可以讓多個進程將等待資源的過程托管給同一個select
函數,哪個資源就緒,select
函數就通知相應的程序進行讀取。
異步IO
由內核在數據拷貝完成時, 通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數據)。
常見異步IO模型
進程需要讀取某種資源時,調用
aio_read
函數(系統調用),將IO(等+拷貝)的過程托管給OS,讓OS負責等,數據準備好后,OS自動將數據拷貝到用戶層的緩沖區,然后返回指定信號,通知進程來處理數據。進程并不參與IO的過程,只負責處理數據。
總結一下:
- 阻塞、非阻塞、信號驅動在IO的效率上并無差別,差別在于等待資源的過程。阻塞式在等的過程中不能做別的事,而非阻塞和信號驅動在等的過程中可以做其他事情。
- 阻塞、非阻塞、信號驅動、多路轉接實際上都參與了IO的過程,即IO的等待過程和拷貝過程,參與了其中一個過程都算作是同步IO。
- 異步IO是將IO過程托管給OS,并沒有參與IO的過程。
- 多路轉接的高效在于可以同時等待多個文件描述符,即等待多個資源就緒,并行等待資源,減少了等待資源的過程。
I/O多路轉接之select
select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的。可以將多個文件描述符托管給select去等待,存在文件描述符就緒,select返回,通知程序調用讀取調用對應的資源。
函數原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds
表示監視文件描述符的最大值加1。readfds
:指向一個fd_set
結構的指針,包含要監視可讀性的文件描述符。writefds
:指向一個fd_set
結構的指針,包含要監視可寫性的文件描述符。exceptfds
:指向一個fd_set
結構的指針,包含要監視異常情況的文件描述符。timeout
:指向struct timeval
結構的指針,用于設置超時時間。如果為NULL
,select
函數將一直阻塞,直到有文件描述符就緒。select
函數的返回值是就緒文件描述符的數量,如果返回值為-1,則表示出現錯誤。在這種情況下,可以使用perror
函數來輸出錯誤信息。
說明一下:
-
由于文件描述符是OS中的文件描述符表的下標,該表是從小到大依次使用,因此所被占用的文件描述符是連續的,即nfds能涵蓋所使用的文件描述符范圍。如nfds=5,表示在0~4號文件描述符中查詢。
-
readfds
,writefds
,exceptfds
和timeout
都是指針,即都是輸入輸出型參數。timeout
所指向的結構是能夠表示秒、微妙。struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */};
struct timeval timeout ={0,0};//表示非阻塞。 struct timeval timeout =nullptr;//表示阻塞。 struct timeval timeout ={5,0};//表示5秒內是阻塞式,超過5秒,非阻塞返回一次。
需要注意的是,timeout是輸入輸出型參數,例如
timeout ={5,0}
,若在第3秒結束時sock就緒,此時timeout
的返回值為等待的剩余時間,即返回值為2秒即timeout ={2,0}
。若在5秒期間sock都沒有就緒,那么返回值為0秒即timeout ={0,0}
,此時再次將該timeout
參數傳入就表示非阻塞等待。因此timeout
參數在傳入時需要重置。 -
fd_set
實際上是一個位圖結構。以readfd
為例,用戶想要OS關心4,5號文件描述符的讀時間,那么輸入的位圖結構是0011 0000
。
當關心時間內5號文件描述符就緒了,OS會對輸入的位圖進行改動,輸出表示哪些文件描述符已經就緒。輸出的位圖結構是0010 0000
。
readfd
, writefd
,exceptfd
的結構都是位圖,且是分別不同的位圖,因此用戶可以傳入一個或多個位圖讓OS關心位圖指定的文件描述符上的讀事件,寫事件,異常事件,OS通過該位圖輸出哪些事件已經就緒。
我們并不需要直接傳入自己設置的位圖結構,而是通過OS提供的接口對位圖進行修改。 可以使用以下宏來操作fd_set
FD_ZERO(fd_set*set);將set中的所有位清零
FD_SET(int fd, fd_set *set);將set中的指定文件描述符位設置為1。傳入fd,用位圖來標定傳入的fd是否需要關心。
FD_CLR(int fd, fd_set *set);將set中的指定文件描述符位清零。
FD_ISSET(int fd, fd_set *set);檢查set中的指定文件描述符位是否被設置為1。
通過一段server代碼來應用select函數
select.hpp
#include<unistd.h>
#include"Sock.hpp"
using namespace std;
static const int defaultport=8081;
class SelectServer
{
public:SelectServer(uint16_t port=defaultport):_port(port)
{}void initserver()
{
_listensock=Sock::Socket();//創建套接字
Sock::Bind(_listensock,_port);//bind信息
Sock::Listen(_listensock);//把sock設置為監聽狀態}void start()
{for(;;){fd_set rfd;FD_ZERO(&rfd);//清空位圖FD_SET(_listensock,&rfd);//把listensock設置進位圖,企圖讓OS對該sock關心struct timeval timeout={1,0};int n=select(_listensock+1,&rfd,nullptr,nullptr,&timeout);switch (n){case 0:cout<<"timeout......"<<endl;break;case -1:cout<<"select err"<<endl;default:cout<<"get new link..."<<endl;break;}sleep(1);}
}private:
uint16_t _port;
int _listensock;
};
main.cc
#include<iostream>
#include<unistd.h>
#include<memory>
#include"select.hpp"
using namespace std;static void Usage(string proc)
{cerr<<"Usage:\n\t"<<proc<<" port "<<"\n\n";
}string resp(const string& s)
{return s;
}int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(USAGE_ERR);}unique_ptr<SelectServer> selsv(new SelectServer(atoi(argv[1])));selsv->initserver();selsv->start();return 0;
}
Sock.hpp
#pragma once#include<iostream>
#include<string>
#include<cstring>
#include<sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};class Sock
{const static int backlog=32;public:static int Socket(){int sock=socket(AF_INET,SOCK_STREAM,0);//創建套接字if(sock<0)//創建失敗{logMessage(FATAL,"create sock error");exit(SOCKET_ERR);}//創建成功logMessage(NORMAL,"create sock success");int opt=1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));//允許套接字關閉后立刻重啟return sock;} static void Bind(int sock,int 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=htons(INADDR_ANY);//不綁定指定IP,可以接收任意IP主機發送來的數據//將本地設置的信息綁定到網絡協議棧if (bind(sock,(struct sockaddr*)&local,sizeof(local))<0){logMessage(FATAL,"bind socket error");exit(BIND_ERR);}logMessage(NORMAL,"bind socket success");}static void Listen(int sock)//將套接字設置為監聽{if(listen(sock,0)<0){logMessage(FATAL,"listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL,"listen socket success");}static int Accpet(int listensock,string * clientip,uint16_t* clientport){struct sockaddr_in cli;socklen_t len= sizeof(cli);int sock=accept(listensock,(struct sockaddr*)&cli,&len);if(sock<0){logMessage(FATAL,"accept error");//這里accept失敗為什么不退出}else{logMessage(NORMAL,"accept a new link,get new sock : %d",sock);*clientip=inet_ntoa(cli.sin_addr);*clientport=ntohs(cli.sin_port);}return sock;}
};
log.hpp
#pragma once#include <iostream>
#include <string>
#include<ctime>
#include <sys/types.h>#include <unistd.h>#include <stdio.h>
#include <stdarg.h>
using namespace std;
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define NUM 1024
#define LOG_STR "./logstr.txt"
#define LOG_ERR "./log.err"
const char* to_str(int level)
{switch(level){case DEBUG: return "DEBUG";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";default: return nullptr;}
}void logMessage(int level, const char* format,...)
{char logprestr[NUM];
snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());char logeldstr[NUM];
va_list arg;
va_start(arg,format);
vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函數列表中的...cout<<logprestr<<logeldstr<<endl;}
說明一下:
-
通過select函數對_listensock進行讀事件的關心,當連接到來時,select返回就緒的時間數。表示連接到來屬于讀事件。
-
連接沒到來時select返回值為0,表示0個事件就緒。連接到來后返回值為1,表示已經有一個事件就緒。多次打印
get new link...
是因為底層的連接到來,上層并沒有把連接取走,因此底層的就緒事件一直存在。
由于服務器在最初時只使用listensock
拿取底層的連接,而后續需要等待多個文件描述符時,可以通過數組來管理。fd_set
位圖的大小為128字節,那么該位圖可以同時關心128*8—1024個sock的就緒事件,因此管理sock的數組大小也應該是1024。
select.hpp
#include <unistd.h>
#include "Sock.hpp"
using namespace std;
static const int defaultport = 8081;
static const int fdnum = sizeof(fd_set) * 8;
static const int defaultfd = -1;
class SelectServer
{
public:SelectServer(uint16_t port = defaultport) : _port(port), _listensock(-1), _fdarry(nullptr){}void initserver(){_listensock = Sock::Socket(); // 創建套接字Sock::Bind(_listensock, _port); // bind信息Sock::Listen(_listensock); // 把sock設置為監聽狀態// cout<<"fd_set size: "<<sizeof(fd_set)<<endl;_fdarry = new int[fdnum]; // 保存fd的數組for (int i = 0; i < fdnum; i++){_fdarry[i] = defaultfd;}_fdarry[0] = _listensock;}void Print()
{cout<<"fd list: ";for(int i=0;i<fdnum;i++){if(_fdarry[i]!=defaultfd)cout<<_fdarry[i]<<" ";}cout<<endl;
}void handleract(fd_set&rfd){if(FD_ISSET(_listensock,&rfd)){char buffer[1024];uint16_t clientport;string clientip;int sock = Sock::Accpet(_listensock, &clientip, &clientport); // 獲取sockif (sock < 0){cout << "Sock::Accept err " << endl;return;}cout << "get a new sock: " << sock << endl;int i=0;for(;i<fdnum;i++){if(_fdarry[i]!=defaultfd)continue;else break;}if(i==fdnum){cout<<"server is full,please wait"<<endl;close(sock);}_fdarry[i]=sock;FD_SET(_fdarry[i],&rfd);Print();}}void start(){for (;;){fd_set rfd;FD_ZERO(&rfd); // 清空位圖int maxfd = _fdarry[0];int i = 0;for (; i < fdnum; i++){if(_fdarry[i]==defaultfd)continue;FD_SET(_fdarry[i],&rfd);maxfd=maxfd>_fdarry[i]?maxfd:_fdarry[i];//更新最大fd數}// struct timeval timeout={1,0};// int n=select(_listensock+1,&rfd,nullptr,nullptr,&timeout);int n = select(maxfd + 1, &rfd, nullptr, nullptr, nullptr); // 阻塞式switch (n){case 0:cout << "timeout......" << endl;break;case -1:cout << "select err" << endl;default:cout << "get new link..." << endl;handleract(rfd);break;}sleep(1);}}private:uint16_t _port;int _listensock;int *_fdarry;
};
適用數組管理的原因在于:
- select的
readfd
,writefd
,exceptfd
參數是輸入輸出型參數,函數返回時會改變這三個位圖,此時就需要通過數組去重置初始化這三個位圖。 - 通過位圖可以方便很方便的知道最大文件描述符數,前提是設置數組的默認sock。
- 根據數組內的默認sock和已經保存的sock,很方便的賦值給
fd_set
位圖參數。
現結合管理數組和select
函數寫一個能夠接收client端發送來的信息,并且能夠返回的服務器
main.cc
#include<iostream>
#include<functional>
#include<vector>
#include<memory>
#include"err.hpp"
#include"selectserver.hpp"
using namespace std;
using namespace Select_sv;static void Usage(string proc)
{cerr<<"Usage:\n\t"<<proc<<" port "<<"\n\n";
}string resp(const string& s)
{return s;
}int main(int argc,char* argv[])
{unique_ptr<SelectServer> selsv(new SelectServer(resp));selsv->initServer();selsv->Start();return 0;
}
selectserver.hpp
#pragma once#include <iostream>
#include <sys/select.h>
#include <string>
#include <functional>
#include "Sock.hpp"using namespace std;namespace Select_sv
{static const int defaultport = 8080; // 默認端口號static const int fdnum = sizeof(fd_set) * 8; // 可使用的套接字數量static const int defaultfd = -1; // 默認套接字標志using func_t = function<string(const string &)>;class SelectServer{public:SelectServer(func_t f, int port = defaultport) : _func(f), _port(port), _listensock(-1), _fdarray(nullptr){}void initServer(){// 獲取套接字_listensock = Sock::Socket();cout << "Sock success" << endl;// 綁定網絡信息Sock::Bind(_listensock, _port);cout << "Bind success" << endl;// 把套接字設置為監聽狀態Sock::Listen(_listensock);cout << "Listen success" << endl;// 給每一個套接字都設置一個數組,保存套接字的設置情況cout << "fd_set size: " << sizeof(fd_set) << endl;_fdarray = new int[fdnum];for (int i = 0; i < fdnum; i++)_fdarray[i] = defaultfd; // 將每個套接字狀態都設置為默認(未使用狀態)_fdarray[0] = _listensock; // 第一個設置的套接字是通信套接字,供accept函數使用-建立連接// cout << "initServer" << endl;}void Print(){cout << "now using socket: ";for (int i = 0; i < fdnum; i++){if (_fdarray[i] != defaultfd)cout << _fdarray[i] << " "; // 將設置進數組內的套接字進行打印}cout << endl;}void Accpter(int lsock){// logMessage(DEBUG, "Accepter begin");string clientip;uint16_t clientport = 0;int sock = Sock::Accpet(lsock, &clientip, &clientport); // 若成功返回,返回一個用于通信的套接字if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);int i = 0;for (; i < fdnum; i++){if (_fdarray[i] != defaultfd)continue;elsebreak;}if (i == fdnum) // 遍歷完全部socket發現沒用可使用的套接字{logMessage(WARNING, "server is full,please wait");close(sock); // 關閉用于通信的套接字,重新建立連接// _fdarray[i] = defaultfd;不需要去除,規定數組的0號下標對應的位置是專門用來拿連接的}else{_fdarray[i] = sock; // 把用于通信的套接字給select監管,讓它等待}Print();// logMessage(DEBUG, "Accepter end");}void Recver(int sock, int pos){// logMessage(DEBUG, "Recver begin");char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;cout << "client# " << buffer << endl;}else if (s == 0){close(sock); // 關閉該套接字,關閉通信通道_fdarray[pos] = defaultfd; // 將數組中的該套接字清除logMessage(NORMAL, "client quit");return;}else{close(sock);_fdarray[pos] = defaultfd; // 將數組中的該套接字清除logMessage(ERROR, "recv error");return;}// 將客戶端發來的數據原樣寫回去string resp = _func(buffer);write(sock, resp.c_str(), resp.size()); // 寫回去// logMessage(DEBUG, "Recever end");}void Handlerop(fd_set &rfds){for (int i = 0; i < fdnum; i++){if (_fdarray[i] == defaultfd)continue;if (FD_ISSET(_fdarray[i], &rfds) && (_fdarray[i] == _listensock))// 此時i對應的數組位置是拿到連接的文件描述符,意味著在底層連接已經拿到,等待上層提取{Accpter(_listensock);}else if (FD_ISSET(_fdarray[i], &rfds)) // 此時存在數組內的對應套接字都是底層讀資源就緒{Recver(_fdarray[i], i);}else{}}}void Start(){// 將數組管理的套接字設置進fd_set類型的結構內for (;;){fd_set rfds; // 當前程序只關心讀事件FD_ZERO(&rfds); // 對該結構(位圖)清空int maxfd = _fdarray[0];for (int i = 0; i < fdnum; i++){if (_fdarray[i] == defaultfd)continue;FD_SET(_fdarray[i], &rfds); // 將需要使用的套接字設置進讀事件結構中//若此時已經將連接拿到上層,因此select管理連接對應的sock就不會就緒,而可以只管理通信資源是否就緒if (maxfd < _fdarray[i])maxfd = _fdarray[i]; // 更新最大文件描述符// cout << "listensock set to _fdarray success" << endl;}// 把讀事件交給select監管cout << "will select " << endl;int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // 阻塞式監管cout << "select end" << endl;switch (n){case 0:logMessage(NORMAL, "timeout..."); // 監管時間內沒用套接字就緒,即超時返回break;case -1:logMessage(WARNING, "select error,error:%d, error string: ", errno, strerror(errno));break;default:logMessage(NORMAL, "get a new link..."); // 拿到新連接,即拿到通信的連接,客戶端主動斷開連接后,為何后續循環select都是拿到連接?Handlerop(rfds);break;}}}~SelectServer(){if (_listensock < 0) // 為什么是小于0?close(_listensock);if (_fdarray)delete[] _fdarray;}private:int _port;int _listensock;int *_fdarray; // 記錄需要交給select管理的套接字,每個套接字交給select管理的方式是傳遞整數給位圖,因此該數組的類型也是整數intfunc_t _func;};
}
說明一下:
- 在
initServer
函數里,完成創建套接字,bind信息,將套接字設置為監聽狀態,并且初始化管理數組_fdarry
,并將監聽套接字設置優先設置進數組的0號下標處,這不再改變。 - 在
Start
函數里,首先該函數是需要保證服務器的正常運行,因此是調用鏈是存在于死循環中。將管理數組內的sock設置進rfds
位圖中,即告訴內核需要關心這些sock。接著調用select
函數進行等待就緒事件。等待到就緒事件后調用Handlerop
函數,對就緒事件進行處理。 - 由于該服務器目前只處理獲取連接,接收客戶端發送過來的數據并返回這兩個業務。因此在
Handlerop
函數中,通過管理數組對已經返回的位圖進行對比,判斷出是listensock就緒還是通信的數據到來。若是獲取到連接,則調用Accepter
函數將底層的連接提取到應用層。若是數據到來,則調用Recver
函數讀取底層的數據,并進行處理。 - 在
Accpter
函數中,不僅將連接獲取上來,還需要將獲取到的連接添加到管理數組中,以便于在下次循環中告訴OS關心該新連接。 - 在
Recver
函數中,調用recv函數進行讀取,通過仿函數對數據進行處理并寫回到sock中。
梳理調用鏈
總結一下:
select可以同時等待多個文件描述符,提高了IO的效率。但是也存在以下缺陷:
- select能夠等待的文件描述符是有上限的,在我這臺云服務器中能夠使用的fd一共有10002個(通過
ulimit -a
查詢
而select使用的位圖結構fd_set
所能管理的sock數為1024,這表明了select能夠同時等待的文件描述符是具有上限的。除非更改內核的參數,否則不能解決。
- 由于
fd_set
位圖是輸入輸出型參數,那么在傳入傳出時必然發送改變,因此我們需要通過第三方數組去管理合法的文件描述符。 select
函數的大部分參數都是輸入輸出型的,調用函數時,通過輸入參數用戶告訴內核信息,函數返回,通過輸出參數內核告訴用戶信息,即采用位圖結構傳遞參數時,需要不斷的進行用戶到內核,內核到用戶的狀態切換,并且還進行了數據拷貝,造成了不少成本。- 由于使用的是位圖結構傳遞參數,并且位圖結構在輸入輸出時發生改變,導致我們需要遍歷所有的文件文件描述符,這帶來了一定的遍歷成本。而
select
的第一個參數是最大fd+1,是用來確定遍歷的范圍。
基于以上select
函數的劣勢,前人總結衍生出了更好的方案,如poll
,epoll
等等。