Socket 程式設計IO Multiplexing

KingsLanding發表於2014-02-15

   Linux Socket 程式設計中I/O Multiplexing 主要通過三個函式來實現:select, poll,epoll來實現。I/O Multiplexing,先構造一張有關描述符的列表,然後呼叫一個函式,直到這些描述符中的一個已準備好進行I/O時,該函式才返回。在返回時,它告訴程式哪些描述符已準備好可以進行I/O。本文具體介紹一下select 和poll的用法,給出簡單的demo程式碼,簡要分析一下這兩個函式的使用易出錯的地方。        

#include<sys/select.h>

int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds, struct timeval* restrict tvptr);

//Returns: count of ready descriptors, 0 on timeout, -1 on error

  中間三個引數readfds、writefds和exceptfds是指向描述符集的指標。這三個描述符集說明了我們關心的可讀、可寫或出於異常條件的各個描述符,設定為NULL則表示不關心。每個描述符集存放在一個fd_set資料型別中。這種資料型別為每一可能的描述符保持一位。描述符集的函式介面(可能實現為巨集)包括:呼叫FD_ZERO將一個指定的fd_set變數的所有位設定為0;呼叫FD_SET設定一個fd_set變數的指定位;呼叫FD_CLR將一指定位清楚;呼叫FD_ISSET測試一指定位是否設定。

#include <sys/select.h>

int FD_ISSET(int fd, fd_set *fdset);

  //Returns: nonzero if fd is in set, 0 otherwise

void FD_CLR(int fd, fd_set *fdset);

void FD_SET(int fd, fd_set *fdset);

void FD_ZERO(fd_set *fdset);
  

 

  檔案描述符集fdset中的檔案描述符的個數是有限制的,最大值由FD_SETSIZE指定,一般為1024.

  Select 最後一個引數用於設定超時值,當select監聽達到超時值時還未有關心的事件發生則返回,函式返回值為0.

struct timeval{

  long tv_sec;//second

  long tv_usec;//microsecond

}

 

  超時引數如果設定為 NULL 則無限等待。

  下面來是一個簡單的select Echo server:

// simpleEcho.cpp
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <vector> #include <string.h> #include <stdlib.h> #include <fcntl.h> #define SEVER_PORT 1314 #define MAX_LINE_LEN 1024 using namespace std; int main() { struct sockaddr_in cli_addr, server_addr; socklen_t sock_len; vector<int> client(FD_SETSIZE,-1); fd_set rset,allset; int listenfd, connfd, sockfd, maxfd, nready, ix,maxid, nrcv,one; char addr_str[INET_ADDRSTRLEN],buf[MAX_LINE_LEN]; bzero(&server_addr,sizeof server_addr); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SEVER_PORT); listenfd = socket(AF_INET,SOCK_STREAM,0); one = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,&one, sizeof(one)); if(bind (listenfd ,(struct sockaddr *)&server_addr ,sizeof server_addr) < 0 ) { printf("socket bind error" ); return 0; } listen(listenfd ,10); FD_ZERO(&allset); FD_SET(listenfd ,&allset ); maxfd = listenfd ; maxid = -1 ; while(1 ) { rset = allset; //! nready = select (maxfd + 1, &rset,NULL,NULL,NULL); if(nready < 0 ) { printf("select error! \n" ); exit(1 ); } if(FD_ISSET (listenfd , &rset )) { sock_len = sizeof cli_addr; connfd = accept (listenfd ,(struct sockaddr *)&cli_addr , &sock_len); printf("recieve from : %s at port %d\n" , inet_ntop(AF_INET,&cli_addr .sin_addr ,addr_str ,INET_ADDRSTRLEN ),cli_addr .sin_port ); for(ix = 0 ; ix < static_cast< int>(client .size()); ix++) { if(client[ix] < 0 ) { client[ix] = connfd ; break; } } printf("client[%d] = %d\n" ,ix ,connfd ); if( FD_SETSIZE == ix) { printf("too many client! \n" ); exit(1 ); } if( connfd > maxfd) { maxfd = connfd; } FD_SET(connfd, &allset ); if(ix > maxid ) { maxid = ix; } if(--nready == 0) { continue; } } for(ix = 0 ; ix <= maxid; ix++) //<= { if((sockfd = client [ix ]) < 0) { continue; } if(FD_ISSET (sockfd ,&rset )) { if( 0 == (nrcv = read(sockfd,buf,MAX_LINE_LEN ))) { close(sockfd); client[ix] = -1 ; FD_CLR(sockfd ,&allset ); } else { printf("RECIEVE: %s \n" ,buf ); write(sockfd,buf,nrcv); } } if(--nready == 0) { break; } } } return 0; }

  在使用select 的時候要注意兩點:

    第一個引數需要是當前所關心的檔案描述符中最大的一個+1

    第二需要注意的是select的中間3個引數採用了“value-result”(UNP1的說法)的方式,設定了關心的檔案描述符進行select,select返回之後對應描述的fdset中只有有事件發生的對應fd會被設定,其它關心但是沒有事件發生的描述符將會從fdset中清除掉,如果不進行重新賦值,下次select就不會關注這些描述符了,因此上述程式碼中allset每次對rset進行復制。

   來看看如果只在while(1) 之前設定rset,在while(1) 中不在每次select之前賦值會發生什麼,在控制檯輸入: strace ./simpleEcho,另外開啟一個控制檯視窗輸入:nc localhost 1314,這作為一個連線Echo server 的 client,然後輸入你想發往Echo Server內容。關鍵我們來看一下Echo server的情況:

  可以看到 select 首先關注的檔案描述符 fd == 3,該描述符是listenfd,然後有client連過來,select關注了 fd 3 和 4,4是accept函式開啟的用於與client通訊的描述符,當client向server寫資料之後select關注的描述就只剩下 fd 4了,也就是當前處於連線狀態的描述符,如果client主動關閉,select返回之後,下次監聽就沒有關注的描述符了,可見select函式的“value-result” 返回方式是這樣工作的:每次只返回監聽描述符中處於active的,其它處於監聽的但是當前沒有事件發生的描述符則會從監聽的fdset中清除掉。因此在每次select之前需要給關注的fdset重新賦值。

  注1:在進行系統呼叫除錯的時候 strace 是一個利器,簡單使用方式如上面在執行程式之前加上 strace 即可。在除錯程式碼邏輯的時候當然還是使用gdb了。

  注2Netcat 或者叫 nc 是 Linux 下的一個用於除錯和檢查網路工具包。可用於建立 TCP/IP 連線,最大的用途就是用來處理 TCP/UDP 套接字。

  

  select 什麼時候會處於準備好並返回呢? UNPv1 上進行了詳細介紹:

  下面四個條件任何一個滿足的時候套件字準備好讀:

  1. 套介面接受緩衝區的資料位元組數大於等於套介面接受緩衝區的低潮限度當前值。對這樣的套介面讀操作將不阻塞並返回一個大於0的值(既準備好讀入的資料量)。我們可以用套介面選項SO_RCVLOWAT來設定此低潮限度,對於TCP和UDP套介面,其預設值為1。

  2. 連線的讀這一半關閉(也就是接收了FIN的TCP連線)。對這樣的套介面讀操作將不阻塞並返回0(記檔案結束符)。

  3. 套介面是一個監聽的套介面且已完成的連線數為非0。正常情況下這樣的套介面上的accpet不會被阻塞。

  4. 有一個套介面錯誤待處理。對這樣的套介面操作將不阻塞並返回一個錯誤-1,errno設定成明確的錯誤條件。

 

  以下三個條件的任何一個滿足時,套介面準備好寫操作:

  1. 套介面傳送緩衝區中可用空間的位元組數大於等於套介面傳送緩衝區低潮限度的當前值,且或者(i)套介面已連線,或者(ii)套介面不需要連線(例如UDP套接字)。這意味著,如果我們將這樣的套介面設定為非阻塞,寫操作將不阻塞且返回一個正值(例如由傳輸層傳入的位元組數)。我們可以用套介面選項SO_SNDLOWAT來設定此低潮限度,對於TCP和UDP套介面其預設值為2048.

  2. 連線的寫這一半關閉,對這樣的套介面寫操作將產生訊號SIGPIPE。

  3. 有一個套介面錯誤待處理。對這樣的套介面操作寫操作將不阻塞且返回一個錯誤-1,errno設定成明確的錯誤條件。這些待處理的錯誤也可通過指定套介面選項SO_ERROR呼叫getsockopt來取得並清除。

  

  如果一個套介面存在帶外資料或者仍處於帶外標記,那他有異常條件待處理。

   poll留到下一篇吧……

  但,I/O multiplexing 就是這樣用的嗎?

相關文章