socket 科普文章

Linux:C/Socket多路複用select 小全

Submitted byELFeron 2006, August 3, 5:46 PM.Unix/Linux

作一個tcp的服務程序,同時可能有大量的客戶端連上來,希望分別記住各個socket
誰有數據過來,就讀出來,放到一個請求隊列--這些事情用一個線程完成

另外有一個結果隊列,如果結果裏包含了socket的編號,用一個線程專門:
取出來按編號找回原來發送者socket,發回給原來的客戶端

還有一個就是處理線程(池),它取出請求隊列裏的一個請求,進行處理,
把處理結果放入結果隊列

不知道有沒有現成的框架?
網上只找到一些很。。。的:
http://fanqiang.chinaunix.net/a4/b7/20010508/112359.html
Linux網絡編程--9. 服務器模型
http://linuxc.51.net 作者:hoyt (2001-05-08 11:23:59)

    學習過《軟件工程》吧.軟件工程可是每一個程序員"必修"的課程啊.如果你沒有學習過, 建議你去看 一看. 在這一章裏面,我們一起來從軟件工程的角度學習網絡編程的思想.在我們寫程序之前, 我們都應該從軟件工程的角度規劃好我們的軟件,這樣我們開發 軟件的效率纔會高. 在網絡程序裏面,一般的來說都是許多客戶機對應一個服務器.爲了處理客戶機的請求, 對服務端的程序就提出了特殊的要求.我們學習一 下目前最常用的服務器模型. 

循環服務器:循環服務器在同一個時刻只可以響應一個客戶端的請求 

併發服務器:併發服務器在同一個時刻可以響應多個客戶端的請求 


9.1 循環服務器:UDP服務器 
UDP循環服務器的實現非常簡單:UDP服務器每次從套接字上讀取一個客戶端的請求,處理, 然後將結果返回給客戶機. 

可以用下面的算法來實現. 

   socket(...);
   bind(...);
   while(1)
    {
         recvfrom(...);
         process(...);
         sendto(...);
   }

因爲UDP是非面向連接的,沒有一個客戶端可以老是佔住服務端. 只要處理過程不是死循環, 服務器對於每一個客戶機的請求總是能夠滿足. 
9.2 循環服務器:TCP服務器 
TCP循環服務器的實現也不難:TCP服務器接受一個客戶端的連接,然後處理,完成了這個客戶的所有請求後,斷開連接. 

算法如下: 

        socket(...);
        bind(...);
        listen(...);
        while(1)
        {
                accept(...);
                while(1)
                {
                        read(...);
                        process(...);
                        write(...);
                }
                close(...);
        }

TCP循環服務器一次只能處理一個客戶端的請求.只有在這個客戶的所有請求都滿足後, 服務器纔可以繼續後面的請求.這樣如果有一個客戶端佔住服務器不放時,其它的客戶機都不能工作了.因此,TCP服務器一般很少用循環服務器模型的. 

9.3 併發服務器:TCP服務器 
爲了彌補循環TCP服務器的缺陷,人們又想出了併發服務器的模型. 併發服務器的思想是每一個客戶機的請求並不由服務器直接處理,而是服務器創建一個 子進程來處理. 

算法如下: 

  socket(...);
  bind(...);
  listen(...);
  while(1)
  {
        accept(...);
        if(fork(..)==0)
          {
              while(1)
               {        
                read(...);
                process(...);
                write(...);
               }
           close(...);
           exit(...);
          }
        close(...);
  }     

TCP併發服務器可以解決TCP循環服務器客戶機獨佔服務器的情況. 不過也同時帶來了一個不小的問題.爲了響應客戶機的請求,服務器要創建子進程來處理. 而創建子進程是一種非常消耗資源的操作. 

9.4 併發服務器:多路複用I/O 
爲了解決創建子進程帶來的系統資源消耗,人們又想出了多路複用I/O模型. 

首先介紹一個函數select 

 int select(int nfds,fd_set *readfds,fd_set *writefds,
                fd_set *except fds,struct timeval *timeout)
 void FD_SET(int fd,fd_set *fdset)
 void FD_CLR(int fd,fd_set *fdset)
 void FD_ZERO(fd_set *fdset)
 int FD_ISSET(int fd,fd_set *fdset)

