1. 程式人生 > >Socket超時設定---select()的妙用

Socket超時設定---select()的妙用

http://fanqiang.chinaunix.net/a4/b7/20010913/0900001283.html

【 原文由 cpu 所發表 】
 
用過 WinSock API 網友們知道:WinSock 程式設計中有一很方便的地方便是其
息驅動機制,不管是底層 API 的 WSAAsyncSelect() 還是 MFC 的非同步Socket類:
CAsyncSocket,都提供了諸如 FD_ACCEPT、FD_READ、FD_CLOSE 之類的訊息
供程式設計人員捕捉並處理。FD_ACCEPT 通知程序有客戶方Socket請求連線,
FD_READ通知程序本地Socket有東東可讀,FD_CLOSE通知程序對方Socket已
關閉。那麼,BSD Socket 是不是真的相形見拙呢?
 
非也! 'cause cpu love unix so.
 
BSD UNIX中有一系統呼叫芳名select()完全可以提供類似的訊息驅動機制。
cpu鄭重宣佈:WinSock的WSAAsyncSeclet()不過是此select()的fork版!
 
bill也是fork出來的嘛,xixi.
 
select()的機制中提供一fd_set的資料結構,實際上是一long型別的陣列,
每一個數組元素都能與一開啟的檔案控制代碼(不管是Socket控制代碼,還是其他
檔案或命名管道或裝置控制代碼)建立聯絡,建立聯絡的工作由程式設計師完成,
當呼叫select()時,由核心根據IO狀態修改fd_set的內容,由此來通知執
行了select()的程序哪一Socket或檔案可讀,下面具體解釋:
 
#include  <sys/types.h>
#include  <sys/times.h>
#include  <sys/select.h>
 
int select(nfds, readfds, writefds, exceptfds, timeout)
int nfds;
fd_set *readfds, *writefds, *exceptfds;
struct timeval *timeout;
 
ndfs:select監視的檔案控制代碼數,視程序中開啟的檔案數而定,一般設為呢要監視各檔案
      中的最大檔案號加一。
readfds:select監視的可讀檔案控制代碼集合。
writefds: select監視的可寫檔案控制代碼集合。
exceptfds:select監視的異常檔案控制代碼集合。
timeout:本次select()的超時結束時間。(見/usr/sys/select.h,
        可精確至百萬分之一秒!)
 
當readfds或writefds中映象的檔案可讀或可寫或超時,本次select()
就結束返回。程式設計師利用一組系統提供的巨集在select()結束時便可判
斷哪一檔案可讀或可寫。對Socket程式設計特別有用的就是readfds。
幾隻相關的巨集解釋如下:
 
FD_ZERO(fd_set *fdset):清空fdset與所有檔案控制代碼的聯絡。
FD_SET(int fd, fd_set *fdset):建立檔案控制代碼fd與fdset的聯絡。
FD_CLR(int fd, fd_set *fdset):清除檔案控制代碼fd與fdset的聯絡。
FD_ISSET(int fd, fdset *fdset):檢查fdset聯絡的檔案控制代碼fd是否
                                可讀寫,>0表示可讀寫。
(關於fd_set及相關巨集的定義見/usr/include/sys/types.h)
 
這樣,你的socket只需在有東東讀的時候才讀入,大致如下:
 
...
int     sockfd;
fd_set  fdR;
struct  timeval timeout = ..;
...
for(;;) {
        FD_ZERO(&fdR);
        FD_SET(sockfd, &fdR);
        switch (select(sockfd + 1, &fdR, NULL, &timeout)) {
                case -1:
                        error handled by u;
                case 0:
                        timeout hanled by u;
                default:
                        if (FD_ISSET(sockfd)) {
                                now u read or recv something;
                                /* if sockfd is father and 
                                server socket, u can now
                                accept() */
                        }
        }
}
 
所以一個FD_ISSET(sockfd)就相當通知了sockfd可讀。
至於struct timeval在此的功能,請man select。不同的timeval設定
使使select()表現出超時結束、無超時阻塞和輪詢三種特性。由於
timeval可精確至百萬分之一秒,所以Windows的SetTimer()根本不算
什麼。你可以用select()做一個超級時鐘。
 
FD_ACCEPT的實現?依然如上,因為客戶方socket請求連線時,會發送
連線請求報文,此時select()當然會結束,FD_ISSET(sockfd)當然大
於零,因為有報文可讀嘛!至於這方面的應用,主要在於服務方的父
Socket,你若不喜歡主動accept(),可改為如上機制來accept()。
 
