socket編程中select()函數的作用

select()在SOCKET編程中還是比較重要的,可是對於初學SOCKET的人來說都不太愛用select()寫程序,他們只是習慣寫諸如 conncet()、accept()、recv()或recvfrom()這樣的阻塞程序(所謂阻塞方式block,顧名思義,就是進程或是線程執行到這些函數時必須等待某個事件發生,如果事件沒有發生,進程或線程就被阻塞,函數不能立即返回)。可是使用select()就可以完成非阻塞(所謂非阻塞方式non-block,就是進程或線程執行此函數時不必非要等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況。如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率高)方式工作的程序,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。下面詳細介紹一下!

select()函數的格式(我所說的是Unix系統下的Berkeley Socket編程,和Windows下的有區別,一會兒說明)

int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);

先說明兩個結構體:

  第一:struct fd_set可以理解爲一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄,這可以是我們所說的普通意義的文件,當然Unix下任何設備、管道、FIFO等都是文件形式,全部包括在內,所以,毫無疑問,一個socket就是一個文件,socket句柄就是一個文件描述符。fd_set集合可以通過一些宏由人爲來操作,比如清空集合:FD_ZERO(fd_set*),將一個給定的文件描述符加入集合之中FD_SET(int, fd_set*),將一個給定的文件描述符從集合中刪除FD_CLR(int,   fd_set*),檢查集合中指定的文件描述符是否可以讀寫FD_ISSET(int, fd_set*)。一會兒舉例說明。

  第二:struct timeval是一個大家常用的結構,用來代表時間值,有兩個成員,一個是秒數,另一個毫秒數。
 

  具體解釋select的參數:

  int maxfdp是一個整數值,是指集合中所有文件描述符的範圍,即所有文件描述符的最大值加1,不能錯!在Windows中這個參數值無所謂,可以設置不正確。

  fd_set* readfds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的讀變化的,即我們關心是否可以從這些文件中讀取數據了,如果這個集合中有一個文件可讀,select就會返回一個大於0的值,表示有文件可讀,如果沒有可讀的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的讀變化。

  fd_set* writefds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的寫變化的,即我們關心是否可以向這些文件中寫入數據了,如果這個集合中有一個文件可寫,select就會返回一個大於0的值,表示有文件可寫,如果沒有可寫的文件,則根據timeout再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的寫變化。

  fe_set* errorfds同上面兩個參數的意圖,用來監視文件錯誤異常。

  struct timeval* timeout是select的超時時間,這個參數至關重要,它可以使select處於三種狀態。

  第一:若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化爲止;
  第二:若將時間值設爲0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;
  第三:timeout的值大於0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。

  返回值:
  負值:select錯誤
  正值:某些文件可讀寫或出錯
  0:等待超時,沒有可讀寫或錯誤的文件

  在有了select後可以寫出像樣的網絡程序來!舉個簡單的例子,就是從網絡上接受數據寫入一個文件中。

  ---------------------------無連接     例子:

 int main()
  {
      int sock;
      FILE* fp;
      struct fd_set fds;
      struct timeval timeout = {3, 0}; //select 等待3秒,3秒輪詢, 要非阻塞就置0
      char buffer[256] = {0}; //256字節的接收緩衝區
      /*假設已經建立UDP連接,具體過程不寫,簡單,當然TCP也同理,主機ip和port都已經給定,要寫的文件已經打開
      sock = socket(...);
      bind(...);
      fp = fopen(...); */
      while(1)
      {
          FD_ZERO(&fds); //每次循環都要清空,否則不能檢測描述符變化
          FD_SET(sock, &fds); //添加描述符
          FD_SET(fp, &fds); //同上
          maxfdp = sock>fp?sock+1:fp+1; //描述符最大值加1
          switch(select(maxfdp, &fds, &fds, NULL, &timeout)) //select使用
          {
              case SOCKET_ERROR: exit(-1); break; //select錯誤,退出程序
              case 0: break; //再次輪詢
              default:
                  if(FD_ISSET(sock, &fds)) //測試sock是否可讀,即是否網絡上有數據
                  {
                      recvfrom(sock, buffer, 256, .... ); //接受網絡數據
                      if(FD_ISSET(fp, &fds)) //測試文件是否可寫
                      fwrite(fp, buffer...); //寫入文件
                      buffer清空;
                  } //end if break
          } //end switch
      } //end while
  } //end main

