windows和linux套接字中的select機制淺析


先來談談為什么會出現select函數,也就是select是解決什么問題的?

平常使用的recv函數時阻塞的,也就是如果沒有數據可讀,recv就會一直阻塞在那里,這是如果有另外一個連接過來,就得一直等待,這樣實時性就不是太好。

這個問題的幾個解決方法:1. 使用ioctlsocket函數,將recv函數設置成非阻塞的,這樣不管套接字上有沒有數據都會立刻返回,可以重復調用recv函數,這種方式叫做輪詢(polling),但是這樣效率很是問題,因為,大多數時間實際上是無數據可讀的,花費時間不斷反復執行read系統調用,這樣就比較浪費CPU的時間。並且循環之間的間隔不好確定。2. 使用fork,使用多進程來解決,這里終止會比較復雜(待研究)。 3.使用多線程來解決,這樣避免了終止的復雜性,但卻要求處理線程之間的同步,在減少復雜性方面這可能會得不償失。4. 使用異步IO(待研究)。5. 就是本文所使用的I/O多路轉接(多路復用)--其實就是在套接字阻塞和非阻塞之間做了一個均衡,我們稱之為半阻塞。

經過對select的初步了解,在windows和linux下的實現小有區別,所以分開來寫。這里先寫windows下的select機制。

select的大概思想:將多個套接字放在一個集合里,然后統一檢查這些套接字的狀態(可讀、可寫、異常等),調用select后,會更新這些套接字的狀態,然后做判斷,如果套接字可讀,就執行read操作。這樣就巧妙地避免了阻塞,達到同時處理多個連接的目的。當然如果沒有事件發生,select會一直阻塞,如果不想一直讓它等待,想去處理其它事情,可以設置一個最大的等待時間。

/***********************************************************************************************************/

下面具體講講函數的參數,參見MSDN的解釋http://msdn.microsoft.com/en-us/library/windows/desktop/ms740141(v=vs.85).aspx

 

[cpp]  view plain copy
 
  1. int select(  
  2.   _In_     int nfds,  
  3.   _Inout_  fd_set *readfds,  
  4.   _Inout_  fd_set *writefds,  
  5.   _Inout_  fd_set *exceptfds,  
  6.   _In_     const struct timeval *timeout  
  7. );  


函數的返回值,表示准備好的套接字的個數,如果是0,則表示沒有一個准備好(超時就是一種情況),如果是-1(SOCKET_ERROR),表示有錯誤發生,可以使用WSAGetLastError()函數來得到錯誤代碼,從而知道是什么錯誤。

 

函數的參數,第一個是輸入參數nfds,表示滿足條件的套接字的個數,windows下可以設置為0,因為fd_set結構體中已經包含了這個參數,這個參數已經是多余的了,之所以還存在,只是是為了與FreeBSD兼容。

第二三四參數都是輸入輸出參數(值-結果參數,輸入和輸出會不一樣),表示套接字的可讀、可寫和異常三種狀態的集合。調用select之后,如果指定套接字不可讀或者不可寫,就會從相應隊列中清除,這樣就可以判斷哪些套接字可讀或者可寫。 

說明一下,這里的可讀性是指:如果有客戶的連接請求到達,套接口就是可讀的,調用accept能夠立即完成,而不發生阻塞;如果套接口接收隊列緩沖區中的字節數大於0,調用recv或者recvfrom就不會阻塞。可寫性是指,可以向套接字發送數據(套接字創建成功后,就是可寫的)。當然不是套接字可寫就會去發送數據,就像不是看到電話就去打電話一樣,而是由打電話的需求了,才去看電話是否可打;可讀就不一樣了,電話響了,自然要去接電話(除非,你有事忙或者不想接,一般都是要接的)。可讀已經包含了緩沖區中有數據可以讀取,可寫只是說明了緩沖區有空間讓你寫,你需不需要寫就要看你有沒有數據要寫了.關於異常,就是指一些意外情況,自己用的比較少,以后用到了,再過來補上。

第五個參數是等待的最大時間,是一個結構體:struct timeval,它的定義是:

 

[cpp]  view plain copy
 
  1. /* 
  2. * Structure used in select() call, taken from the BSD file sys/time.h. 
  3. */  
  4. struct timeval {  
  5.         long    tv_sec;         /* seconds */  
  6.         long    tv_usec;        /* and microseconds */  
  7. };  

具體到秒和微妙,按照等待的時間長短可以分為不等待、等待一定時間、一直等待。對應的設置分別為,(0,0)是不等待,這是select是非阻塞的,(x,y)最大等待時間x秒y微妙(如果有事件就會提前返回,而不繼續等待),NULL表示一直等待,直到有事件發生。這里可以將timeout分別設置成0(不阻塞)或者1微妙(阻塞很短的時間),然后觀察CPU的使用率,會發現設置成非阻塞后,CPU的使用率已下載就上升到了50%左右,這樣可以看出非阻塞占用CPU很多,但利用率不高。

 

/***********************************************************************************************************/

跟select配合使用的幾個宏和fd_set結構體介紹:

套接字描述符為了方便管理是放在一個集合里的,這個集合是fd_set,它的具體定義是:

 

