并发程序设计2:多路IO复用技术(1)

  上一节https://www.cnblogs.com/yuanwebpage/p/12361275.html记录了多进程并发程序,除了已经描述的缺点,考虑服务器端一直在调用accept函数结束客户端请求,所以没办法进行其他响应,如响应用户的输入/输出。而多路IO复用除了能同时执行一种IO的多个操作,还能响应不同类IO的操作。

1. 基于select的IO复用

  select的IO复用原理很简单。每个文件描述符在Linux系统下就是一个整数,比如现在有4个应用客户端与服务器建立连接,其套接字分别为fd0,fd1,fd2,fd3,select的原理就是将这些套接字集中起来管理,采用fd_set数组,fd_set数组每一位代表一个套接字状态,当把fd0,fd1,fd2,fd3装入fd_set时,其状态如图1.1所示

                                                                       

 

                                                                            图1.1 select函数管理的fd_set数组

将某个套接字添加到fd_set数组后,将其全部置0。调用select函数时,发生事件(如fd2的套接字接收到客户端发来的消息),对应的数组位就会从0变成1,此时检测fd_set数组中从0变成1的那些位,就是发生变化的描述符。因此,使用select函数流程如下:

(1) 设置文件描述符,即声明fd_set变量;

(2) 将要监视的描述符加入fd_set数组;

(3) 将数组清零;

(4) 设置超时时间(select函数是阻塞型的,因此设置超时时间,超时还没有fd_set数组变化就返回);

(5) 调用select,监视变化,并进行后续处理。

下面看select函数的具体用法

#include <sys/select.h>
#include <time.h> int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout); maxfd:监视的文件描述符个数 readset:记录“是否存在待读取的文件描述符”的变量 writeset:记录“是否存在待写的文件描述符”的变量 exceptset:记录所有异常的文件描述符的变量, timeout:设置超时的结构体 struct timeval { long tv_sec; // long tv_usec; //毫秒 }

关于fd_set的添加,清零,检测变化的函数如下:

FD_SET(int fd, fd_set* fdset); //将文件描述符fd注册到fdset中
FD_CLR(int fd, fd_set* fdset); //将文件描述符fd从fdset中删除
FD_ZERO(fd_set* fdset);  //将fdset清零
FD_ISSET(int fd, fd_set* fdset);  //判断fd是否发生变化,即是否有事件来临

有了以上基础知识,现在将上一节的基于多进程的回声服务器的服务端改为基于select IO的IO复用。下面是具体代码:

  1 #include <stdlib.h>
  2 #include <stdio.h>
  3 #include <string.h>
  4 #include <sys/socket.h>
  5 #include <arpa/inet.h>
  6 #include <unistd.h>
  7 #include <sys/select.h>
  8 #include <sys/time.h>
  9 
 10 void error_handle(const char* msg)
 11 {
 12     fputs(msg,stderr);
 13     fputc('\n',stderr);
 14     exit(1);
 15 }
 16 
 17 int main(int argc,char* argv[])
 18 {
 19     //服务器建立连接
 20     int servsock,clntsock;
 21     struct sockaddr_in servaddr,clntaddr;
 22     char message[50];
 23     socklen_t clntlen;
 24     
 25     if(argc!=2)
 26         error_handle("Please input port number");
 27     
 28     servsock=socket(PF_INET,SOCK_STREAM,0);  //1.建立套接字
 29     
 30     memset(&servaddr,0,sizeof(servaddr));
 31     servaddr.sin_family=AF_INET;
 32     servaddr.sin_addr.s_addr=htonl(INADDR_ANY);  //默认本机IP地址
 33     servaddr.sin_port=htons(atoi(argv[1]));  
 34     
 35     if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
 36         error_handle("bind error");  //2.建立连接
 37     
 38     if(listen(servsock,10)==-1) //3.监听建立
 39         error_handle("listen() error");
 40     
 41     //到此为止的代码与上一节相同,均为服务器socket的建立
 42     
 43     //设置fd_set
 44     int fdmax,fd_num;
 45     struct timeval timeout; //设置超时时间
 46     fd_set readset,copyset;
 47     FD_ZERO(&readset);
 48     FD_SET(servsock,&readset);
 49     fdmax=servsock;  //因为Linux系统下分配的描述符都是从0开始递增,因此监视的描述符数量等于最新申请到的描述符+1,即fdmax+1
 50     while(1)
 51     {
 52         copyset=readset;
 53         timeout.tv_sec=3;
 54         timeout.tv_usec=500;  //设置超时时间3.5s
 55         
 56         int fd_num=select(fdmax+1,&copyset,0,0,&timeout); //只有读取事件监视
 57         if(fd_num<0)
 58             error_handle("select() error");
 59         else if(fd_num==0)
 60         {
 61             printf("timeout\n");
 62             continue;
 63         }
 64         else  //发生了监听事件
 65         {
 66             for(int i=0;i<fdmax+1;i++) //遍历所有监视的文件描述符
 67             {
 68                 if(FD_ISSET(i,&copyset))
 69                 {
 70                     if(i==servsock) //有客户端来建立连接
 71                     {
 72                         clntlen=sizeof(clntaddr);
 73                         if((clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen))==-1) //接收连接请求
 74                             printf("accept() error\n");
 75 
 76                         printf("connecting\n");
 77                         FD_SET(clntsock,&readset); //新的描述符添加进监视
 78                         if(fdmax<clntsock)
 79                             fdmax=clntsock; //新的最大文件描述符
 80                     }
 81                     else  //说明是已建立的客户端发来的消息
 82                     {
 83                         int str_len=read(i,message,sizeof(message));
 84                         if(str_len<0)
 85                         {
 86                             printf("read() error\n");
 87                         }
 88                         else if(str_len==0) //客户端断开连接
 89                         {
 90                             FD_CLR(i,&readset); //清除该描述符
 91                             close(i);
 92                         }
 93                         else
 94                             write(i,message,str_len);
 95                     }
 96                 }
 97                 
 98             }
 99         }
100     }
101     close(servsock);
102     return 0;
103     
104 }

 

 