至於FD_CLOSE的實現及處理,頗費了一堆cpu處理時間,未完待續。
 
--
討論關於利用select()檢測對方Socket關閉的問題:
 
仍然是本地Socket有東東可讀,因為對方Socket關閉時,會發一個關閉連線
通知報文,會馬上被select()檢測到的。關於TCP的連線(三次握手)和關
閉(二次握手)機制,敬請參考有關TCP/IP的書籍。
 
不知是什麼原因,UNIX好象沒有提供通知程序關於Socket或Pipe對方關閉的
訊號,也可能是cpu所知有限。總之,當對方關閉,一執行recv()或read(),
馬上回返回-1,此時全域性變數errno的值是115,相應的sys_errlist[errno]
為"Connect refused"(請參考/usr/include/sys/errno.h)。所以,在上
篇的for(;;)...select()程式塊中,當有東西可讀時,一定要檢查recv()或
read()的返回值,返回-1時要作出關斷本地Socket的處理,否則select()會
一直認為有東西讀,其結果曾幾令cpu傷心欲斷針腳。不信你可以試試:不檢
查recv()返回結果,且將收到的東東(實際沒收到)寫至標準輸出...
在有名管道的程式設計中也有類似問題出現。具體處理詳見拙作:釋出一個有用
的Socket客戶方原碼。
 
至於主動寫Socket時對方突然關閉的處理則可以簡單地捕捉訊號SIGPIPE並作
出相應關斷本地Socket等等的處理。SIGPIPE的解釋是:寫入無讀者方的管道。
在此不作贅述,請詳man signal。
 
以上是cpu在作tcp/ip資料傳輸實驗積累的經驗,若有錯漏,請狂炮擊之。
 
唉,昨天在hacker區被一幫孫子轟得差點兒沒短路。ren cpu(奔騰的心) z80
 
補充關於select在非同步(非阻塞)connect中的應用,剛開始搞socket程式設計的時候
我一直都用阻塞式的connect,非阻塞connect的問題是由於當時搞proxy scan
而提出的呵呵
通過在網上與網友們的交流及查詢相關FAQ,總算知道了怎麼解決這一問題.同樣
用select可以很好地解決這一問題.大致過程是這樣的:
 
1.將開啟的socket設為非阻塞的,可以用fcntl(socket, F_SETFL, O_NDELAY)完
成(有的系統用FNEDLAY也可).
 
2.發connect呼叫,這時返回-1,但是errno被設為EINPROGRESS,意即connect仍舊
在進行還沒有完成.
 
3.將開啟的socket設進被監視的可寫(注意不是可讀)檔案集合用select進行監視,
如果可寫,用
        getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int));
來得到error的值,如果為零,則connect成功.
 
在許多unix版本的proxyscan程式你都可以看到類似的過程,另外在solaris精華
區->程式設計技巧中有一個通用的帶超時引數的connect模組.


===========================================

阻塞與非阻塞



在 server 進入 listen 狀態之後, 我們下面分別討論兩種實現方法:
1. fcntl 方法
對一個檔案描述符指定的檔案或裝置, 有兩種工作方式: 阻塞與非阻塞, 阻塞的意思是指, 當試圖對該檔案描述符進行讀寫時, 如果當時沒有東西可讀, 或者暫時不可寫, 程式就進入等待狀態, 直到有東西可讀或者可寫為止. 而對於非阻塞狀態, 如果沒有東西可讀, 或者不可寫, 讀寫函式馬上返回, 而不會等待. 預設情況下, 檔案描述符處於阻塞狀態. 在實現聊天室時, server 輪流查詢與各client 建立 socket, 一旦可讀就將該 socket 中的字元讀出來並向所有其他client 傳送. 並且, server 還要隨時檢視是否有新的 client 試圖建立連線, 這樣, 如果 server 在任何一個地方阻塞了, 其他 client 傳送的內容就會受到影響, 新 client 試圖建立連線也會受到影響. 因此, 我們使用 fcntl 將該

檔案描述符變為非阻塞的:

fcntl( sockfd, F_SETFL, O_NONBLOCK);
// sockfd      是要改變狀態的檔案描述符.
// F_SETFL     表明要改變檔案描述符的狀態
// O_NONBLOCK  表示將檔案描述符變為非阻塞的.

使用自然語言描述聊天室 server :
while ( 1)
{
   if 有新連線 then 建立並記錄該新連線;
   for( 所有的有效連線)
   {
      if 該連線中有字元可讀 then
      {
         for( 所有其他的有效連線)
         {
           將該字串傳送給該連線;
         }
      }
   }
}