---------------------------面向連接

  #include <winsock.h>
   #include <stdio.h>
   #define PORT       5150
   #define MSGSIZE     1024
   #pragma comment(lib, "ws2_32.lib")
   int     g_iTotalConn = 0;
   SOCKET g_CliSocketArr[FD_SETSIZE];
   DWORD WINAPI WorkerThread(LPVOID lpParameter);
   int main()
   {   
       WSADATA     wsaData;   
       SOCKET       sListen, sClient;   
       SOCKADDR_IN local, client;   
       int         iaddrSize = sizeof(SOCKADDR_IN);   
       DWORD       dwThreadId;   
       // Initialize Windows socket library   
       WSAStartup(0x0202, &wsaData);   
       // Create listening socket   
       sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);   
       // Bind           
       local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
       local.sin_family = AF_INET;
       local.sin_port = htons(PORT);   
       bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));   
       // Listen   listen(sListen, 3);   
       // Create worker thread   
       CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);     
       while (TRUE)   
       {               // Accept a connection     
           sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);     
           printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));     
           // Add socket to g_CliSocketArr     
           g_CliSocketArr[g_iTotalConn++] = sClient;   
       }     
       return 0;
   }

   DWORD WINAPI WorkerThread(LPVOID lpParam)
   {   
       int             i;   
       fd_set         fdread;   
       int             ret;   
       struct timeval tv = {1, 0};   
       char           szMessage[MSGSIZE];     
       while (TRUE)   
       {     
           FD_ZERO(&fdread);     
           for (i = 0; i < g_iTotalConn; i++)
           {
               FD_SET(g_CliSocketArr, &fdread);
           }                     // We only care read event
           ret = select(0, &fdread, NULL, NULL, &tv);
           if (ret == 0)
           {       // Time expired
               continue;
           }
           for (i = 0; i < g_iTotalConn; i++)
           {
               if (FD_ISSET(g_CliSocketArr, &fdread))
                 {         // A read event happened on g_CliSocketArr
                     ret = recv(g_CliSocketArr, szMessage, MSGSIZE, 0);
                     if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
                       {
                           // Client socket closed           
                           printf("Client socket %d closed.\n", g_CliSocketArr);
                           closesocket(g_CliSocketArr);
                           if (i < g_iTotalConn - 1)
                           {
                               g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
                           }
                       }
                       else
                       {
                             // We received a message from client
                             szMessage[ret] = '\0';
                             send(g_CliSocketArr, szMessage, strlen(szMessage), 0);
                       }
                 } //if
           }//for
       }//while     
       return 0;
   }

服務器的幾個主要動作如下:

   1.創建監聽套接字,綁定,監聽;
   2.創建工作者線程;
   3.創建一個套接字數組,用來存放當前所有活動的客戶端套接字,每accept一個連接就更新一次數組;
   4.接受客戶端的連接。
   這裏有一點需要注意的,就是我沒有重新定義FD_SETSIZE宏,所以服務器最多支持的併發連接數爲64。而且,這裏決不能無條件的ccept,服務器應該根據當前的連接數來決定
是否接受來自某個客戶端的連接。一種比較好的實現方案就是採用WSAAccept函數,而且讓WSAAccept回調自己實現的Condition Function。

   如下所示:

   int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR *
g,DWORD dwCallbackData)
   {
       if (當前連接數 < FD_SETSIZE)
           return CF_ACCEPT;
       else   
           return CF_REJECT;
   }

 工作者線程裏面是一個死循環,一次循環完成的動作是:

   1.將當前所有的客戶端套接字加入到讀集fdread中;

   2.調用select函數;

   3.查看某個套接字是否仍然處於讀集中,如果是,則接收數據。如果接收的數據長度爲0,或者發生WSAECONNRESET錯誤,則表示客戶端套接字主動關閉,這時需要將服務器中對應的套接字所綁定的資源釋放掉,然後調整我們的套接字數組(將數組中最後一個套接字挪到當前的位置上)。

   除了需要有條件接受客戶端的連接外,還需要在連接數爲0的情形下做特殊處理,因爲如果讀集中沒有任何套接字,select函數會立刻返回,這將導致工作者線程成爲一個毫無停頓的死循環,CPU的佔用率馬上達到100%。

   關係到套接字列表的操作都需要使用循環,在輪詢的時候,需要遍歷一次,再新的一輪開始時,將列表加入隊列又需要遍歷一次.也就是說,Select在工作一次時,需要至少遍歷2次列表,這是它效率較低的原因之一.

   在大規模的網絡連接方面,還是推薦使用IOCP或EPOLL模型.但是Select模型可以使用在諸如對戰類遊戲上,比如類似星際這種,因爲它小巧易於實現,且對戰類遊戲的網絡連接量並不大. 對於Select模型想要突破Windows 64個限制的話,可以採取分段輪詢,一次輪詢64個.例如套接字列表爲128個,在第一次輪詢時,將前64個放入隊列中用Select進行狀態查詢,待本次操作全部結束後.將後64個再加入輪詢隊列中進行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支持無限多個.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章