先來談談為什么會出現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:
- int select(
- _In_ int nfds,
- _Inout_ fd_set *readfds,
- _Inout_ fd_set *writefds,
- _Inout_ fd_set *exceptfds,
- _In_ const struct timeval *timeout
- );
函數的返回值,表示准備好的套接字的個數,如果是0,則表示沒有一個准備好(超時就是一種情況),如果是-1(SOCKET_ERROR),表示有錯誤發生,可以使用WSAGetLastError()函數來得到錯誤代碼,從而知道是什么錯誤。
函數的參數,第一個是輸入參數nfds,表示滿足條件的套接字的個數,windows下可以設置為0,因為fd_set結構體中已經包含了這個參數,這個參數已經是多余的了,之所以還存在,只是是為了與FreeBSD兼容。
第二三四參數都是輸入輸出參數(值-結果參數,輸入和輸出會不一樣),表示套接字的可讀、可寫和異常三種狀態的集合。調用select之后,如果指定套接字不可讀或者不可寫,就會從相應隊列中清除,這樣就可以判斷哪些套接字可讀或者可寫。
說明一下,這里的可讀性是指:如果有客戶的連接請求到達,套接口就是可讀的,調用accept能夠立即完成,而不發生阻塞;如果套接口接收隊列緩沖區中的字節數大於0,調用recv或者recvfrom就不會阻塞。可寫性是指,可以向套接字發送數據(套接字創建成功后,就是可寫的)。當然不是套接字可寫就會去發送數據,就像不是看到電話就去打電話一樣,而是由打電話的需求了,才去看電話是否可打;可讀就不一樣了,電話響了,自然要去接電話(除非,你有事忙或者不想接,一般都是要接的)。可讀已經包含了緩沖區中有數據可以讀取,可寫只是說明了緩沖區有空間讓你寫,你需不需要寫就要看你有沒有數據要寫了.關於異常,就是指一些意外情況,自己用的比較少,以后用到了,再過來補上。
第五個參數是等待的最大時間,是一個結構體:struct timeval,它的定義是:
- /*
- * Structure used in select() call, taken from the BSD file sys/time.h.
- */
- struct timeval {
- long tv_sec; /* seconds */
- long tv_usec; /* and microseconds */
- };
具體到秒和微妙,按照等待的時間長短可以分為不等待、等待一定時間、一直等待。對應的設置分別為,(0,0)是不等待,這是select是非阻塞的,(x,y)最大等待時間x秒y微妙(如果有事件就會提前返回,而不繼續等待),NULL表示一直等待,直到有事件發生。這里可以將timeout分別設置成0(不阻塞)或者1微妙(阻塞很短的時間),然后觀察CPU的使用率,會發現設置成非阻塞后,CPU的使用率已下載就上升到了50%左右,這樣可以看出非阻塞占用CPU很多,但利用率不高。
/***********************************************************************************************************/
跟select配合使用的幾個宏和fd_set結構體介紹:
套接字描述符為了方便管理是放在一個集合里的,這個集合是fd_set,它的具體定義是:
- typedef struct fd_set {
- u_int fd_count; /* how many are SET? */
- SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
- } fd_set;
fd_count是集合中已經設置的套接口描述符的數量。fd_array數組保存已經設置的套接口描述符,其中FD_SETSIZE的定義是:
- #ifndef FD_SETSIZE
- #define FD_SETSIZE 64
- #endif /* FD_SETSIZE */
這個默認值在一般的程序中已經夠用,如果需要,可以將其更改為更大的值。
集合的管理操作,比如元素的清空、加入、刪除以及判斷元素是否在集合中都是用宏來完成的。四個宏是:
- FD_ZERO(*set)
- FD_SET(s, *set)
- FD_ISSET(s, *set)
- FD_CLR(s, *set)
下面一一介紹這些宏的作用和定義:
FD_ZERO(*set),是把集合清空(初始化為0,確切的說,是把集合中的元素個數初始化為0,並不修改描述符數組).使用集合前,必須用FD_ZERO初始化,否則集合在棧上作為自動變量分配時,fd_set分配的將是隨機值,導致不可預測的問題。它的宏定義如下:
- #define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
FD_SET(s,*set),向集合中加入一個套接口描述符(如果該套接口描述符s沒在集合中,並且數組中已經設置的個數小於最大個數時,就把該描述符加入到集合中,集合元素個數加1)。這里是將s的值直接放入數組中。它的宏定義如下:
- #define FD_SET(fd, set) do { \
- if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) \
- ((fd_set FAR *)(set))->fd_array[((fd_set FAR *)(set))->fd_count++]=(fd);\
- } while(0)
FD_ISSET(s,*set),檢查描述符是否在集合中,如果在集合中返回非0值,否則返回0. 它的宏定義並沒有給出具體實現,但實現的思路很簡單,就是搜索集合,判斷套接字s是否在數組中。它的宏定義是:
- #define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))
FD_CLR(s,*set),從集合中移出一個套接口描述符(比如一個套接字連接中斷后,就應該移除它)。實現思路是,在數組集合中找到對應的描述符,然后把后面的描述依次前移一個位置,最后把描述符的個數減1. 它的宏定義是:
- #define FD_CLR(fd, set) do { \
- u_int __i; \
- for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
- if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
- while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
- ((fd_set FAR *)(set))->fd_array[__i] = \
- ((fd_set FAR *)(set))->fd_array[__i+1]; \
- __i++; \
- } \
- ((fd_set FAR *)(set))->fd_count--; \
- break; \
- } \
- } \
- } while(0)
/***********************************************************************************************************/
至此,一些基礎的點基本就講完了,然后給出大概流程和一個示例:
1.調用FD_ZERO來初始化套接字狀態;
2.調用FD_SET將感興趣的套接字描述符加入集合中(每次循環都要重新加入,因為select更新后,會將一些沒有滿足條件的套接字移除隊列);
3.設置等待時間后,調用select函數--更新套接字的狀態;
4.調用FD_ISSET,來判斷套接字是否有相應狀態,然后做相應操作,比如,如果套接字可讀,就調用recv函數去接收數據。
關鍵技術:套接字隊列和狀態的表示與處理。
server端得程序如下(套接字管理隊列一個很重要的作用就是保存套接字描述符,因為accept得到的套接字描述符會覆蓋掉原來的套接字描述符,而readfs中的描述符在select后會刪除這些套接字描述符):
- // server.cpp :
- //程序中加入了套接字管理隊列,這樣管理起來更加清晰、方便,當然也可以不用這個東西
- #include "winsock.h"
- #include "stdio.h"
- #pragma comment (lib,"wsock32.lib")
- struct socket_list{
- SOCKET MainSock;
- int num;
- SOCKET sock_array[64];
- };
- void init_list(socket_list *list)
- {
- int i;
- list->MainSock = 0;
- list->num = 0;
- for(i = 0;i < 64;i ++){
- list->sock_array[i] = 0;
- }
- }
- void insert_list(SOCKET s,socket_list *list)
- {
- int i;
- for(i = 0;i < 64; i++){
- if(list->sock_array[i] == 0){
- list->sock_array[i] = s;
- list->num += 1;
- break;
- }
- }
- }
- void delete_list(SOCKET s,socket_list *list)
- {
- int i;
- for(i = 0;i < 64; i++){
- if(list->sock_array[i] == s){
- list->sock_array[i] = 0;
- list->num -= 1;
- break;
- }
- }
- }
- void make_fdlist(socket_list *list,fd_set *fd_list)
- {
- int i;
- FD_SET(list->MainSock,fd_list);
- for(i = 0;i < 64;i++){
- if(list->sock_array[i] > 0){
- FD_SET(list->sock_array[i],fd_list);
- }
- }
- }
- int main(int argc, char* argv[])
- {
- SOCKET s,sock;
- struct sockaddr_in ser_addr,remote_addr;
- int len;
- char buf[128];
- WSAData wsa;
- int retval;
- struct socket_list sock_list;
- fd_set readfds,writefds,exceptfds;
- timeval timeout; //select的最多等待時間,防止一直等待
- int i;
- unsigned long arg;
- WSAStartup(0x101,&wsa);
- s = socket(AF_INET,SOCK_STREAM,0);
- ser_addr.sin_family = AF_INET;
- ser_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
- ser_addr.sin_port = htons(0x1234);
- bind(s,(sockaddr*)&ser_addr,sizeof(ser_addr));
- listen(s,5);
- timeout.tv_sec = 5; //如果套接字集合中在1s內沒有數據,select就會返回,超時select返回0
- timeout.tv_usec = 0;
- init_list(&sock_list);
- FD_ZERO(&readfds);
- FD_ZERO(&writefds);
- FD_ZERO(&exceptfds);
- sock_list.MainSock = s;
- arg = 1;
- ioctlsocket(sock_list.MainSock,FIONBIO,&arg);
- while(1){
- make_fdlist(&sock_list,&readfds);
- //make_fdlist(&sock_list,&writefds);
- //make_fdlist(&sock_list,&exceptfds);
- retval = select(0,&readfds,&writefds,&exceptfds,&timeout); //超過這個時間,就不阻塞在這里,返回一個0值。
- if(retval == SOCKET_ERROR){
- retval = WSAGetLastError();
- break;
- }
- else if(retval == 0) {
- printf("select() is time-out! There is no data or new-connect coming!\n");
- continue;
- }
- if(FD_ISSET(sock_list.MainSock,&readfds)){
- len = sizeof(remote_addr);
- sock = accept(sock_list.MainSock,(sockaddr*)&remote_addr,&len);
- if(sock == SOCKET_ERROR)
- continue;
- printf("accept a connection\n");
- insert_list(sock,&sock_list);
- }
- for(i = 0;i < 64;i++){
- if(sock_list.sock_array[i] == 0)
- continue;
- sock = sock_list.sock_array[i];
- if(FD_ISSET(sock,&readfds)){
- retval = recv(sock,buf,128,0);
- if(retval == 0){
- closesocket(sock);
- printf("close a socket\n");
- delete_list(sock,&sock_list);
- continue;
- }else if(retval == -1){
- retval = WSAGetLastError();
- if(retval == WSAEWOULDBLOCK)
- continue;
- closesocket(sock);
- printf("close a socket\n");
- delete_list(sock,&sock_list); //連接斷開后,從隊列中移除該套接字
- continue;
- }
- buf[retval] = 0;
- printf("->%s\n",buf);
- send(sock,"ACK by server",13,0);
- }
- //if(FD_ISSET(sock,&writefds)){
- //}
- //if(FD_ISSET(sock,&exceptfds)){
- }
- FD_ZERO(&readfds);
- FD_ZERO(&writefds);
- FD_ZERO(&exceptfds);
- }
- closesocket(sock_list.MainSock);
- WSACleanup();
- return 0;
- }
關於linux下的select跟windows下的區別還有待學習。
參考書籍:
《WinSock網絡編程經絡》第19章
《UNIX環境高級編程》