由於判斷是否有新連線, 是否可讀都是非阻塞的, 因此每次判斷, 不管有還是沒有, 都會馬上返回. 這樣, 任何一個 client 向 server 傳送字元或者試圖建立新連線, 都不會對其他 client 的活動造成影響. 對 client 而言, 建立連線之後, 只需要處理兩個檔案描述符, 一個是建立了連線的 socket 描述符, 另一個是標準輸入. 和 server 一樣, 如果使用阻塞方式的話, 很容易因為其中一個暫時沒有輸入而影響另外一個的讀入. 因此將它們都變成非阻塞的, 然後client 進行如下動作:
while( 不想退出)
{
    if( 與 server 的連線有字元可讀)
    {
       從該連線讀入, 並輸出到標準輸出上去.
    }
 
    if ( 標準輸入可讀)
    {
      從標準輸入讀入, 並輸出到與 server 的連線中去.
    }
}


分析上面的程式可以知道, 不管是 server 還是 client, 它們都不停的輪流查詢各個檔案描述符, 一旦可讀就讀入並進行處理. 這樣的程式, 不停的在執行, 只要有CPU 資源, 就不會放過. 因此對系統資源的消耗非常大. server 或者 client 單獨執行時, CPU 資源的 98% 左右都被其佔用. 因此, 我們可以使用另外一種阻塞的方法來解決這個問題, 這就是 select.



2. select 方法


select 方法中, 所有檔案描述符都是阻塞的. 使用 select 判斷一 組檔案描述符中是否有一個可讀(寫), 如果沒有就阻塞, 直到有一個的時候就被喚醒. 我們先看比較簡單的 client 的實現:

由於 client 只需要處理兩個檔案描述符, 因此, 需要判斷是否有可讀寫的檔案描述符只需要加入兩項:

FD_ZERO( sockset);          // 將 sockset 清空
FD_SET( sockfd, sockset);   // 把 sockfd 加入到 sockset 集合中
FD_SET( 0, sockset);        // 把 0 (標準輸入) 加入到 sockset 集合中

然後 client 的處理如下:
while ( 不想退出)
{
     select( sockfd+1, &sockset, NULL, NULL, NULL);
     // 此時該函式將阻塞直到標準輸入或者 sockfd 中有一個可讀為止
     // 第一個引數是 0 和 sockfd 中的最大值加一
     // 第二個引數是 讀集, 也就是 sockset
     // 第三, 四個引數是寫集和異常集, 在本程式中都為空
     // 第五個引數是超時時間, 即在指定時間內仍沒有可讀, 則出錯
     // 並返回. 當這個引數為NULL 時, 超時時間被設定為無限長.
     // 當 select 因為可讀返回時, sockset 中包含的只是可讀的
     // 那些檔案描述符.
     if( FD_ISSET( sockfd, &sockset))
     {
        // FD_ISSET 這個巨集判斷 sockfd 是否屬於可讀的檔案描述符
        從 sockfd 中讀入, 輸出到標準輸出上去.
     }

     if ( FD_ISSET( 0, &sockset))
     {
        // FD_ISSET 這個巨集判斷 sockfd 是否屬於可讀的檔案描述符
        從標準輸入讀入, 輸出到 sockfd 中去.
     }
     重新設定 sockset. (即將 sockset 清空, 並將 sockfd 和 0 加入)
}



下面看 server 的情況:

設定 sockset 如下:
FD_ZERO( sockset);
FD_SET( sockfd, sockset);
for ( 所有有效連線)
{
    FD_SET( userfd[i], sockset);
}
maxfd = 最大的檔案描述符號 + 1;
server 處理如下:
while ( 1)
{
    select( maxfd, &sockset, NULL, NULL, NULL);
    if ( FD_ISSET( sockfd, &sockset))
    {
         // 有新連線
         建立新連線, 並將該連線描述符加入到 sockset 中去了.
    }

    for( 所有有效連線)
    {
       if( FD_ISSET ( userfd[i], &sockset))
       {
          // 該連線中有字元可讀
          從該連線中讀入字元, 併發送到其他有效連線中去.
       }
    }
    重新設定 sockset;
}

由於採用 select 機制, 因此當沒有字元可讀時, 程式處於阻塞狀態, 最小程度的佔用CPU 資源, 在同一臺機器上執行一個 server 和若干個client 時, 系統負載只有 0.1 左右, 而採用原來的 fcntl 方法, 只執行一個 server, 系統負載就可以達到 1.5 左右. 因此我們推薦使用 select.