ps:以上代码有几点注意事项

(1) 头文件内一定要包含time.h。之前我的程序没包含这个头文件,不报错,而且timeout提示也正常出现,但是一连接客户端就卡死。估计是select的定时用到了time.h内的一些机制;

(2) 52-54行每个循环内都要重置timeout结构体并将初始时的readset复制到copyset,并用copyset调用select函数。因为timeout结构体的值随着计时改变而改变,即定时结束时,值变成了0,需要重置;而每次复制初始的readset是因为select会改变数组的内容,必须保存原始数组内容。

 

select主要有以下缺点:

(1) 套接字或文件描述符是属于OS所有。因此每次select函数实际上都向操作系统重新传递了一遍文件描述符,从应用程序向OS传递数据是很耗时的;

(2) 因为发生文件描述符变化时,不知道具体是哪个发生变化。因此对于fd_set内管理的所有文件描述符,都需要遍历以找到变换的描述符(对应每次for循环)。

以上两个原因造成select在大规模多客户端时非常耗时。

但select也有优点:

(1) 几乎所有操作系统都支持select函数,这使得基于select的编程移植性强。

 

 

 因此对于连接少,断开不频繁的操作,select也有其优越性。

 

2. pselect

pselect的基本用法和功能跟select非常相似,函数原型如下:

#include <sys/select.h>

int pselect(int maxfd,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,,const struct timespec* tsptr,const sigset_t* sigmask);

跟select的主要不同在于:

(1) select的超时结构体为timeval,其中用秒(tv_sec)和微秒(tv_usec)表示时间;pselect的超时结构体为timespec,用秒(tv_sec)和纳秒(tv_nsec,long类型)表示时间;

(2) pselect的超时设置结构体为const,一直不会改变,这样就不用每次调用pselect的时候都重置timespec,只需要初始化一次即可;

(3) pselect最后一个参数为可选信号屏蔽字,如果置为NULL,则与select调用结果相同。

 

3. poll

poll()函数虽然长得像epoll,但是其实现原理跟select基本一样,因此把它放入本节。poll函数的原型如下:

#include <poll.h>

int poll(struct pollfd fdarray[],nfds_t nfds,int timeout); //timeout单位为毫秒

struct pollfd
{
    int fd;
    short events; //要监视的事件类型
    short revents; //发生的事件类型
}

其中监视类型和返回类型的具体种类如下图:

             

 

                     第一行为输入事件;第二行为输出事件;第三行不需要注册,发生时自动返回

