Socket編程實踐(8) --Select-I/O複用

五種I/O模型介紹

(1)阻塞I/O[默認]


   當上層應用App調用recv系統調用時,如果對等方沒有發送數據(Linux內核緩衝區中沒有數據),上層應用Application1將阻塞;當對等方發送了數據,Linux內核recv端緩衝區數據到達,內核會把數據copy給用戶空間。然後上層應用App解除阻塞,執行下一步操作。

 

(2)非阻塞I/O[少用]


   上層應用App將套接字設置成非阻塞模式, 然後循環調用recv函數,接受數據。若緩衝區沒有數據,上層應用不會阻塞,recv返回值爲-1,錯誤碼是EWOULDBLOCK。

   上層應用程序不斷輪詢有沒有數據到來。造成上層應用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。應用範圍小,一般和IO複用配合使用

 

(3)I/O多路複用[重點]


   上層應用App調用select等其他IO複用系統調用(該機制由Linux內核支持,避免了App忙等待),進行輪詢文件描述符的狀態變化; 當select管理的文件描述符沒有數據(或者狀態沒有變化時),上層應用也會阻塞。

   好處是:select機制可以管理多個文件描述符; 可以將select看成一個管理者,用select來管理多個IO, 一旦檢測到的一個IO或者多個IO,有我們感興事件發生時,select函數將返回,返回值爲檢測到的事件個數。進而可以利用select相關API函數,操作具體事件。

   select函數可以設置等待時間,避免了上層應用App長期僵死。

   和阻塞IO模型相比,select I/O複用模型相當於提前阻塞了。等到有數據到來時,再調用recv就不會發生阻塞。

 

(4)信號驅動I/O[並不常用]


   上層應用App建立SIGIO信號處理程序。當緩衝區有數據到來,內核會發送信號告訴上層應用App; 當上層應用App接收到信號後,調用recv函數,因緩衝區有數據,recv函數一般不會阻塞。

   這種用於模型用的比較少,屬於典型的“拉模式(上層應用被動的去Linux內核空間中拉數據)”。即:上層應用App,需要調用recv函數把數據拉進來,會有時間延遲,我們無法避免在延遲時,又有新的信號的產生。

 

(5)異步I/O[並不常用]


   上層應用App調用aio_read函數,同時提交一個應用層的緩衝區buf;調用完畢後,不會阻塞。上層應用程序App可以繼續其他任務; 當TCP/IP協議緩衝區有數據時,Linux主動的把內核數據copy到用戶空間。然後再給上層應用App發送信號;告訴App數據到來,需要處理!

   異步IO屬於典型的“推模式”, 是效率最高的一種模式,上層應用程序App有異步處理的能力(在Linux內核的支持下,處理其他任務的同時,也可支持IO通訊, 與Windows平臺下的完成端口作用類似IOCP)。

 

Select-I/O複用

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. #include <sys/select.h>  
  2. #include <sys/time.h>  
  3. #include <sys/types.h>  
  4. #include <unistd.h>  
  5. int select(int nfds, fd_set *readfds, fd_set *writefds,  
  6.            fd_set *exceptfds, struct timeval *timeout);  

    select實現的是一個管理者的功能: 用select來管理多個IO, 一旦其中的一個IO或者多個IO檢測到我們所感興趣的事件, select就返回, 返回值就是檢測到的事件個數, 並且由第2~4個參數返回那些IO發送了事件, 這樣我們就可以遍歷這些事件, 進而處理這些事件;

參數:

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返回前,會經歷的最大等待時間, 注意此處是指的是相對時間]

  1. //timeval結構:  
  2. struct timeval  
  3. {  
  4.     long    tv_sec;     /* seconds */  
  5.     long    tv_usec;    /* microseconds */  
  6. };  
  7. //一些調用使用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

 

返回值:

  On success, select() and pselect() return the number of  file  descriptors  contained  in  

the  three  returned descriptor sets (that is, the total number of bits that are  set  in  

readfds,  writefds,  exceptfds) which  may  be  zero if the timeout expires before anything 

interesting happens.  On error, -1 is returned, and errno is set appropriately; the sets  

and  timeout  become  undefined, so do not rely on their contents after an error.

   如果成功,返回所有sets中描述符的個數;如果超時,返回0;如果出錯,返回-1.

 

讀, 寫, 異常事件發生條件

可讀:

可寫:

異常:

套接口緩衝區有數據可讀(連接的對等方發送數據過來, 填充了本地套接口緩衝區, 所以導致套接口緩衝區有數據可讀);

套接口發送緩衝區有空間容納數據(因爲大部分時間發送緩衝區是未滿的, 因此我們一般不關心這個事件);

套接口存在帶外數據;

 

連接的讀一半(對端)關閉(對方調用了close), 即接收到FIN段, 讀操作將返回0;

連接的寫一半關閉. 即收到RST段之後, 再次調用write操作;

 

如果是監聽套接口, 已完成隊列不爲空時;

套接口發生了一個錯誤待處理, 錯誤可以通過getsockopt指定SO_ERROR選項來獲取;

 