[cpp]  view plain copy
 
  1. typedef struct fd_set {  
  2.         u_int   fd_count;               /* how many are SET? */  
  3.         SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */  
  4. } fd_set;  

fd_count是集合中已經設置的套接口描述符的數量。fd_array數組保存已經設置的套接口描述符,其中FD_SETSIZE的定義是:

 

 

[cpp]  view plain copy
 
  1. #ifndef FD_SETSIZE  
  2. #define FD_SETSIZE      64  
  3. #endif /* FD_SETSIZE */  

這個默認值在一般的程序中已經夠用,如果需要,可以將其更改為更大的值。

 

集合的管理操作,比如元素的清空、加入、刪除以及判斷元素是否在集合中都是用宏來完成的。四個宏是:

 

[html]  view plain copy
 
  1. FD_ZERO(*set)  
  2. FD_SET(s, *set)  
  3. FD_ISSET(s, *set)  
  4. FD_CLR(s, *set)  

下面一一介紹這些宏的作用和定義:

 

FD_ZERO(*set),是把集合清空(初始化為0,確切的說,是把集合中的元素個數初始化為0,並不修改描述符數組).使用集合前,必須用FD_ZERO初始化,否則集合在棧上作為自動變量分配時,fd_set分配的將是隨機值,導致不可預測的問題。它的宏定義如下:

 

[cpp]  view plain copy
 
  1. #define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)  

FD_SET(s,*set),向集合中加入一個套接口描述符(如果該套接口描述符s沒在集合中,並且數組中已經設置的個數小於最大個數時,就把該描述符加入到集合中,集合元素個數加1)。這里是將s的值直接放入數組中。它的宏定義如下:

 

 

[cpp]  view plain copy
 
  1. #define FD_SET(fd, set) do { \  
  2.     if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) \  
  3.         ((fd_set FAR *)(set))->fd_array[((fd_set FAR *)(set))->fd_count++]=(fd);\  
  4. while(0)  

FD_ISSET(s,*set),檢查描述符是否在集合中,如果在集合中返回非0值,否則返回0. 它的宏定義並沒有給出具體實現,但實現的思路很簡單,就是搜索集合,判斷套接字s是否在數組中。它的宏定義是:

 

 

[cpp]  view plain copy
 
  1. #define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))  

FD_CLR(s,*set),從集合中移出一個套接口描述符(比如一個套接字連接中斷后,就應該移除它)。實現思路是,在數組集合中找到對應的描述符,然后把后面的描述依次前移一個位置,最后把描述符的個數減1. 它的宏定義是:

 

 

[cpp]  view plain copy
 
  1. #define FD_CLR(fd, set) do { \  
  2.     u_int __i; \  
  3.     for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \  
  4.         if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \  
  5.             while (__i < ((fd_set FAR *)(set))->fd_count-1) { \  
  6.                 ((fd_set FAR *)(set))->fd_array[__i] = \  
  7.                     ((fd_set FAR *)(set))->fd_array[__i+1]; \  
  8.                 __i++; \  
  9.             } \  
  10.             ((fd_set FAR *)(set))->fd_count--; \  
  11.             break; \  
  12.         } \  
  13.     } \  
  14. while(0)  


/***********************************************************************************************************/

 

至此,一些基礎的點基本就講完了,然后給出大概流程和一個示例:

1.調用FD_ZERO來初始化套接字狀態;

2.調用FD_SET將感興趣的套接字描述符加入集合中(每次循環都要重新加入,因為select更新后,會將一些沒有滿足條件的套接字移除隊列);

3.設置等待時間后,調用select函數--更新套接字的狀態;

4.調用FD_ISSET,來判斷套接字是否有相應狀態,然后做相應操作,比如,如果套接字可讀,就調用recv函數去接收數據。

關鍵技術:套接字隊列和狀態的表示與處理。

server端得程序如下(套接字管理隊列一個很重要的作用就是保存套接字描述符,因為accept得到的套接字描述符會覆蓋掉原來的套接字描述符,而readfs中的描述符在select后會刪除這些套接字描述符):

 