该函数使用方法与select基本相同。下面给出服务器端的代码:

  1 #include <stdlib.h>
  2 #include <stdio.h>
  3 #include <string.h>
  4 #include <sys/socket.h>
  5 #include <arpa/inet.h>
  6 #include <unistd.h>
  7 #include <sys/select.h>
  8 #include <sys/time.h>
  9 #include <poll.h>
 10 
 11 #define MAX_EVENT  100 //监视事件的最大数量
 12 
 13 void error_handle(const char* msg)
 14 {
 15     fputs(msg,stderr);
 16     fputc('\n',stderr);
 17     exit(1);
 18 }
 19 
 20 
 21 int main(int argc,char* argv[])
 22 {
 23     //服务器建立连接
 24     int servsock,clntsock;
 25     struct sockaddr_in servaddr,clntaddr;
 26     char message[50];
 27     socklen_t clntlen;
 28     
 29     if(argc!=2)
 30         error_handle("Please input port number");
 31     
 32     servsock=socket(PF_INET,SOCK_STREAM,0);  //1.建立套接字
 33     
 34     memset(&servaddr,0,sizeof(servaddr));
 35     servaddr.sin_family=AF_INET;
 36     servaddr.sin_addr.s_addr=htonl(INADDR_ANY);  //默认本机IP地址
 37     servaddr.sin_port=htons(atoi(argv[1]));  
 38     
 39     if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
 40         error_handle("bind error");  //2.建立连接
 41     
 42     if(listen(servsock,10)==-1) //3.监听建立
 43         error_handle("listen() error");
 44     
 45     //到此为止的代码与上一节相同,均为服务器socket的建立
 46     
 47     //设置fd_set
 48     int fd_num;
 49     servsock;  //因为Linux系统下分配的描述符都是从0开始递增,因此监视的描述符数量等于最新申请到的描述符+1,即fdmax+1
 50     struct pollfd POLLFD[MAX_EVENT];
 51     POLLFD[0].fd=servsock;
 52     POLLFD[0].events=POLLIN;
 53     int cur_event_num=1; //记录POLLFD结构体含有的成员数量
 54     
 55     
 56     while(1)
 57     {
 58         int fd_num=poll(POLLFD,cur_event_num,3000); //只有读取事件监视
 59         printf("fd_num:%d\n",fd_num);
 60         if(fd_num<0)
 61             error_handle("poll() error");
 62         else if(fd_num==0)
 63         {
 64             printf("timeout\n");
 65             continue;
 66         }
 67         else  //发生了监听事件
 68         {
 69             for(int i=0;i<cur_event_num;i++) //遍历所有监视的文件描述符
 70             {
 71                 if(POLLFD[i].revents==POLLIN)
 72                 {
 73                     if(POLLFD[i].fd==servsock) //有客户端来建立连接
 74                     {
 75                         clntlen=sizeof(clntaddr);
 76                         if((clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen))==-1) //接收连接请求
 77                             printf("accept() error\n");
 78 
 79                         printf("connecting\n");
 80                         if(cur_event_num==MAX_EVENT)
 81                         {
 82                             printf("POLLFD is full\n");
 83                             close(clntsock);
 84                             continue;
 85                         }
 86                         else{
 87                             POLLFD[cur_event_num].fd=clntsock;
 88                             POLLFD[cur_event_num].events=POLLIN;
 89                             cur_event_num++;
 90                         }
 91                     }
 92                     else  //说明是已建立的客户端发来的消息
 93                     {
 94                         memset(message,0,sizeof(message));
 95                         int str_len=read(POLLFD[i].fd,message,sizeof(message));
 96                         if(str_len<0)
 97                         {
 98                             printf("read() error\n");
 99                         }
100                         else if(str_len==0) //客户端断开连接
101                         {
102                             close(POLLFD[i].fd);
103                             for(int j=i;j<cur_event_num-1;j++){
104                                 POLLFD[i].fd=POLLFD[i+1].fd;
105                                 POLLFD[i].events=POLLFD[i+1].events;
106                             }
107                             cur_event_num--;
108                         }
109                         else
110                             write(POLLFD[i].fd,message,str_len);
111                     }
112                 }
113                 
114             }
115         }
116     }
117     close(servsock);
118     return 0;
119     
120 }
View Code

 

posted @ 2020-02-25 17:45  晨枫1  阅读(267)  评论(0编辑  收藏  举报