并发程序设计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,©set,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,©set)) 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 }