歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux下的socket編程實踐(七)I/O多路復用技術之select模型

Linux下的socket編程實踐(七)I/O多路復用技術之select模型

日期:2017/3/1 12:23:10   编辑:關於Linux

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

Copyright © Linux教程網 All Rights Reserved