select模型支持IO多路復用,select函數如下
int select ( IN int nfds, //windows下無意義,linux有意義 IN OUT fd_set* readfds, //檢查可讀性 IN OUT fd_set* writefds, //檢查可寫性 IN OUT fd_set* exceptfds, //例外數據 IN const struct timeval* timeout); //函數的返回時間
逐個解釋每個參數意義:
nfds:一個整型變量,表示比最大文件描述符+1
readfds: 這個集合監測讀事件的描述符,將要監聽
讀事件的文件描述符放入readfds中,通過調用select,
readfds中將沒有就緒的讀事件文件描述符清除,留下
就緒的讀事件描述符,可以通過read或者recv來處理
writefds:這個集合監測寫事件的描述符,將要監聽的
寫事件的文件描述符放入writefds中,通過調用select,
writefds中沒有就緒的寫事件文件描述符被清除,留下
就緒的寫事件描述符,可以通過write或者send來處理。
execptfds:這個集合在調用select后會存有錯誤的文件
描述符。根據Linux網絡網絡編程第二版中介紹,可以
監視帶外數據OOB,帶外數據使用MSG_OOB標志發送
到套接字上,當select()函數返回的時候,readfds將清除
其中的其他文件描述符,留下OOB數據
函數返回值:
當返回0時表示超時,-1表示有錯誤,大於0表示沒有錯誤。
當監視文件集中有文件描述符符合要求,即讀文件描述符集
合中有文件可讀,寫文件描述符集合中有文件可寫,或者
錯誤文件描述符集合中有錯誤的描述符,都會返回大於0的數。
timeval結構體解釋
struct timeval { long tv_sec; //秒 long tv_usec; //毫秒 };
timeval指針為NULL,表示一直等待,直到有符合條件的描述符
觸發select返回
如果timeval中個參數均為0,表示立即返回,否則在select沒有
符合條件的描述符,等待對應的時間和,然后返回。
另外需要了解一些select的操作宏函數
fd_set是一個SOCKET隊列,以下宏可以對該隊列進行操作:
FD_CLR( s, fd_set *set) 從隊列set刪除句柄s;
FD_ISSET( s,fd_set *set) 檢查句柄s是否存在與隊列set中;
FD_SET( s, fd_set *set )把句柄s添加到隊列set中;
FD_ZERO( fd_set *set ) 把set隊列初始化成空隊列.
看一個select的使用示例
//前面是服務器socket的創建,綁定和監聽 int listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // Bind local.sin_family = AF_INET; local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_port = htons(PORT); bind(listenSocket, (sockaddr*)&local, sizeof(SOCKADDR_IN)); // Listen listen(listenSocket, 3); //設置非阻塞 fcntl(listenSocket, F_SETFL, O_NONBLOCK ); //這個使用來統計所有已經連接上來的文件描述符, //也可以用數組表示,只是遍歷的時候要根據數據結構進行更改 FD_SET socketSet; FD_SET writeSet; FD_SET readSet; //清空 socketSet FD_ZERO(&socketSet); //將文件描述符放入 socketSet, //用於accept FD_SET(listenSocket,&socketSet); //統計最大的socket int maxfd = listenSocket; int conNum = 1; //數組存儲連接的socket int connectArray[1024]={0}; while(true) { //清空讀寫集合 FD_ZERO(&readSet); FD_ZERO(&writeSet); //讀寫都監聽 readSet=socketSet; writeSet=socketSet; //同時檢查套接字的可讀可寫性。 //為等待時間傳入NULL,則永久等待。傳入0立即返回。不要勿用。 int ret=select(maxfd,&readSet,&writeSet,NULL,NULL); if(ret==-1) { return false; } sockaddr_in addr; int len=sizeof(addr); //是否存在客戶端的連接請求。 //在readset中會返回已經調用過listen的套接字 if(FD_ISSET(listenSocket,&readSet)) { acceptSocket=accept(listenSocket,(sockaddr*)&addr,&len); if(acceptSocket==INVALID_SOCKET) { return false; } else { //大於我們最大的監聽數量了 if(conNum > 1024) { return false; } //更新數組 connectArray[conNum] = acceptSocket; //設置非阻塞 fcntl(connectArray[conNum], F_SETFL, O_NONBLOCK ); //加到socketset里,以后賦值給讀寫集合 FD_SET(connectArray[conNum],&socketSet); if(acceptSocket > maxfd) { maxfd = acceptSocket; } conNum++; } } for(int i=0;i<conNum;i++) { //判斷是否有讀事件 if(FD_ISSET(connectArray[i],&readSet)) { //調用recv,接收數據。 //判斷recv結果,為0則客戶端斷開, //那么調用FD_CLR並關閉對應的socket } //判斷是否有寫事件 if(FD_ISSET(connectArray[i],&writeSet) { //調用send,發送數據。 //判斷send結果,為0客戶端斷開, //那么調用FD_CLR並關閉對應的socket } } }
上面的例子結合了網上提供的一些demo,其實writeSet不一定要放入socket,當某個socket需要send內容時
再調用FD_SET(socket,&writeSet),寫成功后再調用FD_CLR(socket,&writeSet);避免造成busyloop,
因為當緩沖區非空時,寫事件是一直就緒的。
我的微信公眾號,定期推送一些技術總結,一起努力吧。