多路複用網絡編程

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
#defineMY_PORT        8888

int main(int argc ,char **argv)
{
 int listen_fd,accept_fd;
 structsockaddr_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(structsockaddr_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;
   elseif(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);
   }
   elseif(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是否在集合中
最 後,是有點古怪的數據結構 structtimeval。有時你可不想永遠等待別人發送數據過來。也許什麼事情都沒有發生的時候你也想每隔96秒在終端上打印字符串"StillGoing..."。這個數據結構允許你設定一個時間,如果 時間到了,而 select()還沒有找到一個準備好的文件描述符,它將返回讓你繼續處理。
數據結構 struct timeval 是這樣的:
struct timeval {
   int tv_sec;
   int tv_usec;
   };
只 要將 tv_sec 設置爲你要等待的秒數,將 tv_usec設置爲你要等待的微秒數就可以了。是的,是微秒而不是毫秒。1,000微秒等於1毫秒,1,000毫秒等於1秒。也就是說,1秒等於1,000,000微秒。爲什麼用符號"usec" 呢? 字母"u" 很象希臘字母 Mu,而 Mu表示"微" 的意思。當然,函數返回的時候 timeout 可能是剩餘的時間,之所以是可能,是因爲它依賴於你的 Unix操作系統。
哈!我們現在有一個微秒級的定時器!別計算了,標準的 Unix 系統的時間片是100毫秒,所以無論你如何設置你的數據結構 structtimeval,你都要等待那麼長的時間。
還 有一些有趣的事情:如果你設置數據結構 struct timeval 中的數據爲 0,select()將立即超時,這樣就可以有效地輪詢集合中的所有的文件描述符。如果你將參數 timeout 賦值爲NULL,那麼將永遠不會發生超時,即一直等到第一個文件描述符就緒。最後,如果你不是很關心等待多長時間,那麼就把它賦爲 NULL吧。
下面的代碼演示了在標準輸入上等待 2.5 秒:
#include
  #include
  #include
#define STDIN 0
main()
   {
  struct timeval tv;
  fd_set readfds;
tv.tv_sec = 2;
  tv.tv_usec = 500000;
FD_ZERO(&readfds);
  FD_SET(STDIN,&readfds);

  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() 要講的所有的東西。


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