在進入今天的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;
}
}