套接口發生了一個錯誤等待處理, 錯誤可以通過getsockopt指定SO_ERROR選項來獲取;

 

  1. /**示例1: 用select來改進echo回聲服務器的client端的echoClient函數 
  2. 使得可以在單進程的情況下同時監聽多個文件描述符; 
  3. **/  
  4. void echoClient(int sockfd)  
  5. {  
  6.     char buf[512];  
  7.     fd_set rset;  
  8.     //確保標準輸入不會被重定向  
  9.     int fd_stdin = fileno(stdin);  
  10.     int maxfd = fd_stdin > sockfd ? fd_stdin : sockfd;  
  11.     while (true)  
  12.     {  
  13.         FD_ZERO(&rset);  
  14.         FD_SET(fd_stdin, &rset);  
  15.         FD_SET(sockfd, &rset);  
  16.         int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);  
  17.         if (nReady == -1)  
  18.             err_exit("select error");  
  19.         else if (nReady == 0)  
  20.             continue;  
  21.   
  22.         /** nReady > 0: 檢測到了可讀事件 **/  
  23.   
  24.         if (FD_ISSET(fd_stdin, &rset))  
  25.         {  
  26.             memset(buf, 0, sizeof(buf));  
  27.             if (fgets(buf, sizeof(buf), stdin) == NULL)  
  28.                 break;  
  29.             if (writen(sockfd, buf, strlen(buf)) == -1)  
  30.                 err_exit("write socket error");  
  31.         }  
  32.         if (FD_ISSET(sockfd, &rset))  
  33.         {  
  34.             memset(buf, 0, sizeof(buf));  
  35.             int readBytes = readline(sockfd, buf, sizeof(buf));  
  36.             if (readBytes == 0)  
  37.             {  
  38.                 cerr << "server connect closed..." << endl;  
  39.                 exit(EXIT_FAILURE);  
  40.             }  
  41.             else if (readBytes == -1)  
  42.                 err_exit("read-line socket error");  
  43.   
  44.             cout << buf;  
  45.         }  
  46.     }  
  47. }  
  1. /**示例2: 用select來改進echo回聲服務器的server端的接受連接與處理連接部分的代碼: 
  2. 使得可以在單進程的情況下處理多客戶連接, 對於單核的CPU來說, 單進程使用select處理連接與監聽套接字其效率不一定就會比多進程/多線程性能差; 
  3.  
  4. **/  
  5.     struct sockaddr_in clientAddr;  
  6.     socklen_t addrLen;  
  7.     int maxfd = listenfd;  
  8.     fd_set rset;  
  9.     fd_set allset;  
  10.     FD_ZERO(&rset);  
  11.     FD_ZERO(&allset);  
  12.     FD_SET(listenfd, &allset);  
  13.   
  14.     //用於保存已連接的客戶端套接字  
  15.     int client[FD_SETSIZE];  
  16.     for (int i = 0; i < FD_SETSIZE; ++i)  
  17.         client[i] = -1;  
  18.     int maxi = 0;   //用於保存最大的不空閒的位置, 用於select返回之後遍歷數組  
  19.   
  20.     while (true)  
  21.     {  
  22.         rset = allset;  
  23.         int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);  
  24.         if (nReady == -1)  
  25.         {  
  26.             if (errno == EINTR)  
  27.                 continue;  
  28.             err_exit("select error");  
  29.         }  
  30.         //nReady == 0表示超時, 但是此處是一定不會發生的  
  31.         else if (nReady == 0)  
  32.             continue;  
  33.   
  34.         if (FD_ISSET(listenfd, &rset))  
  35.         {  
  36.             addrLen = sizeof(clientAddr);  
  37.             int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);  
  38.             if (connfd == -1)  
  39.                 err_exit("accept error");  
  40.   
  41.             int i;  
  42.             for (i = 0; i < FD_SETSIZE; ++i)  
  43.             {  
  44.                 if (client[i] < 0)  
  45.                 {  
  46.                     client[i] = connfd;  
  47.                     if (i > maxi)  
  48.                         maxi = i;  
  49.                     break;  
  50.                 }  
  51.             }  
  52.             if (i == FD_SETSIZE)  
  53.             {  
  54.                 cerr << "too many clients" << endl;  
  55.                 exit(EXIT_FAILURE);  
  56.             }  
  57.             //打印客戶IP地址與端口號  
  58.             cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)  
  59.                  << ", " << ntohs(clientAddr.sin_port) << endl;  
  60.             //將連接套接口放入allset, 並更新maxfd  
  61.             FD_SET(connfd, &allset);  
  62.             if (connfd > maxfd)  
  63.                 maxfd = connfd;  
  64.   
  65.             if (--nReady <= 0)  
  66.                 continue;  
  67.         }  
  68.   
  69.         /**如果是已連接套接口發生了可讀事件**/  
  70.         for (int i = 0; i <= maxi; ++i)  
  71.             if ((client[i] != -1) && FD_ISSET(client[i], &rset))  
  72.             {  
  73.                 char buf[512] = {0};  
  74.                 int readBytes = readline(client[i], buf, sizeof(buf));  
  75.                 if (readBytes == -1)  
  76.                     err_exit("readline error");  
  77.                 else if (readBytes == 0)  
  78.                 {  
  79.                     cerr << "client connect closed..." << endl;  
  80.                     FD_CLR(client[i], &allset);  
  81.                     close(client[i]);  
  82.                     client[i] = -1;  
  83.                 }  
  84.                 //注意此處: Server從Client獲取數據之後並沒有立即回射回去,  
  85.                 //        而是等待四秒鐘之後再進行回射  
  86.                 sleep(4);  
  87.                 cout << buf;  
  88.                 if (writen(client[i], buf, readBytes) == -1)  
  89.                     err_exit("writen error");  
  90.   
  91.                 if (--nReady <= 0)  
  92.                     break;  
  93.             }  
  94.     }  

完整源代碼請參照:

http://download.csdn.net/detail/hanqing280441589/8486517

發佈了52 篇原創文章 · 獲贊 10 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章