[cpp]  view plain copy
 
  1. // server.cpp :   
  2. //程序中加入了套接字管理隊列,這樣管理起來更加清晰、方便,當然也可以不用這個東西  
  3.   
  4. #include "winsock.h"  
  5. #include "stdio.h"  
  6. #pragma comment (lib,"wsock32.lib")  
  7. struct socket_list{  
  8.     SOCKET MainSock;  
  9.     int num;  
  10.     SOCKET sock_array[64];  
  11. };  
  12. void init_list(socket_list *list)  
  13. {  
  14.     int i;  
  15.     list->MainSock = 0;  
  16.     list->num = 0;  
  17.     for(i = 0;i < 64;i ++){  
  18.         list->sock_array[i] = 0;  
  19.     }  
  20. }  
  21. void insert_list(SOCKET s,socket_list *list)  
  22. {  
  23.     int i;  
  24.     for(i = 0;i < 64; i++){  
  25.         if(list->sock_array[i] == 0){  
  26.             list->sock_array[i] = s;  
  27.             list->num += 1;  
  28.             break;  
  29.         }  
  30.     }  
  31. }  
  32. void delete_list(SOCKET s,socket_list *list)  
  33. {  
  34.     int i;  
  35.     for(i = 0;i < 64; i++){  
  36.         if(list->sock_array[i] == s){  
  37.             list->sock_array[i] = 0;  
  38.             list->num -= 1;  
  39.             break;  
  40.         }  
  41.     }  
  42. }  
  43. void make_fdlist(socket_list *list,fd_set *fd_list)  
  44. {  
  45.     int i;  
  46.     FD_SET(list->MainSock,fd_list);  
  47.     for(i = 0;i < 64;i++){  
  48.         if(list->sock_array[i] > 0){  
  49.             FD_SET(list->sock_array[i],fd_list);  
  50.         }  
  51.     }  
  52. }  
  53. int main(int argc, char* argv[])  
  54. {  
  55.     SOCKET s,sock;  
  56.     struct sockaddr_in ser_addr,remote_addr;  
  57.     int len;  
  58.     char buf[128];  
  59.     WSAData wsa;  
  60.     int retval;  
  61.     struct socket_list sock_list;  
  62.     fd_set readfds,writefds,exceptfds;  
  63.     timeval timeout;        //select的最多等待時間,防止一直等待  
  64.     int i;  
  65.     unsigned long arg;  
  66.   
  67.     WSAStartup(0x101,&wsa);  
  68.     s = socket(AF_INET,SOCK_STREAM,0);  
  69.     ser_addr.sin_family = AF_INET;  
  70.     ser_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  
  71.     ser_addr.sin_port = htons(0x1234);  
  72.     bind(s,(sockaddr*)&ser_addr,sizeof(ser_addr));  
  73.   
  74.     listen(s,5);  
  75.     timeout.tv_sec = 5;     //如果套接字集合中在1s內沒有數據,select就會返回,超時select返回0  
  76.     timeout.tv_usec = 0;  
  77.     init_list(&sock_list);  
  78.     FD_ZERO(&readfds);  
  79.     FD_ZERO(&writefds);  
  80.     FD_ZERO(&exceptfds);  
  81.     sock_list.MainSock = s;  
  82.     arg = 1;  
  83.     ioctlsocket(sock_list.MainSock,FIONBIO,&arg);  
  84.     while(1){  
  85.         make_fdlist(&sock_list,&readfds);  
  86.         //make_fdlist(&sock_list,&writefds);  
  87.         //make_fdlist(&sock_list,&exceptfds);  
  88.   
  89.         retval = select(0,&readfds,&writefds,&exceptfds,&timeout);     //超過這個時間,就不阻塞在這里,返回一個0值。  
  90.         if(retval == SOCKET_ERROR){  
  91.             retval = WSAGetLastError();  
  92.             break;  
  93.         }  
  94.         else if(retval == 0) {  
  95.             printf("select() is time-out! There is no data or new-connect coming!\n");  
  96.             continue;  
  97.         }  
  98.         if(FD_ISSET(sock_list.MainSock,&readfds)){  
  99.             len = sizeof(remote_addr);  
  100.             sock = accept(sock_list.MainSock,(sockaddr*)&remote_addr,&len);  
  101.             if(sock == SOCKET_ERROR)  
  102.                 continue;  
  103.             printf("accept a connection\n");  
  104.             insert_list(sock,&sock_list);  
  105.         }  
  106.         for(i = 0;i < 64;i++){  
  107.             if(sock_list.sock_array[i] == 0)  
  108.                 continue;  
  109.             sock = sock_list.sock_array[i];  
  110.             if(FD_ISSET(sock,&readfds)){  
  111.                 retval = recv(sock,buf,128,0);  
  112.                 if(retval == 0){  
  113.                     closesocket(sock);  
  114.                     printf("close a socket\n");  
  115.                     delete_list(sock,&sock_list);  
  116.                     continue;  
  117.                 }else if(retval == -1){  
  118.                     retval = WSAGetLastError();  
  119.                     if(retval == WSAEWOULDBLOCK)  
  120.                         continue;  
  121.                     closesocket(sock);  
  122.                     printf("close a socket\n");  
  123.                     delete_list(sock,&sock_list);   //連接斷開后,從隊列中移除該套接字  
  124.                     continue;  
  125.                 }  
  126.                 buf[retval] = 0;  
  127.                 printf("->%s\n",buf);  
  128.                 send(sock,"ACK by server",13,0);  
  129.             }  
  130.             //if(FD_ISSET(sock,&writefds)){  
  131.             //}  
  132.             //if(FD_ISSET(sock,&exceptfds)){  
  133.               
  134.         }  
  135.         FD_ZERO(&readfds);  
  136.         FD_ZERO(&writefds);  
  137.         FD_ZERO(&exceptfds);  
  138.     }  
  139.     closesocket(sock_list.MainSock);  
  140.     WSACleanup();  
  141.     return 0;  
  142. }  

 

關於linux下的select跟windows下的區別還有待學習。

 

參考書籍:

《WinSock網絡編程經絡》第19章

《UNIX環境高級編程》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM