1. 程式人生 > >linux網路程式設計之用select函式實現io複用(基於TCP)引發的思考

linux網路程式設計之用select函式實現io複用(基於TCP)引發的思考

1、基本概念


   IO多路複用是指核心一旦發現程序指定的一個或者多個IO條件準備讀取,它就通知該程序。IO多路複用適用如下場合:
  (1)當客戶處理多個描述字時(一般是互動式輸入和網路套介面),必須使用I/O複用。
  (2)當一個客戶同時處理多個套介面時,而這種情況是可能的,但很少出現。
  (3)如果一個TCP伺服器既要處理監聽套介面,又要處理已連線套介面,一般也要用到I/O複用。
  (4)如果一個伺服器即要處理TCP,又要處理UDP,一般要使用I/O複用。
  (5)如果一個伺服器要處理多個服務或多個協議,一般要使用I/O複用。

  與多程序和多執行緒技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必建立程序/執行緒,也不必維護這些程序/執行緒,從而大大減小了系統的開銷。


2、select函式


該函式准許程序指示核心等待多個事件中的任何一個傳送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒。函式原型如下:
#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

返回值:就緒描述符的數目,超時返回0,出錯返回-1

函式引數介紹如下:

(1)第一個引數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(因此把該引數命名為maxfdp1),描述字0、1、2...maxfdp1-1均將被測試。

因為檔案描述符是從0開始的。

(2)中間的三個引數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指標。struct fd_set可以理解為一個集合,這個集合中存放的是檔案描述符,可通過以下四個巨集進行設定:

          void FD_ZERO(fd_set *fdset);           //清空集合
          void FD_SET(int fd, fd_set *fdset);   //將一個給定的檔案描述符加入集合之中
          void FD_CLR(int fd, fd_set *fdset);   //將一個給定的檔案描述符從集合中刪除
          int FD_ISSET(int fd, fd_set *fdset);   // 檢查集合中指定的檔案描述符是否可以讀寫 

一定要注意:FD_ISSET函式,並不是判斷fd是不是在fdset集合裡面,理解如下

判斷描述符fd是否在給定的描述符集fdset中,通常配合select函式使用,由於select函式成功返回時會將未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函式返回後,某個描述符是否準備好,以便進行接下來的處理操作。 當描述符fd在描述符集fdset中返回非零值,否則,返回零。

(3)timeout告知核心等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
         struct timeval{
                   long tv_sec;   //seconds
                   long tv_usec;  //microseconds
       };


這個引數有三種可能:

(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。為此,把該引數設定為空指標NULL。
(2)等待一段固定時間:在有一個描述字準備好I/O時返回,但是不超過由該引數所指向的timeval結構中指定的秒數和微秒數。

(3)根本不等待:檢查描述字後立即返回,這稱為輪詢。為此,該引數必須指向一個timeval結構,而且其中的定時器值必須為0。


3、 原理圖:

4、服務端程式碼

        實現服務端的併發,客戶端想服務端發訊息,然後服務端把收到的訊息再會給客戶端。

        tcp_select.c檔案如下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <assert.h>

#define IPADDR "127.0.0.1"
#define PORT 8888
#define MAXLINE 1024
#define LISTENQ 5
#define SIZE 100

typedef struct server_context_st
{
    int cli_cnt;//客戶端個數
    int clifds[SIZE];//客戶端的個數
    fd_set allfds;//控制代碼集合
    int maxfd;//控制代碼最大值 
} server_context_st;

static server_context_st *s_srv_ctx = NULL;

//初始化服務端contxt
static int server_init()
{
    s_srv_ctx = (struct server_context_st*)malloc(sizeof(server_context_st));
    if (s_srv_ctx == NULL) {
       return -1;
    }
    memset(s_srv_ctx, 0, sizeof(server_context_st));
    for (int i = 0; i < SIZE; i++) {
       s_srv_ctx->clifds[i] = -1;
    }
    return 0;
}


int create_server_proc(const char* ip, int port)
{
    int fd;
    struct sockaddr_in server_addr;
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1) {
       fprintf(stderr, "create socket fail, errno:%d, reason:%s\n", errno, strerror(errno));
       return -1;
    }

    //一個埠釋放後會等待2分鐘才能再次被使用,SO_REUSEADDR是讓埠釋放後立即可以被再次使用
    int reuse  = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
       return -1;
    }
 
    bzero(&server_addr, sizeof(struct sockaddr_in));
    server_addr.sin_family  = AF_INET;
    inet_pton(AF_INET, ip, &server_addr.sin_addr);
    server_addr.sin_port = htons(port);
    
    if (bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error:");
        return -1;
    } 

    listen(fd, LISTENQ);
    return fd;
}

static int accept_client_proc(int srvfd)
{
    struct sockaddr_in cliaddr;
    socklen_t cliaddrlen;
    cliaddrlen = sizeof(cliaddr);
    int clifd = -1;
    puts("accept client proc is call");
ACCEPT:
    clifd = accept(srvfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
    if (clifd == -1) {
        if (errno == EINTR) {
           goto ACCEPT;
        } else {
           fprintf(stderr, "accept fail error:%s\n", strerror(errno));
           return -1;
        }
    }
  
    fprintf(stdout, "accept a new client %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
    
    //將新的連線描述符新增到陣列中
    int i = 0;
    for(i = 0; i < SIZE; i++) {
       if (s_srv_ctx->clifds[i] < 0) {
           s_srv_ctx->clifds[i] = clifd;
           s_srv_ctx->cli_cnt++;
           break;
       }
    }
    
    if (i == SIZE) {
       fprintf(stderr, "too many clients\n");
       return -1;
    }
} 

static int handle_client_msg(int fd, char* buf)
{
   assert(buf);
   printf("recv buf is %s\n", buf);
   write(fd, buf, strlen(buf) + 1);
   return 0;
}

static void recv_client_msg(fd_set *readfds)
{
   int i = 0, n = 0;
   int clifd;
   char buf[MAXLINE] = {0};
   for (i = 0; i <= s_srv_ctx->cli_cnt; i++) {
       clifd = s_srv_ctx->clifds[i];
       if (clifd < 0) {
           continue;
       }
       //判斷客戶端套接字是否有資料
       if (FD_ISSET(clifd, readfds)) {
          //接收客戶端訊息
          n = read(clifd, buf, MAXLINE);
          if (n <= 0) {
             // n == 0表示讀取完才,客戶都關閉套接字
             FD_CLR(clifd, &s_srv_ctx->allfds);
             close(clifd);
             s_srv_ctx->clifds[i] = -1;
             continue;
          }
          handle_client_msg(clifd, buf);
       }
   }

}


//開始接收並且處理客戶端的請求
static void handle_client_proc(int srvfd)
{

   int clifd = -1;
   int retval = 0;
   fd_set *readfds = &s_srv_ctx->allfds;
   struct timeval tv;
   int i = 0;
   while (1) {
       //每次呼叫select前都要重新設定檔案描述符和時間,因為事件發生之後,檔案描述符和時間都被核心修改
       FD_ZERO(readfds);
       FD_SET(srvfd, readfds);
       s_srv_ctx->maxfd = srvfd;
       
       tv.tv_sec = 30;
       tv.tv_usec = 0;
       //新增客戶端套接字
       for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
           clifd = s_srv_ctx->clifds[i];
           //去除無效的客戶端控制代碼 
           if (clifd != -1) {
   	      FD_SET(clifd, readfds);
   	   } 
           s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
       }       
       //開始輪詢接收處理服務端和客戶端套接字 
       retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
       if (retval == -1) {
 	  fprintf(stderr, "select error %s\n", strerror(errno));
	  return;
       }      
       if (retval == 0) {
          fprintf(stdout, "select is timeout\n");
          continue;
       }
       if (FD_ISSET(srvfd, readfds)) 
       {
           //監聽客戶端請求
           accept_client_proc(srvfd);
       } else {
           //接受處理客戶端的訊息
           recv_client_msg(readfds);
      }
   } 
}


static void server_uninit()
{
    if (s_srv_ctx) {
       free(s_srv_ctx);
    } 
    s_srv_ctx = NULL;
}



int main()
{
    int srvfd;
    //初始化服務端contxt
    if (server_init() < 0)
    {
       return -1;
    } 
    //建立服務,開始監聽
    srvfd = create_server_proc(IPADDR, PORT);
    if (srvfd < 0)
    {
       fprintf(stderr, "socket create or bind failed\n");
       goto error;
    }
    //開始接收並處理客戶端請求
    handle_client_proc(srvfd);
    server_uninit();
    return 0;
error:
    server_uninit();
    return -1;
}


5、客戶端程式碼

        tcp_select_client.c檔案如下

#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>




#define MAXLINE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8888

#define max(a,b) (a > b) ? a : b

static void handle_recv_msg(int sockfd, char *buf) 
{
	printf("client recv msg is:%s\n", buf);
	sleep(5);
	write(sockfd, buf, strlen(buf) +1);
}

static void handle_connection(int sockfd)
{
	char sendline[MAXLINE],recvline[MAXLINE];
	int maxfdp,stdineof;
	fd_set readfds;
	int n;
	struct timeval tv;
	int retval = 0;

	while (1) {

	FD_ZERO(&readfds);
	FD_SET(sockfd,&readfds);
	maxfdp = sockfd;

	tv.tv_sec = 5;
	tv.tv_usec = 0;

	retval = select(maxfdp+1,&readfds,NULL,NULL,&tv);

	if (retval == -1) {
	return ;
	}

	if (retval == 0) {
	printf("client timeout.\n");
	continue;
	}

	if (FD_ISSET(sockfd, &readfds)) {
		n = read(sockfd,recvline,MAXLINE);
	if (n <= 0) {
		fprintf(stderr,"client: server is closed.\n");
		close(sockfd);
		FD_CLR(sockfd,&readfds);
		return;
	}

	handle_recv_msg(sockfd, recvline);
	}
 }
}

int main(int argc,char *argv[])
{
	int sockfd;
	struct sockaddr_in servaddr;

	sockfd = socket(AF_INET,SOCK_STREAM,0);

	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);

	int retval = 0;
	retval = connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	if (retval < 0) {
		fprintf(stderr, "connect fail,error:%s\n", strerror(errno));
		return -1;
	}

	printf("client send to server .\n");
	write(sockfd, "hello server", 32);

	handle_connection(sockfd);

return 0;
}

6、執行結果


7、FD_ISSET函式的迷惑和總結

   while (1) {
       //每次呼叫select前都要重新設定檔案描述符和時間,因為事件發生之後,檔案描述符和時間都被核心修改
       FD_ZERO(readfds);
       FD_SET(srvfd, readfds);
       s_srv_ctx->maxfd = srvfd;
       
       tv.tv_sec = 30;
       tv.tv_usec = 0;
       //新增客戶端套接字
       for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
           clifd = s_srv_ctx->clifds[i];
           //去除無效的客戶端控制代碼 
           if (clifd != -1) {
   	      FD_SET(clifd, readfds);
   	   } 
           s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
       }       
       //開始輪詢接收處理服務端和客戶端套接字 
       retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
       if (retval == -1) {
 	  fprintf(stderr, "select error %s\n", strerror(errno));
	  return;
       }      
       if (retval == 0) {
          fprintf(stdout, "select is timeout\n");
          continue;
       }
       if (FD_ISSET(srvfd, readfds)) 
       {
           //監聽客戶端請求
           accept_client_proc(srvfd);
       } else {
           //接受處理客戶端的訊息
           recv_client_msg(readfds);
      }
   } 
一開始,很明顯
 FD_SET(srvfd, readfds);
然後select函式掛起,如果有事件發生就觸發了select函式,然後進行判斷
if (FD_ISSET(srvfd, readfds)) 
剛才才把srvfd加到readds裡面去,現在判斷是不是在裡面,肯定在呀,那程式怎麼進入else裡面呢?這不日了狗嗎?一直不理解,日了狗,後面再群裡面問了也不是太懂,可能自己計算機資質太差了,然後吃完飯問搞服務端的人,才搞明白。

我們先看下函式FD_ISSET函式解釋

一定要注意:FD_ISSET函式,並不是判斷fd是不是在fdset集合裡面,理解如下

判斷描述符fd是否在給定的描述符集fdset中,通常配合select函式使用,由於select函式成功返回時會將未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函式返回後,某個描述符是否準備好,以便進行接下來的處理操作。 當描述符fd在描述符集fdset中返回非零值,否則,返回零。

當第一個客戶端第一次執行Connect的函式的時候會呼叫select函式,然後這是時候srvfd,準備就緒,設定為1,所以FD_ISSET函式大於0,所以會執行accept_client_proc函式,這個時候服務端得到客戶端的fd,也就是clifd,因為執行TCP三次握手之後,客戶端再次發訊息,這個時候客戶端和服務端通訊也就是服務端新的clifd通訊,當客戶端發訊息的時候,srvfd基本上就被遺棄了,沒有準備好,會把srvfd清0,所以這個時候執行FD_ISSET函式的話,程式會跑到else裡面去,也就是執行recv_client_msg函式,好了,終於搞懂了,不知道理解有沒有錯,明天再學習poll epoll來實現io複用。

select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點。select的一 個缺點在於單個程序能夠監視的檔案描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改巨集定義甚至重新編譯核心的方式提升這一限制,但 是這樣也會造成效率的降低。