一般的來說當我們在向文件讀寫時,進程有可能在讀寫出阻塞,直到一定的條件滿足. 比如我們從一個套接字讀數據時,可能緩衝區裏面沒有數據可讀 (通信的對方還沒有 發送數據過來),這個時候我們的讀調用就會等待(阻塞)直到有數據可讀.如果我們不 希望阻塞,我們的一個選擇是用select系統 調用. 只要我們設置好select的各個參數,那麼當文件可以讀寫的時候select回"通知"我們 說可以讀寫了. readfds所有要讀的文件文 件描述符的集合 
writefds所有要的寫文件文件描述符的集合 

exceptfds其他的服要向我們通知的文件描述符 

timeout超時設置. 

nfds所有我們監控的文件描述符中最大的那一個加1 

在我們調用select時進程會一直阻塞直到以下的一種情況發生. 1)有文件可以讀.2)有文件可以寫.3)超時所設置的時間到. 

爲了設置文件描述符我們要使用幾個宏. FD_SET將fd加入到fdset 

FD_CLR將fd從fdset裏面清除 

FD_ZERO從fdset中清除所有的文件描述符 

FD_ISSET判斷fd是否在fdset集合中 

使用select的一個例子 

int use_select(int *readfd,int n)
{
   fd_set my_readfd;
   int maxfd;
   int i;
   
   maxfd=readfd[0];
   for(i=1;i
    if(readfd[i]>maxfd) maxfd=readfd[i];
   while(1)
   {
        /*   將所有的文件描述符加入   */
        FD_ZERO(&my_readfd);
        for(i=0;i
            FD_SET(readfd[i],*my_readfd);
        /*     進程阻塞                 */
        select(maxfd+1,& my_readfd,NULL,NULL,NULL); 
        /*        有東西可以讀了       */
        for(i=0;i
          if(FD_ISSET(readfd[i],&my_readfd))
              {
                  /* 原來是我可以讀了  */ 
                        we_read(readfd[i]);
              }
   }
}

使用select後我們的服務器程序就變成了. 


        初始話(socket,bind,listen);
        
    while(1)
        {
        設置監聽讀寫文件描述符(FD_*);   
        
        調用select;
        
        如果是傾聽套接字就緒,說明一個新的連接請求建立
             { 
                建立連接(accept);
                加入到監聽文件描述符中去;
             }
       否則說明是一個已經連接過的描述符
                {
                    進行操作(read或者write);
                 }
                        
        }               

多路複用I/O可以解決資源限制的問題.着模型實際上是將UDP循環模型用在了TCP上面. 這也就帶來了一些問題.如由於服務器依次處理客戶的請求,所以可能會導致有的客戶 會等待很久. 

9.5 併發服務器:UDP服務器 
人們把併發的概念用於UDP就得到了併發UDP服務器模型. 併發UDP服務器模型其實是簡單的.和併發的TCP服務器模型一樣是創建一個子進程來處理的 算法和併發的TCP模型一樣. 

除非服務器在處理客戶端的請求所用的時間比較長以外,人們實際上很少用這種模型. 


9.6 一個併發TCP服務器實例 

#include 
#include 
#include 
#include 
#include 
#define MY_PORT         8888

int main(int argc ,char **argv)
{
 int listen_fd,accept_fd;
 struct sockaddr_in     client_addr;
 int n;
 
 if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
  {
        printf("Socket Error:%s\n\a",strerror(errno));
        exit(1);
  }
 
 bzero(&client_addr,sizeof(struct sockaddr_in));
 client_addr.sin_family=AF_INET;
 client_addr.sin_port=htons(MY_PORT);
 client_addr.sin_addr.s_addr=htonl(INADDR_ANY);
 n=1;
 /* 如果服務器終止後,服務器可以第二次快速啓動而不用等待一段時間  */
 setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
 if(bind(listen_fd,(struct sockaddr *)&client_addr,sizeof(client_addr))<0)
  {
        printf("Bind Error:%s\n\a",strerror(errno));
        exit(1);
  }
  listen(listen_fd,5);
  while(1)
  {
   accept_fd=accept(listen_fd,NULL,NULL);
   if((accept_fd<0)&&(errno==EINTR))
          continue;
   else if(accept_fd<0)
    {
        printf("Accept Error:%s\n\a",strerror(errno));
        continue;
    }
  if((n=fork())==0)
   {
        /* 子進程處理客戶端的連接 */
        char buffer[1024];

        close(listen_fd);
        n=read(accept_fd,buffer,1024);
        write(accept_fd,buffer,n);
        close(accept_fd);
        exit(0);
   }
   else if(n<0)
        printf("Fork Error:%s\n\a",strerror(errno));
   close(accept_fd);
  }

  

Linux網絡服務器 

     Linux系統網絡服務器模型主要有兩種:併發服務器和循環服務器。所謂併發服務器就是在同一個時刻可以處理來自多個客戶端的請求;循環服務器是指服務器在同一時刻指可以響應一個客戶端的請求。而且對於TCP和UDP套接字,這兩種服務器的實現方式也有不同的特點。

1、TCP循環服務器:首先TCP服務器接受一個客戶端的連接請求,處理連接請求,在完成這個客戶端的所有請求後斷開連接,然後再接受下一個客戶端的請求。

   創建TCP循環服務器的算法如下:

    socket(……);   //創建一個TCP套接字

    bind(……);      //邦定公認的端口號

    listen(……);  //傾聽客戶端連接

    while(1)          //開始循環接收客戶端連接

   {

                accept(……);//接收當前客戶端的連接

                while(1)

                 {                    //處理當前客戶端的請求

                         read(……);

                         process(……);

                          write(……);

                 }

                close(……);   //關閉當前客戶端的連接,準備接收下一個客戶端連接

    }

TCP循環服務器一次只處理一個客戶端的請求,如果有一個客戶端佔用服務器不放時,其它的客戶機連接請求都得不到及時的響應。因此,TCP服務器一般很少用循環服務器模型的。

2、TCP併發服務器:併發服務器的思想是每一個客戶端的請求並不由服務器的主進程直接處理,而是服務器主進程創建一個子進程來處理。

    創建TCP併發服務器的算法如下:

    socket(……); //創建一個TCP套接字

    bind(……);    //邦定公認的端口號

    listen(……);//傾聽客戶端連接

    while(1)       //開始循環接收客戶端的接收

   {

                accept(……);//接收一個客戶端的連接

                if(fork(……)==0)  //創建子進程

                {                   

                         while(1)

                             {             //子進程處理某個客戶端的連接

                                   read(……);

                                   process(……);

                                   write(……);

                              }

                             close(……);  //關閉子進程處理的客戶端連接

                              exit(……) ;//終止該子進程

                    }

                 close(……);           //父進程關閉連接套接字描述符,準備接收下一個客戶端連接

       }

TCP併發服務器可以解決TCP循環服務器客戶端獨佔服務器的情況。但同時也帶來了一個不小的問題,即響應客戶機的請求,服務器要創建子進程來處理,而創建子進程是一種非常消耗資源的操作。

 3、UDP循環服務器:UDP服務器每次從套接字上讀取一個客戶端的數據報請求,處理接收到的UDP數據報,然後將結果返回給客戶機。

    創建UDP循環服務器的算法如下:

    socket(……);  //創建一個數據報類型的套接字

    bind(……);     //邦定公認的短口號

     while(1)        //開始接收客戶端的連接

    {                   //接收和處理客戶端的UDP數據報

                   recvfrom(……);

                   process(……);

                   sendto(……);

                  //準備接收下一個客戶機的數據報

        }

因爲UDP是非面向連接的,沒有一個客戶端可以獨佔服務器。只要處理過程不是死循環,服務器對於每一個客戶機的請求總是能夠處理的。

UDP循環服務器在數據報流量過大時由於處理任務繁重可能造成客戶技數據報丟失,但是因爲UDP協議本身不保證數據報可靠到達,所以UDP協議是尤許丟失數據報的。

 鑑於以上兩點,一般的UDP服務器採用循環方式

4、UDP併發服務器

把併發的概念應用UDP就得到了併發UDP服務器,和併發TCP服務器模型一樣是創建子進程來處理的。

 創建UDP併發服務器的算法如下:

     socket(……);  //創建一個數據報類型的套接字

     bind(……);     //邦定公認的短口號

     while(1)        //開始接收客戶端的連接

    {                   //接收和處理客戶端的UDP數據報

                   recvfrom(……);

                  if(fork(……)==0)  //創建子進程

                  {

                         process(……);

                         sendto(……);

                   }

         }

除非服務器在處理客戶端的請求所用的時間比較長以外,人們實際上很少用這種UDP併發服務器模型的。

5、多路複用I/O併發服務器:創建子進程會帶來系統資源的大量消耗,爲了解決這個問題,採用多路複用I/O模型的併發服務器。採用select函數創建多路複用I/O模型的併發服務器的算法如下:

  初始化(socket,bind,listen);

  while(1)

  {

     設置監聽讀寫文件描述符(FD_*);

     調用select;

     如果是傾聽套接字就緒,說明一個新的連接請求建立

     {

           建立連接(accept);

           加入到監聽文件描述符中去;

       }

     否則說明是一個已經連接過的描述符

       {

                 進行操作(read或者write);

       }

  }

多路複用I/O可以解決資源限制問題,此模型實際上是將UDP循環模型用在了TCP上面。這也會帶了一些問題,如由於服務器依次處理客戶的請求,所以可能導致友的客戶會等待很久。
 
http://bbs.chinaitlab.com/dispbbs.asp?boardid=148&id=114926


阻塞 
  阻塞,你也許早就聽說了。"阻塞"是"sleep" 的科技行話。你可能注意 到前面運行的 listener 程序,它在那裏不停地運行,等待數據包的到來。 實際在運行的是它調用 recvfrom(),然後沒有數據,因此 recvfrom() 說" 阻塞 (block)",直到數據的到來。
很多函數都利用阻塞。accept() 阻塞,所有的 recv*() 函數阻塞。它 們之所以能這樣做是因爲它們被允許這樣做。當你第一次調用 socket() 建 立套接字描述符的時候,內核就將它設置爲阻塞。如果你不想套接字阻塞, 你就要調用函數 fcntl(): 
#include
  #include
   . 
   . 
   sockfd = socket(AF_INET, SOCK_STREAM, 0); 
   fcntl(sockfd, F_SETFL, O_NONBLOCK); 
   . 
   . 
  通過設置套接字爲非阻塞,你能夠有效地"詢問"套接字以獲得信息。如 果你嘗試着從一個非阻塞的套接字讀信息並且沒有任何數據,它不允許阻 塞--它將返回 -1 並將 errno 設置爲 EWOULDBLOCK。 
但是一般說來,這種詢問不是個好主意。如果你讓你的程序在忙等狀 態查詢套接字的數據,你將浪費大量的 CPU 時間。更好的解決之道是用 下一章講的 select() 去查詢是否有數據要讀進來。
--------------------------------------------------------------------------------
select()--多路同步 I/O
  雖然這個函數有點奇怪,但是它很有用。假設這樣的情況:你是個服 務器,你一邊在不停地從連接上讀數據,一邊在偵聽連接上的信息。 沒問題,你可能會說,不就是一個 accept() 和兩個 recv() 嗎? 這麼 容易嗎,朋友? 如果你在調用 accept() 的時候阻塞呢? 你怎麼能夠同時接 受 recv() 數據? “用非阻塞的套接字啊!” 不行!你不想耗盡所有的 CPU 吧? 那麼,該如何是好?
select() 讓你可以同時監視多個套接字。如果你想知道的話,那麼它就 會告訴你哪個套接字準備讀,哪個又準備寫,哪個套接字又發生了例外 (exception)。
閒話少說,下面是 select():
#include
  #include
  #include
int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set 
*exceptfds, struct timeval *timeout);
這個函數監視一系列文件描述符,特別是 readfds、writefds 和 exceptfds。如果你想知道你是否能夠從標準輸入和套接字描述符 sockfd 讀入數據,你只要將文件描述符 0 和 sockfd 加入到集合 readfds 中。參 數 numfds 應該等於最高的文件描述符的值加1。在這個例子中,你應該 設置該值爲 sockfd+1。因爲它一定大於標準輸入的文件描述符 (0)。 當函數 select() 返回的時候,readfds 的值修改爲反映你選擇的哪個 文件描述符可以讀。你可以用下面講到的宏 FD_ISSET() 來測試。 在我們繼續下去之前,讓我來講講如何對這些集合進行操作。每個集 合類型都是 fd_set。下面有一些宏來對這個類型進行操作: 
FD_ZERO(fd_set *set) – 清除一個文件描述符集合
  FD_SET(int fd, fd_set *set) - 添加fd到集合 
  FD_CLR(int fd, fd_set *set) – 從集合中移去fd 
  FD_ISSET(int fd, fd_set *set) – 測試fd是否在集合中 
最後,是有點古怪的數據結構 struct timeval。有時你可不想永遠等待 別人發送數據過來。也許什麼事情都沒有發生的時候你也想每隔96秒在終 端上打印字符串"Still Going..."。這個數據結構允許你設定一個時間,如果 時間到了,而 select() 還沒有找到一個準備好的文件描述符,它將返回讓 你繼續處理。 
數據結構 struct timeval 是這樣的: 
struct timeval { 
   int tv_sec; /* seconds */ 
   int tv_usec; /* microseconds */ 
   }; 
只要將 tv_sec 設置爲你要等待的秒數,將 tv_usec 設置爲你要等待 的微秒數就可以了。是的,是微秒而不是毫秒。1,000微秒等於1毫秒,1,000 毫秒等於1秒。也就是說,1秒等於1,000,000微秒。爲什麼用符號"usec" 呢? 字母"u" 很象希臘字母 Mu,而 Mu 表示"微" 的意思。當然,函數 返回的時候 timeout 可能是剩餘的時間,之所以是可能,是因爲它依賴於 你的 Unix 操作系統。 
哈!我們現在有一個微秒級的定時器!別計算了,標準的 Unix 系統 的時間片是100毫秒,所以無論你如何設置你的數據結構 struct timeval, 你都要等待那麼長的時間。 
還有一些有趣的事情:如果你設置數據結構 struct timeval 中的數據爲 0,select() 將立即超時,這樣就可以有效地輪詢集合中的所有的文件描述 符。如果你將參數 timeout 賦值爲 NULL,那麼將永遠不會發生超時,即 一直等到第一個文件描述符就緒。最後,如果你不是很關心等待多長時間, 那麼就把它賦爲 NULL 吧。 
下面的代碼演示了在標準輸入上等待 2.5 秒: 
#include
  #include
  #include
#define STDIN 0 /* file descriptor for standard input */ 
main() 
   { 
  struct timeval tv; 
  fd_set readfds; 
tv.tv_sec = 2; 
  tv.tv_usec = 500000; 
FD_ZERO(&readfds); 
  FD_SET(STDIN,&readfds); 
/* don&apost care about writefds and exceptfds: */ 
  select(STDIN+1,&readfds, NULL, NULL,&tv); 
if (FD_ISSET(STDIN,&readfds)) 
  printf("A key was pressed!\n"); 
  else 
  printf("Timed out.\n"); 
  } 
如果你是在一個 line buffered 終端上,那麼你敲的鍵應該是回車 (RETURN),否則無論如何它都會超時。
現在,你可能回認爲這就是在數據報套接字上等待數據的方式--你是對 的:它可能是。有些 Unix 系統可以按這種方式,而另外一些則不能。你 在嘗試以前可能要先看看本系統的 man page 了。
最後一件關於 select() 的事情:如果你有一個正在偵聽 (listen()) 的套 接字,你可以通過將該套接字的文件描述符加入到 readfds 集合中來看是 否有新的連接。
這就是我關於函數select() 要講的所有的東西。


http://www.pcdog.com/p/html/2004123/31220042887_1.htm
深入UNIX編程:一個簡單聊天室的兩種實現 (fcntl 和 select)

--------------------------------------------------------------------------------
 
 
http://www.pcdog.com 2004-12-3 互聯網
 
         
      在互聯網相當普及的今天,在互聯網上聊天對很多“網蟲”來說已經是家常便飯了。聊天室程序可以說是網上最簡單的多點通信程序。聊天室的實現方法有很多,但都是利用所謂的“多用戶空間”來對信息進行交換,具有典型的多路I/O的
架構。一個簡單的聊天室, 從程序員的觀點來看就是在多個I/O端點之間實現多對多的通信。其架構如圖一所示。這樣的實現在用戶的眼裏就是聊天室內任何一個人輸入一段字符之後,其他用戶都可以得到這一句話。這種“多用戶空間”的架構在其他多點通信程序中應用的非常廣泛,其核心就是多路I/O通信。多路I/O通信又被稱爲I/O多路複用(I/O Multiplexing)一般被使用在以下的場合:
 
      客戶程序需要同時處理交互式的輸入和同服務器之間的網絡連接時需要處理I/O多路複用問題;
      客戶端需要同時對多個網絡連接作出反應(這種情況很少見);
      TCP服務器需要同時處理處於監聽狀態和多個連接狀態的socket;
      服務器需要處理多個網絡協議的socket;
      服務器需要同時處理不同的網絡服務和協議。

      聊天室所需要面對的情況正是第一和第三兩種情況。我們將通過在TCP/IP協議之上建立一個功能簡單的聊天室讓大家更加了解多路I/O以及它的實現方法。 我們要討論的聊天室功能非常簡單, 感興趣的朋友可以將其功能擴展, 發展成一個功能比較完整的聊天室, 如加上用戶認證, 用戶暱稱, 祕密信息, semote 等功能. 首先它是一個 client/server 結構的程序, 首先啓動 server, 然後用戶使用 client 進行連接. client/server 結構的優點是速度快, 缺點是當 server 進行更新時, client 也必需更新.
 
網絡初始化
 
      首先是初始化 server, 使server 進入監聽狀態: (爲了簡潔起見,以下引用的程序與實際程序略有出入, 下同)
sockfd = socket( AF_INET,SOCK_STREAM, 0);
// 首先建立一個 socket, 族爲 AF_INET, 類型爲 SOCK_STREAM.
// AF_INET = ARPA Internet protocols 即使用 TCP/IP 協議族
// SOCK_STREAM 類型提供了順序的, 可靠的, 基於字節流的全雙工連接.
// 由於該協議族中只有一個協議, 因此第三個參數爲 0
 
bind( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));
// 再將這個 socket 與某個地址進行綁定.
// serv_addr 包括 sin_family = AF_INET 協議族同 socket
// sin_addr.s_addr = htonl( INADDR_ANY) server 所接受的所有其他
// 地址請求建立的連接.
// sin_port = htons( SERV_TCP_PORT) server 所監聽的端口
// 在本程序中, server 的 IP和監聽的端口都存放在 config 文件中.

listen( sockfd, MAX_CLIENT);
// 地址綁定之後, server 進入監聽狀態.
// MAX_CLIENT 是可以同時建立連接的 client 總數.
server 進入 listen 狀態後, 等待 client 建立連接。

Client端要建立連接首先也需要初始化連接:
sockfd = socket( AF_INET,SOCK_STREAM,0));
// 同樣的, client 也先建立一個 socket, 其參數與 server 相同.

connect( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));
// client 使用 connect 建立一個連接.
// serv_addr 中的變量分別設置爲:
// sin_family = AF_INET 協議族同 socket
// sin_addr.s_addr = inet_addr( SERV_HOST_ADDR) 地址爲 server
// 所在的計算機的地址.
// sin_port = htons( SERV_TCP_PORT) 端口爲 server 監聽的端口.

