在進入今天的select模型的主題之前,我們先來簡單了解一下五種I/O模型:
(1)阻塞I/O(默認采用這種方式)
在服務端socket編程中,我們常見的accpet函數、recv函數都是采取的阻塞形式。以recv為例: 當上層應用App調用recv系統調用時,如果對等方沒有發送數據(Linux內核緩沖區中沒有數據),上層應用Application1將阻塞;當對等方發送了數據,Linux內核recv端緩沖區數據到達,內核會把數據copy給用戶空間。然後上層應用App解除阻塞,執行下一步操作。
(2)非阻塞I/O(不推薦)
上層應用如果應用非阻塞模式, 會循環調用recv函數,接受數據。若緩沖區沒有數據,上層應用不會阻塞,recv返回值為-1,錯誤碼是EWOULDBLOCK(圖中的標記有誤)。上層應用程序不斷輪詢有沒有數據到來。造成上層應用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。應用范圍小,一般和IO復用配合使用。
(3)信號驅動I/O模型(不經常使用)
上層應用建立SIGIO信號處理程序。當緩沖區有數據到來,內核會發送信號告訴上層應用App; 當上層應用App接收到信號後,調用recv函數,因緩沖區有數據,recv函數一般不會阻塞。但是這種用於模型用的比較少,屬於典型的“拉模式(上層應用被動的去Linux內核空間中拉數據)”。即:上層應用App,需要調用recv函數把數據拉進來,會有時間延遲,我們無法避免在延遲時,又有新的信號的產生,這也是他的缺陷。
(4)異步I/O(不常用)
上層應用調用aio_read函數,同時提交一個應用層的緩沖區buf;調用完畢後,不會阻塞。上層應用程序App可以繼續其他任務; 當TCP/IP協議緩沖區有數據時,Linux主動的把內核數據copy到用戶空間。然後再給上層應用App發送信號;告訴App數據到來,需要處理!
異步IO屬於典型的“推模式”, 是效率最高的一種模式,上層應用程序App有異步處理的能力(在Linux內核的支持下,處理其他任務的同時,也可支持IO通訊, 與Windows平台下的完成端口作用類似IOCP)。
(5)I/O復用的select模型(本篇的重點)
試想如果你遇到下面的問題會怎麼處理?
1)server除了要對外響應client的服務外,還要能夠接受標准輸入的命令來進行管理。
假如使用上述阻塞方式,在單線程中,accept調用和read調用必定有先後順序,而它們都是阻塞的。比如先調用accept,後調用 read,那麼如果沒有客戶請求時,服務器會一直阻塞在accept,沒有機會調用read,也就不能響應標准輸入的命令。
2) server要對外提供大量的client請求服務。
假如使用阻塞方式,在單線程中,由於accept和recev都是阻塞式的,那麼當一個client被服務器accept後,它可能在send發送消息時阻塞,因此服務器就會阻塞在recev調用。即時此時有其他的client進行connect,也無法進行響應。
這時就需要select來解決啦!select實現的是一個管理者的功能: 用select來管理多個IO, 一旦其中的一個IO或者多個IO檢測到我們所感興趣的事件, select就返回, 返回值就是檢測到的事件個數, 並且由第2~4個參數返回那些IO發送了事件, 這樣我們就可以遍歷這些事件, 進而處理這些事件。
有人說,我用多線程不就可以了嗎?但是在UNIX平台下多進程模型擅長處理並發長連接,但卻不適用於連接頻繁產生和關閉的情形。當然select並不是最高效的,有著O(N)的時間復雜度,關於更高效的epoll我將在後面的博客中繼續講解,歡迎大家關注,╰( ̄▽ ̄)╮
#include#include #include #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: is the highest-numbered file descriptor in any of the three sets,plus 1[讀,寫,異常集合中的最大文件描述符+1].
fd_set[四個宏用來對fd_set進行操作]
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
timeout[從調用開始到select返回前,會經歷的最大等待時間, 注意此處是指的是相對時間]
/timeval結構: struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; //一些調用使用3個空的set, n為0, 一個非空的timeout來達到較為精確的sleep.
Linux中, select函數改變了timeout值,用來指示還剩下的時間,但很多實現並不改timeout。
為了較好的可移植性,timeout在循環中一般常被重新賦初值。
Timeout取值:
timeout== NULL
無限等待,被信號打斷時返回-1, errno 設置成 EINTR
timeout->tv_sec == 0 && tvptr->tv_usec == 0
不等待立即返回
timeout->tv_sec != 0 || tvptr->tv_usec != 0
等待特定時間長度, 超時返回0
注:關於select用來設置超時時間的用法可以參考我的另外一篇博客 http://blog.csdn.net/nk_test/article/details/49050379
返回值:
如果成功,返回所有sets中描述符的個數;如果超時,返回0;如果出錯,返回-1.
下面是使用select改進服務器端和客戶端的程序,解決了上述提出的兩個問題:
服務器端:
/*示例1: 用select來改進echo回聲服務器的client端的echoClient函數 使得可以在單進程的情況下同時監聽多個文件描述符; */ void echoClient(int sockfd) { char buf[512]; fd_set rset; //確保標准輸入不會被重定向 int fd_stdin = fileno(stdin); int maxfd = fd_stdin > sockfd ? fd_stdin : sockfd; while (true) { FD_ZERO(&rset); //監視兩個I/O FD_SET(fd_stdin, &rset); FD_SET(sockfd, &rset); int nReady = select(maxfd+1, &rset, NULL, NULL, NULL); //不需要的置NULL if (nReady == -1) err_exit("select error"); else if (nReady == 0) continue; /** nReady > 0: 檢測到了可讀事件 **/ if (FD_ISSET(fd_stdin, &rset)) { memset(buf, 0, sizeof(buf)); if (fgets(buf, sizeof(buf), stdin) == NULL) break; if (writen(sockfd, buf, strlen(buf)) == -1) err_exit("write socket error"); } if (FD_ISSET(sockfd, &rset)) { memset(buf, 0, sizeof(buf)); int readBytes = readline(sockfd, buf, sizeof(buf)); if (readBytes == 0) { cerr << "server connect closed..." << endl; exit(EXIT_FAILURE); } else if (readBytes == -1) err_exit("read-line socket error"); cout << buf; } } }
/*示例2: 用select來改進echo回射服務器的server端的接受連接與處理連接部分的代碼: 使得可以在單進程的情況下處理多客戶連接, 對於單核的CPU來說, 單進程使用select處理連接與監聽套接字其效率不一定就會比多進程/多線程性能差; */ struct sockaddr_in clientAddr; socklen_t addrLen; int maxfd = listenfd; fd_set rset; fd_set allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(listenfd, &allset); int client[FD_SETSIZE]; //用於保存已連接的客戶端套接字 for (int i = 0; i < FD_SETSIZE; ++i) client[i] = -1; int maxi = 0; //用於保存最大的不空閒的位置, 用於select返回之後遍歷數組 while (true) { rset = allset; int nReady = select(maxfd+1, &rset, NULL, NULL, NULL); if (nReady == -1) { if (errno == EINTR) continue; err_exit("select error"); } //nReady == 0表示超時, 但是此處是一定不會發生的 else if (nReady == 0) continue; if (FD_ISSET(listenfd, &rset)) { addrLen = sizeof(clientAddr); int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen); if (connfd == -1) err_exit("accept error"); int i; for (i = 0; i < FD_SETSIZE; ++i) { if (client[i] < 0) { client[i] = connfd; if (i > maxi) maxi = i; break; } } if (i == FD_SETSIZE) { cerr << "too many clients" << endl; exit(EXIT_FAILURE); } //打印客戶IP地址與端口號 cout << "Client information: " << inet_ntoa(clientAddr.sin_addr) << ", " << ntohs(clientAddr.sin_port) << endl; //將連接套接口放入allset, 並更新maxfd FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (--nReady <= 0) continue; } /**如果是已連接套接口發生了可讀事件**/ for (int i = 0; i <= maxi; ++i) if ((client[i] != -1) && FD_ISSET(client[i], &rset)) { char buf[512] = {0}; int readBytes = readline(client[i], buf, sizeof(buf)); if (readBytes == -1) err_exit("readline error"); else if (readBytes == 0) { cerr << "client connect closed..." << endl; FD_CLR(client[i], &allset); close(client[i]); client[i] = -1; } //注意此處: Server從Client獲取數據之後並沒有立即回射回去, // 而是等待四秒鐘之後再進行回射 sleep(4); cout << buf; if (writen(client[i], buf, readBytes) == -1) err_exit("writen error"); if (--nReady <= 0) break; } }