當 client 建立新連接的請求被送到Server端時, server 使用 accept 來接受該連接:
accept( sockfd, (struct sockaddr*)&cli_addr,&cli_len);
// 在函數返回時, cli_addr 中保留的是該連接對方的信息
// 包括對方的 IP 地址和對方使用的端口.
// accept 返回一個新的文件描述符.

      在 server 進入 listen 狀態之後, 由於已有多個用戶在線,所以程序需要同時對這些用戶進行操作,並在它們之間實現信息交換。這在實現上稱爲I/O多路複用技術。多路複用一般有以下幾種方法:

      非阻塞通信方法:將文件管道通過fcntl()設爲非阻塞通信方式,每隔一端時間對他們實行一次輪詢,以判斷是否可以進行讀寫操作。這種方式的缺點是費用太高,大部分資源浪費在輪詢上。

      子進程方法:應用多個子進程,每一個對一個單工阻塞方式通信。所有子進程通過IPC和父進程進行通信。父進程掌管所有信息。這種方式的缺點是實現複雜,而且由於IPC在各個操作系統平臺上並不完全一致,會導致可移植性降低。

      信號驅動(SIGIO)的異步I/O方法:首先,異步I/O是基於信號機制的,並不可靠。其次單一的信號不足以提供更多的信息來源。還是需要輔助以其他的手段,實現上有很高的難度。

      select ()方法:在BSD中提供了一種可以對多路I/O進行阻塞式查詢的方法——select()。它提供同時對多個I/O描述符進行阻塞式查詢的方法,利用它,我們可以很方便的實現多路複用。根據統一UNIX規範的協議,POSIX也採用了這種方法,因此,我們可以在大多數操作系統中使用select方法。

      使用專門的I/O多路複用器:在“UNIX? SYSTEM V Programmer&aposs Guide: STREAMS”一書中詳細的說明了構造和使用多路複用器的方法。這裏就不再詳述了。
 
我們下面分別討論多路I/O的兩種實現方法:

1. 非阻塞通信方法

      對一個文件描述符指定的文件或設備, 有兩種工作方式: 阻塞與非阻塞。所謂阻塞方式的意思是指, 當試圖對該文件描述符進行讀寫時, 如果當時沒有東西可讀,或者暫時不可寫, 程序就進入等待狀態, 直到有東西可讀或者可寫爲止。而對於非阻塞狀態, 如果沒有東西可讀, 或者不可寫, 讀寫函數馬上返回, 而不會等待。缺省情況下, 文件描述符處於阻塞狀態。在實現聊天室時, server 需要輪流查詢與各client 建立的 socket, 一旦可讀就將該 socket 中的字符讀出來並向所有其他client 發送。並且, server 還要隨時查看是否有新的 client 試圖建立連接,這樣, 如果 server 在任何一個地方阻塞了, 其他 client 發送的內容就會受到影響,得不到服務器的及時響應。新 client 試圖建立連接也會受到影響。所以我們在這裏不能使用缺省的阻塞的文件工作方式,而需要將文件的工作方式變成非阻塞方式。在UNIX下,函數fcntl()可以用來改變文件I/O操作的工作方式,函數描述如下:

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

爲了節省篇幅我們使用自然語言描述聊天室 server :
while ( 1)
{
      if 有新連接 then 建立並記錄該新連接;
      for ( 所有的有效連接)
            begin
                  if 該連接中有字符可讀 then
                        begin
                              讀入字符串;
                        for ( 所有其他的有效連接)
                              begin
                                    將該字符串發送給該連接;
                              end;
                        end;
            end;
      end.

      由於判斷是否有新連接, 是否可讀都是非阻塞的, 因此每次判斷,不管有還是沒有, 都會馬上返回. 這樣,任何一個 client 向 server 發送字符或者試圖建立新連接, 都不會對其他 client 的活動造成影響。
對 client 而言, 建立連接之後, 只需要處理兩個文件描述符, 一個是建立了連接的 socket 描述符, 另一個是標準輸入. 和 server 一樣, 如果使用阻塞方式的話, 很容易因爲其中一個暫時沒有輸入而影響另外一個的讀入.. 因此將它們都變成非阻塞的, 然後client 進行如下動作:

while ( 不想退出)
      begin
      if ( 與 server 的連接有字符可讀)
            begin
            從該連接讀入, 並輸出到標準輸出上去.
            End;
      if ( 標準輸入可讀)
            Begin
            從標準輸入讀入, 並輸出到與 server 的連接中去.
            End;
      End.

上面的讀寫分別調用這樣兩個函數:
read( userfd[i], line, MAX_LINE);
// userfd[i] 是指第 i 個 client 連接的文件描述符.
// line 是指讀出的字符存放的位置.
// MAX_LINE 是一次最多讀出的字符數.
// 返回值是實際讀出的字符數.

write( userfd[j], line, strlen( line));
// userfd[j] 是第 j 個 client 的文件描述符.
// line 是要發送的字符串.
// strlen( line) 是要發送的字符串長度.

分析上面的程序可以知道, 不管是 server 還是 client, 它們都不停的輪流查詢各個文件描述符, 一旦可讀就讀入並進行處理. 這樣的程序, 不停的在執行, 只要有CPU 資源, 就不會放過。因此對系統資源的消耗非常大。server 或者 client 單獨執行時, CPU 資源的 98% 左右都被其佔用。極大的消耗了系統資源。

select 方法

      因此,雖然我們不希望在某一個用戶沒有反應時阻塞其他的用戶,但我們卻應該在沒有任何用戶有反應的情況之下停止程序的運行,讓出搶佔的系統資源,進入阻塞狀態。有沒有這種方法呢?現在的UNIX系統中都提供了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 左右, 而採用原來的非阻塞通信方法, 只運行一個 server, 系統負載就可以達到 1.5 左右. 因此我們推薦使用 select.

參考文獻:
[1] UNIX Network Programming Volume 1 W.Richard Stevens 1998 Prentice Hall
[2] 計算機實用網絡編程 湯毅堅 1993 人民郵電出版社
[3] UNIX? SYSTEM V RELEASE 4 Programmer&aposs Guide:STREAMS AT&T 1990 Prentice Hall
[4] UNIX? SYSTEM V RELEASE 4 Network Programmer&aposs Guide AT&T 1990 Prentice Hall
所有源程序均登載在eDOC網站上,如有需要可以去http://edoc.163.net下載
By Simon Lei, Jul.01,1999.
作者介紹:
姓名:雷雲飛
筆名:eDOC工作組 聯繫地址: 安徽省合肥市四號信箱2331 230027
http://dev.csdn.net/develop/article/27/27235.shtm 




WinSock I/O系列1:多路複用I/O支持多Client的實現及效率討論 

關鍵字   多路複用I/O select效率 多客戶端
出處   
 
 1.       引言

多路複用I/O模型(select)是UNIX/LINUX用得的最多的一種I/O模型,在Windows下也

可做爲一種同步I/O使用。本文給出該I/O模型處理多Client的簡單(在主線程中)實現。

2.       關於select

select I/O模型是一種異步I/O模型,在單線程中Linux/WinNT默認支持64個客戶端套

接字。這種I/O模型主要涉及以下幾個函數及宏:

int select(…)、FD_ZERO、FD_SET、FD_ISSET以及FD_SETSIZE。

3.       用select開發一個Server

3.1 只支持單個Client

    // 相關初始化處理, 創建監聽套接字

    listen(listensock,  5);

    clientsock  =  accept(listensock,  NULL,  NULL);

    for  (; ;)

    {

             FD_ZERO(&readfds); 

             FD_SET(clientsock,&readfds);

             nResult = select(

                     0,         // Windows中這個參數忽略,Linux中在此處爲1

                     readfds,    // 可讀套接字集合

                     ……

              )

             if   (nResult  = =  SOCKET_ERROR)

                    return –1;

             // 判斷cliensock是否處於待讀狀態

             if  (FD_ISSET(clientsock, &readfds))

            {
                              // 相關處理

            }

    }

其實Winsock中的WSAEventSelect模型是與之類似的。

3.2  在單線程中支持63個Client

   SOCKET clientsockarray[FD_SETSIZE – 1];   // FD_SETSIZE is 64

  // 相關初始化處理, 創建監聽套接字

   

  listen(listensock, 5);

  // 初始化套接字數組

 InitSock(clientsockarray);

  FD_ZERO(&readfds);

  FD_SET(listensock, &readfds);

  for  (; ;)

 {

 nRet  = select(0, &readfds,  NULL,  NULL,  NULL);

// 判斷監聽套接字是否可讀

 if  (FD_ISSET(listensock, &readfds))

 {

         clientsock = accept(listensock,  NULL,  NULL);

          // 將客戶套接字放到套接字數組中

          if   (!InsertSock(clientsockarray, clientsock))

          {

                   printf("客戶端超過了63個,此次連接被拒絕.\n");

                   closesocket(clientsock);

                   continue;

           }   

  }

   

  // 逐個處理處於待決狀態的套接字

  for  (nIndex  =  0;  nIndex <  FD_SETSIZE  -  1;  nIndex++)

 {

           if   (FD_ISSET(clientsockarray[nIndex], &readfds))

           {

                     nRet  =  recv(clientsockarray[nIndex],  buff,  sizeof(buff),  0);

                     if  (nRet  = =  0  ||  nRet  = =  SOCKET_ERROR)

                     {

                                closesocket(clientsockarray[nIndex]);

                            clientsockarray[nIndex] = INVALID_SOCKET;

                            continue;       // 繼續處理套接字句柄數組中的其它套接字

                     }

                     // 將接受到的數據進行處理,此處只將其輸出

                     printf("%s\n", buff);

              }

       }

 

       // 初始化套接字集合

       FD_ZERO(&readfds);

       FD_SET(listensock,&readfds);

       // 將所有有效的套接字句柄加入到套接字句柄數組中

       for (nIndex = 0; nIndex< FD_SETSIZE - 1; nIndex++)

       {

if (clientsockarray[nIndex] != INVALID_SOCKET)

              FD_SET(clientsockarray[nIndex],&readfds);

       }

}

 

BOOL InsertSock(SOCKET* pSock,  SOCKET sock)

{

          for   (int  nIndex  =  0;  nIndex <  FD_SETSIZE – 1;  nIndex++)

         {
                                     if   (pSock[nIndex]  = =  INVALID_SOCKET)

                  {

                          pSock[nIndex] = sock;

                          break;

                  }

          }

 

          if   (nIndex = = FD_SETSIZE – 1)

                 return FALSE;

     

          return TRUE;

 }

 

       上面只是給簡要的代碼,有的輔助函數也沒有給出。用select支持多Client是比較方便的,在一個線程中可支持63個;可以採用多線程支持更大數量的Client。

4.       效率的討論

4.1  對套接字數組掃描的效率問題

    在上面的程序中,存在多處對套接字句柄的掃描處理,這個肯定會影響效率。不知道各位朋友是怎麼處理這個問題的。

4.2 對客戶端實時響應問題

上面的程序處理待決的套接字的時候,是逐個處理的,如果響應某個Client的時間長到一定程度的話,肯定會影響對其它客戶端的響應。我的解決方法是當這個套接字處於可讀的待決狀態的話,產生一個子線程去處理------接收數據和處理數據。這樣主線程繼續自己的工作,其它Client可以得及時的響應;當然當有大量的Client請求時,對線程的控制會成爲一個新問題。

在UNIX/LINUX下做一個支持大量Client的Server的話,本人還是最先選擇select這種I/O模型,這是因爲我還不知道LINUX還有哪些更好的I/O模型。WinNT的話,還有CompletionPort和Overlapped,特別對於有大數據量傳送,同時只有少量的Client時,Overlapped可以發揮相當大的作用。各位朋友請給出使用select的好方法。

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