IO多路复用

一、五种网络I/O模型

1.四种调用方式

  • 描述用户线程与内核的交互方式:
    同步:是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;(知道事情什么时候会发生)
    异步:是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。(不知道事情什么时候会发生)
  • 描述用户线程调用内核IO操作的方式:
    阻塞:是指IO操作在没有接收完数据者没有得到结果之前不会返回,需要彻底完成后才返回到用户空间;
    非阻塞:是指IO操作被调用后立即返回给用一个状态值,无需等到IO操作彻底完成。(异步不等于非阻塞,异步会发消息,非阻塞要自己查看)

2.五种常见的IO模型

  1. 同步阻塞IO(Blocking IO):即传统的IO模型,在linux中默认情况下所有的socket都是阻塞模式。理解:给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不会做其他事情。
  2. 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的。理解:给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信和等待不会做其他事情;
  3. IO多路复用(IO Multiplexing):IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题,此外poll、epoll都是这种模型。理解: 找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以做些其他的事情,例如可以顺便玩玩王者荣耀, 打篮球等等。IO复用又包括 select, poll, epoll 模式,它们的区别是:
  • select大妈:每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子;
  • poll大妈:不限制盯着女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神;
  • epoll大妈:不限制盯着女生的数量, 并且也不需要一个一个去问, epoll大妈会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大妈再通知你。
  1. 信号驱动IO(signal driven IO):调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是阻塞的。
  2. 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。“真正”的异步IO需要操作系统更强的支持。

二、select多路复用

1.select函数

  1. select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。
  2. select函数
#include <sys/select.h>
#include <sys/time.h>
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds微秒
};

FD_ZERO(fd_set* fds)            //清空集合
FD_SET(int fd, fd_set* fds)     //将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中
FD_CLR(int fd, fd_set* fds)    //将给定的描述符从文件中删除 

int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);//最后一个参数是设置select的超时时间,如果设置为NULL则永不超时

说明:

  • select监视并等待多个文件描述符的属性发生变化,它监视的属性分3类,分别是readfds(文件描述符有数据到来可读)、writefds(文件描述符可写)、和exceptfds(文件描述符异常);
  • 调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间)发生函数才返回。当select()函数返回后,可以通过遍历 fdset,来找到究竟是哪些文件描述符就绪;
  • select函数的返回值是就绪描述符的数目,超时时返回0,出错返回-1;
  • 中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为NULL;
  • 需要注意的是待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1;
  • 在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数中,这也意味着select所用到的FD_SET是有限的,也正是这个原因select()默认只能同时处理1024个客户端的连接请求:
/linux/posix_types.h:
#define __FD_SETSIZE 1024

2.使用select()多路复用实现网络socket服务器多路并发的流程图

在这里插入图片描述

3.使用select()多路复用实现的服务器端示例代码

 1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <errno.h>
  6 #include <ctype.h>
  7 #include <time.h>
  8 #include <pthread.h>
  9 #include <getopt.h>
 10 #include <libgen.h>
 11 #include <sys/types.h>
 12 #include <sys/socket.h>
 13 #include <arpa/inet.h>
 14 #include <netinet/in.h>
 15 
 16 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))    //整个数组的长度除以第一个元素的长度等于元素的个数
 17 
 18 static inline void msleep(unsigned long ms);
 19 static inline void print_usage(char *progname);
 20 int socket_server_init(char *listen_ip, int listen_port);
 21 
 22 int main(int argc, char **argv)
 23 {
 24         int          listenfd, connfd;
 25         int          serv_port = 0;
 26         int          daemon_run = 0;
 27         char         *progname = NULL;
 28         int          opt;
 29         fd_set       rdset;
 30         int          rv;
 31         int          i, j;
 32         int          found;
 33         int          maxfd=0;
 34         char         buf[1024];
 35         int          fds_array[1024];
 36         //参数解析
 37         struct option     long_options[] =
 38         {
 39                 {"daemon", no_argument, NULL, 'b'},
 40                 {"port", required_argument, NULL, 'p'},
 41                 {"help", no_argument, NULL, 'h'},
 42                 {NULL, 0, NULL, 0}
 43         };
 44 
 45         //basename的作用就是找出./a.out中./后面的内容
 46         progname = basename(argv[0]);
 47         //解析命令行参数
 48         while ((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1)
 49         {
 50                 switch (opt)
 51                 {
 52                         case 'b':
 53                                 daemon_run=1;
 54                                 break;
 55                         case 'p':
 56                                 serv_port = atoi(optarg);
 57                                 break;
 58                         case 'h': //得到帮助信息
 59                                 print_usage(progname);
 60                                 return EXIT_SUCCESS;
 61                         default:
 62                                 break;
 63                 }
 64         }
 65         //判断是否设置端口
 66         if( !serv_port )
 67         {
 68                 print_usage(progname);
 69                 return -1;
 70         }
 71 
 72         //监听,socket_server_init这个函数还需要加强学习
 73         if( (listenfd=socket_server_init(NULL, serv_port)) < 0 )
 74         {
 75                 printf("ERROR: %s server listen on port %d failure\n", argv[0],serv_port);
 76                 return -2;
 77         }
 78         printf("%s server start to listen on port %d\n", argv[0],serv_port);
 79 
 80         //设置程序在后台运行
 81         if( daemon_run )
 82         {
 83                 daemon(0, 0);
 84         }
 85         //ARRAY_SIZE是一个比较经典的宏,作用为求一个数组内有多少个元素
 86         for(i=0; i<ARRAY_SIZE(fds_array) ; i++)
 87         {
 88                 fds_array[i]=-1;//将每一个元素都设置为-1,就可以看作这个数组为空,不设为0是因为文件描述符也有可能为0
 89         }
 90         /*最开始没有客户端,只有listenfd*/
 91         fds_array[0] = listenfd;//将listenfd的文件描述符加入到array_size里
 92 
 93         /*将listenfd加入到array_size中去*/
 94         for ( ; ; )//标准的死循环模式
 95         {
 96                 FD_ZERO(&rdset);//将rd的集合牵连,FD_ZERO一般是个死循环
 97                 for(i=0; i<ARRAY_SIZE(fds_array) ; i++)//遍历ARRAY_SIZE每个元素
 98                 {
 99                         if( fds_array[i] < 0 )//小于0即为不存在
100                                 continue;
101                         maxfd = fds_array[i]>maxfd ? fds_array[i] : maxfd;//如果maxfd比当前的array要大,就更新为maxfd,否则保持不变
102                         FD_SET(fds_array[i], &rdset);//将array_aize文件描述符加到rdset这个集合里面去
103                 }
104 
105                 /*调用select函数,刚开始只有listenfd发生了事件,使array_size获得了文件描述符connfd,将connfd加入到array_size中后,更新集合,再次调用select函数,这时有listen和刚刚已经连接的客户端都会发生事件,需要判断,判断后读写*/
106                 //程序会阻塞在这里;select函数重点
107                 rv = select(maxfd+1, &rdset, NULL, NULL, NULL);
108                 if(rv < 0)//小于0即为select失败
109                 {
110                         printf("select failure: %s\n", strerror(errno));
111                         break;
112                 }
113                 else if(rv == 0)//等于即为select超时
114                 {
115                         printf("select get timeout\n");
116                         continue;
117                 }
118                 //大于0就要判断是谁发生的,有两类事件,一是listen的connfd,一是已经连上的客户端
119                 if ( FD_ISSET(listenfd, &rdset) )//如果是listen,就意味着有新的客户端连过来
120                 {
121                         //调用accept,返回新的文件描述符connfd(文件描述符的知识要加强)
122                         if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0)
123                         {
124                                 printf("accept new client failure: %s\n", strerror(errno));
125                                 continue;
126                         }
127                         //将新的文件描述符放到array_size里面去
128                         found=0;//将found设为0避免万一有1025个客户端来,会溢出
129                         for(i=0; i<ARRAY_SIZE(fds_array) ; i++)
130                         {
131                                 if( fds_array[i] < 0 )//小于0意味着该位置还没有被占用
132                                 {
133                                         printf("accept new client[%d] and add it into array\n", connfd );
134                                         fds_array[i] = connfd;//将connfd加入到array_size中去
135                                         found = 1;
136                                         break;
137                                 }
138                         }
139 
140                         if( !found )//找不到found,说明1024个位置都被占用了
141                         {
142                                 printf("accept new client[%d] but full, so refuse it\n", connfd);
143                                 close(connfd);//无法处理connfd了,所以关闭
144                         }
145                 }
146                 else //已经被客户端连接
147                 {
148                         for(i=0; i<ARRAY_SIZE(fds_array); i++)//遍历整个数组
149                         {
150                                 if( fds_array[i]<0 || !FD_ISSET(fds_array[i], &rdset) )//小于0或者不是这个客户端发生的事件
151                                         continue;
152                                 //找到了这个>文件描述符,为fds_array[i],因此读取
153                                 if( (rv=read(fds_array[i], buf, sizeof(buf))) <= 0)//小于等于0意味着要么异常,要么对方断了
154                                 {
155                                         printf("socket[%d] read failure or get disconncet.\n", fds_array[i]);
156                                         close(fds_array[i]);//将对方关闭
157                                         fds_array[i] = -1;//将该位置腾出来
158                                 }
159                                 else
160                                 {
161                                         printf("socket[%d] read get %d bytes data\n", fds_array[i], rv);
162                                         //小写转大写
163                                         for(j=0; j<rv; j++)
164                                                 buf[j]=toupper(buf[j]);
165                                         //将其写进array_size里
166                                         if( write(fds_array[i], buf, rv) < 0 )//写出错了
167                                         {
168                                                 printf("socket[%d] write failure: %s\n", fds_array[i], strerror(errno));
169                                                 close(fds_array[i]);//将其关闭
170                                                 fds_array[i] = -1;//腾出位置
171                                         }
172                                 }
173                         }
174                 }
175         }
176 
177 CleanUp:
178         close(listenfd);
179         return 0;
180 }
181 
182 //msleep函数实现了一个3500ms延时的机制
183 static inline void msleep(unsigned long ms)
184 {
185         struct timeval tv;
186         tv.tv_sec = ms/1000;
187         tv.tv_usec = (ms%1000)*1000;
188 
189         select(0, NULL, NULL, NULL, &tv);
190 }
191 static inline void print_usage(char *progname)
192 {
193         printf("Usage: %s [OPTION]...\n", progname);
194 
195         printf(" %s is a socket server program, which used to verify client and echo back string from it\n",
196                         progname);
197         printf("\nMandatory arguments to long options are mandatory for short options too:\n");
198 
199         printf(" -b[daemon ] set program running on background\n");
200         printf(" -p[port ] Socket server port address\n");
201         printf(" -h[help ] Display this help information\n");
202 
203         printf("\nExample: %s -b -p 8873\n", progname);
204         return ;
205 }
206 int socket_server_init(char *listen_ip, int listen_port)
207 {
208         struct sockaddr_in servaddr;
209         int rv = 0;
210         int on = 1;
211         int listenfd;
212         if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
213         {
214                 printf("Use socket() to create a TCP socket failure: %s\n", strerror(errno));
215                 return -1;
216         }
217         //设置 socket 端口可重用,修复 socket 服务器重新启动时“地址已经在使用”的 bug
218         setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
219         memset(&servaddr, 0, sizeof(servaddr));
220         servaddr.sin_family = AF_INET;
221         servaddr.sin_port = htons(listen_port);
222 
223         //监听所有公众ip的地址
224         if( !listen_ip )//未传过来ip
225         {
226                 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //将其ip地址设为INADDR_ANY
227         }
228         else //监听指定ip的地址
229         {
230                 if (inet_pton(AF_INET, listen_ip, &servaddr.sin_addr) <= 0)//将其ip转入四字节的sin.addr中去
231                 {
232                         printf("inet_pton() set listen IP address failure.\n");
233                         rv = -2;
234                         goto CleanUp;
235                 }
236         }
237         if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
238         {
239                 printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
240                 rv = -3;
241                 goto CleanUp;
242         }
243         if(listen(listenfd, 13) < 0)
244         {
245                 printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
246                 rv = -4;
247                 goto CleanUp;
248         }
249 CleanUp:
250         if(rv<0)
251                 close(listenfd);
252         else
253                 rv = listenfd;
254         return rv;
255 }                                                                                                                                                                                                                                                                                                                                                                                                                         

基于select的I/O复用模型的是单进程执行可以为多个客户端服务,这样可以减少创建线程或进程所需要的CPU时间片或内存资源的开销;此外几乎所有的平台上都支持select(),其良好跨平台支持是它的另一个优点。当然它也有两个主要的缺点:

  1. 每次调用 select()都需要把fd集合从用户态拷贝到内核态,之后内核需要遍历所有传递进来的fd,这时如果客户端fd很多时会导致系统开销很大;
  2. 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过setrlimit()、修改宏定义甚至重新编译内核等方式来提升这一限制,但是这样也会造成效率的降低。

三、poll多路复用

1. poll函数

  1. select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在System V中引入的。
  2. poll函数的原型说明如下:
#include <poll.h>

struct pollfd
{
 int fd; /* 文件描述符 */
 short events; /* 等待的事件 */
 short revents; /* 实际发生了的事件 */
} ;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 第一个参数用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符;
  • 第二个参数 nfds 指定数组中监听的元素个数;
  • 第三个参数 timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
  • 该函数成功调用时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
      EBADF   一个或多个结构体中指定的文件描述符无效。
      EFAULTfds   指针指向的地址超出进程的地址空间。
      EINTR     请求的事件之前产生一个信号,调用可以重新发起。
      EINVALnfds  参数超出PLIMIT_NOFILE值。
      ENOMEM   可用内存不足,无法完成请求。

2.使用poll()多路复用实现的服务器端示例代码

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <errno.h>
  6 #include <ctype.h>
  7 #include <time.h>
  8 #include <pthread.h>
  9 #include <getopt.h>
 10 #include <libgen.h>
 11 #include <sys/types.h>
 12 #include <sys/socket.h>
 13 #include <arpa/inet.h>
 14 #include <netinet/in.h>
 15 
 16 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))    //整个数组的长度除以第一个元素的长度等于元素的个数
 17 
 18 static inline void msleep(unsigned long ms);
 19 static inline void print_usage(char *progname);
 20 int socket_server_init(char *listen_ip, int listen_port);
 21 
 22 int main(int argc, char **argv)
 23 {
 24         int          listenfd, connfd;
 25         int          serv_port = 0;
 26         int          daemon_run = 0;
 27         char         *progname = NULL;
 28         int          opt;
 29         fd_set       rdset;
 30         int          rv;
 31         int          i, j;
 32         int          found;
 33         int          maxfd=0;
 34         char         buf[1024];
 35         int          fds_array[1024];
 36         //参数解析
 37         struct option     long_options[] =
 38         {
 39                 {"daemon", no_argument, NULL, 'b'},
 40                 {"port", required_argument, NULL, 'p'},
 41                 {"help", no_argument, NULL, 'h'},
 42                 {NULL, 0, NULL, 0}
 43         };
 44 
 45         //basename的作用就是找出./a.out中./后面的内容
 46         progname = basename(argv[0]);
 47         //解析命令行参数
 48         while ((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1)
 49         {
 50                 switch (opt)
 51                 {
 52                         case 'b':
 53                                 daemon_run=1;
 54                                 break;
 55                         case 'p':
 56                                 serv_port = atoi(optarg);
 57                                 break;
 58                         case 'h': //得到帮助信息
 59                                 print_usage(progname);
 60                                 return EXIT_SUCCESS;
 61                         default:
 62                                 break;
 63                 }
 64         }
 65         //判断是否设置端口
 66         if( !serv_port )
 67         {
 68                 print_usage(progname);
 69                 return -1;
 70         }
 71 
 72         //监听,socket_server_init这个函数还需要加强学习
 73         if( (listenfd=socket_server_init(NULL, serv_port)) < 0 )
 74         {
 75                 printf("ERROR: %s server listen on port %d failure\n", argv[0],serv_port);
 76                 return -2;
 77         }
 78         printf("%s server start to listen on port %d\n", argv[0],serv_port);
 79 
 80         //设置程序在后台运行
 81         if( daemon_run )
 82         {
 83                 daemon(0, 0);
 84         }
 85         //ARRAY_SIZE是一个比较经典的宏,作用为求一个数组内有多少个元素
 86         for(i=0; i<ARRAY_SIZE(fds_array) ; i++)
 87         {
 88                 fds_array[i]=-1;//将每一个元素都设置为-1,就可以看作这个数组为空,不设为0是因为文件描述符也有可能为0
 89         }
 90         /*最开始没有客户端,只有listenfd*/
 91         fds_array[0] = listenfd;//将listenfd的文件描述符加入到array_size里
 92 
 93         /*将listenfd加入到array_size中去*/
 94         for ( ; ; )//标准的死循环模式
 95         {
 96                 FD_ZERO(&rdset);//将rd的集合牵连,FD_ZERO一般是个死循环
 97                 for(i=0; i<ARRAY_SIZE(fds_array) ; i++)//遍历ARRAY_SIZE每个元素
 98                 {
 99                         if( fds_array[i] < 0 )//小于0即为不存在
100                                 continue;
101                         maxfd = fds_array[i]>maxfd ? fds_array[i] : maxfd;//如果maxfd比当前的array要大,就更新为maxfd,否则保持不变
102                         FD_SET(fds_array[i], &rdset);//将array_aize文件描述符加到rdset这个集合里面去
103                 }
104 
105                 /*调用select函数,刚开始只有listenfd发生了事件,使array_size获得了文件描述符connfd,将connfd加入到array_size中后,更新集合,再次调用select函数,这时有listen和刚刚已经连接的客户端都会发生事件,需要判断,判断后读写*/
106                 //程序会阻塞在这里;select函数重点
107                 rv = select(maxfd+1, &rdset, NULL, NULL, NULL);
108                 if(rv < 0)//小于0即为select失败
109                 {
110                         printf("select failure: %s\n", strerror(errno));
111                         break;
112                 }
113                 else if(rv == 0)//等于即为select超时
114                 {
115                         printf("select get timeout\n");
116                         continue;
117                 }
118                 //大于0就要判断是谁发生的,有两类事件,一是listen的connfd,一是已经连上的客户端
119                 if ( FD_ISSET(listenfd, &rdset) )//如果是listen,就意味着有新的客户端连过来
120                 {
121                         //调用accept,返回新的文件描述符connfd(文件描述符的知识要加强)
122                         if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0)
123                         {
124                                 printf("accept new client failure: %s\n", strerror(errno));
125                                 continue;
126                         }
127                         //将新的文件描述符放到array_size里面去
128                         found=0;//将found设为0避免万一有1025个客户端来,会溢出
129                         for(i=0; i<ARRAY_SIZE(fds_array) ; i++)
130                         {
131                                 if( fds_array[i] < 0 )//小于0意味着该位置还没有被占用
132                                 {
133                                         printf("accept new client[%d] and add it into array\n", connfd );
134                                         fds_array[i] = connfd;//将connfd加入到array_size中去
135                                         found = 1;
136                                         break;
137                                 }
138                         }
139 
140                         if( !found )//找不到found,说明1024个位置都被占用了
141                         {
142                                 printf("accept new client[%d] but full, so refuse it\n", connfd);
143                                 close(connfd);//无法处理connfd了,所以关闭
144                         }
145                 }
146                 else //已经被客户端连接
147                 {
148                         for(i=0; i<ARRAY_SIZE(fds_array); i++)//遍历整个数组
149                         {
150                                 if( fds_array[i]<0 || !FD_ISSET(fds_array[i], &rdset) )//小于0或者不是这个客户端发生的事件
151                                         continue;
152                                 //找到了这个>文件描述符,为fds_array[i],因此读取
153                                 if( (rv=read(fds_array[i], buf, sizeof(buf))) <= 0)//小于等于0意味着要么异常,要么对方断了
154                                 {
155                                         printf("socket[%d] read failure or get disconncet.\n", fds_array[i]);
156                                         close(fds_array[i]);//将对方关闭
157                                         fds_array[i] = -1;//将该位置腾出来
158                                 }
159                                 else
160                                 {
161                                         printf("socket[%d] read get %d bytes data\n", fds_array[i], rv);
162                                         //小写转大写
163                                         for(j=0; j<rv; j++)
164                                                 buf[j]=toupper(buf[j]);
165                                         //将其写进array_size里
166                                         if( write(fds_array[i], buf, rv) < 0 )//写出错了
167                                         {
168                                                 printf("socket[%d] write failure: %s\n", fds_array[i], strerror(errno));
169                                                 close(fds_array[i]);//将其关闭
170                                                 fds_array[i] = -1;//腾出位置
171                                         }
172                                 }
173                         }
174                 }
175         }
176 
177 CleanUp:
178         close(listenfd);
179         return 0;
180 }
181 
182 //msleep函数实现了一个3500ms延时的机制
183 static inline void msleep(unsigned long ms)
184 {
185         struct timeval tv;
186         tv.tv_sec = ms/1000;
187         tv.tv_usec = (ms%1000)*1000;
188 
189         select(0, NULL, NULL, NULL, &tv);
190 }
191 static inline void print_usage(char *progname)
192 {
193         printf("Usage: %s [OPTION]...\n", progname);
194 
195         printf(" %s is a socket server program, which used to verify client and echo back string from it\n",
196                         progname);
197         printf("\nMandatory arguments to long options are mandatory for short options too:\n");
198 
199         printf(" -b[daemon ] set program running on background\n");
200         printf(" -p[port ] Socket server port address\n");
201         printf(" -h[help ] Display this help information\n");
202 
203         printf("\nExample: %s -b -p 8873\n", progname);
204         return ;
205 }

四、epoll多路复用

1.select的缺点:

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
  2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

2.epoll设计

epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个部分:

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源);
  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字;
  3. 调用epolll_wait收集发生的事件的连接。

3. 一些相关函数

  1. 创建epoll实例:epoll_create()
#include <sys/epoll.h>
int epoll_create(int size);
  • 系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。
  • 参数size指定了我们想要通过epoll实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。
  1. 修改epoll的兴趣列表:epoll_ctl()
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
  • 系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。若成功返回0,若出错返回-1。
  • 第一个参数epfd是epoll_create()的返回值;
  • 第二个参数op用来指定需要执行的操作,它可以是如下几种值:
    EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误;
    EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误;
    EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图移除一个不在epfd的兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除;
  • 第三个参数fd指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;
  • 第四个参数ev是指向结构体epoll_event的指针,结构体的定义如下:
  1. 事件等待:epoll_wait()
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

4.使用epoll()多路复用实现的服务器端示例代码

 1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <errno.h>
  6 #include <ctype.h>
  7 #include <time.h>
  8 #include <pthread.h>
  9 #include <getopt.h>
 10 #include <libgen.h>
 11 #include <sys/types.h>
 12 #include <sys/socket.h>
 13 #include <arpa/inet.h>
 14 #include <netinet/in.h>
 15 #include <sys/resource.h>
 16 #include <sys/epoll.h>
 17 
 18 #define MAX_EVENTS       512
 19 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))    //整个数组的长度除以第一个元素的长度等于元素的个数
 20 
 21 //static inline void msleep(unsigned long ms);
 22 static inline void print_usage(char *progname);
 23 int socket_server_init(char *listen_ip, int listen_port);
 24 void set_socket_rlimit(void);
 25 
 26 int main(int argc, char **argv)
 27 {
 28         int                          listenfd, connfd;
 29         int                          serv_port = 0;
 30         int                          daemon_run = 0;
 31         char                        *progname = NULL;
 32         int                          opt;
 33         //      fd_set                       rdset;
 34         int                          rv;
 35         int                          i, j;
 36         int                          found;
 37         //      int                          maxfd=0;
 38         //      int                          max;
 39         char                         buf[1024];
 40         int                          epollfd;
 41         struct epoll_event           event;
 42         struct epoll_event           event_array[MAX_EVENTS];
 43         int                          events;
 44 
 45         //参数解析
 46         struct option     long_options[] =
 47         {
 48                 {"daemon", no_argument, NULL, 'b'},
 49                 {"port", required_argument, NULL, 'p'},
 50                 {"help", no_argument, NULL, 'h'},
 51                 {NULL, 0, NULL, 0}
 52         };
 53 
 54         //basename的作用就是找出./a.out中./后面的内容
 55         progname = basename(argv[0]);
 56         //解析命令行参数
 57         while ((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1)
 58         {
 59                 switch (opt)
 60                 {
 61                         case 'b':
 62                                 daemon_run=1;
 63                                 break;
 64                         case 'p':
 65                                 serv_port = atoi(optarg);
 66                                 break;
 67                         case 'h': //得到帮助信息
 68                                 print_usage(progname);
 69                                 return EXIT_SUCCESS;
 70                         default:
 71                                 break;
 72                 }
 73         }
 74         //判断是否设置端口
 75         if( !serv_port )
 76         {
 77                 print_usage(progname);
 78                 return -1;
 79         }
 80 
 81         set_socket_rlimit();//
 82 
 83         //监听,socket_server_init这个函数还需要加强学习
 84         if( (listenfd=socket_server_init(NULL, serv_port)) < 0 )
 85         {
 86                 printf("ERROR: %s server listen on port %d failure\n", argv[0],serv_port);
 87                 return -2;
 88         }
 89         printf("%s server start to listen on port %d\n", argv[0],serv_port);
 90 
 91         //设置程序在后台运行
 92         if( daemon_run )
 93         {
 94                 daemon(0, 0);
 95         }
 96 
 97         if((epollfd=epoll_create(MAX_EVENTS)) < 0)
 98         {
 99                 printf("epoll_create() failure:%s\n",strerror(errno));
100                 return -3;
101         }
102 
103         //
104         event.events=EPOLLIN;
105         event.data.fd=listenfd;
106 
107         if(epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event)<0)
108         {
109                 printf("epoll and listen socket failure:%s\n",strerror(errno));
110                 return -4;
111         }
112 
113         for( ; ; )
114         {
115                 //程序会阻塞在这里;epoll函数重点
116                 events=epoll_wait(epollfd,event_array,MAX_EVENTS,-1);
117                 if(events<0)
118                 {
119                         printf("epoll failure: %s\n", strerror(errno));
120                         break;
121                 }
122                 else if(events == 0)//等于即为select超时
123                 {
124                         printf("epoll get timeout\n");
125                         continue;
126                 }
127 
128                 //
129                 for(i=0;i<events;i++)
130                 {
131                         if( (event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP) )
132                         {
133                                 printf("epoll_wait get error on fd[%d]:%s\n",event_array[i].data.fd,strerror(errno));
134                                 epoll_ctl(epollfd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
135                                 close(event_array[i].data.fd);
136                         }
137 
138                  //
139                         if(event_array[i].data.fd==listenfd)
140                         {
141                                 if( (connfd=accept(listenfd,(struct sockaddr *)NULL,NULL))<0)
142                                 {
143                                         printf("accept new client failure:%s\n",strerror(errno));
144                                         continue;
145                                 }
146 
147                                 event.data.fd=connfd;
148                                 //event.events=EPOLLIN|EPOLLET;
149                                 event.events=EPOLLIN;
150                                 if(epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&event)<0)
151                                 {
152                                         printf("epoll add client socket failure: %s\n", strerror(errno));
153                                         close(event_array[i].data.fd);
154                                         continue;
155                                 }
156                                 printf("epoll add new client socket[%d] ok.\n", connfd);
157                         }
158                         else /* already connected client socket get data incoming */
159                         {
160                                 if( (rv=read(event_array[i].data.fd, buf, sizeof(buf))) <= 0)
161                                 {
162                                         printf("socket[%d] read failure or get disconncet and will be removed.\n",
163                                                         event_array[i].data.fd);
164                                         epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
165                                         close(event_array[i].data.fd);
166                                         continue;
167                                 }
168                                 else
169                                 {
170                                         printf("socket[%d] read get %d bytes data\n", event_array[i].data.fd, rv);
171                                         /* convert letter from lowercase to uppercase */
172                                         for(j=0; j<rv; j++)
173                                                 buf[j]=toupper(buf[j]);
174                                         if( write(event_array[i].data.fd, buf, rv) < 0 )
175                                         {
176                                                 printf("socket[%d] write failure: %s\n", event_array[i].data.fd, strerror(errno));
177                                                 epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
178                                                 close(event_array[i].data.fd);
179                                         }
180                                 }
181                         }
182                 } /* for(i=0; i<rv; i++) */
183         } /* while(1) */
184 
185 CleanUp:
186         close(listenfd);
187         return 0;
188 }
189 
190 static inline void print_usage(char *progname)
191 {
192         printf("Usage: %s [OPTION]...\n", progname);
193 
194         printf(" %s is a socket server program, which used to verify client and echo back string from it\n",
195                         progname);
196         printf("\nMandatory arguments to long options are mandatory for short options too:\n");
197 
198         printf(" -b[daemon ] set program running on background\n");
199         printf(" -p[port ] Socket server port address\n");
200         printf(" -h[help ] Display this help information\n");
201 
202         printf("\nExample: %s -b -p 8873\n", progname);
203         return ;
204 }
205 int socket_server_init(char *listen_ip, int listen_port)
206 {
207         struct               sockaddr_in servaddr;
208         int                  rv = 0;
209         int                  on = 1;
210         int                  listenfd;
211 
212         if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
213         {
214                 printf("Use socket() to create a TCP socket failure: %s\n", strerror(errno));
215                 return -1;
216         }
217         //设置 socket 端口可重用,修复 socket 服务器重新启动时“地址已经在使用”的 bug
218         setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
219         memset(&servaddr, 0, sizeof(servaddr));
220         servaddr.sin_family = AF_INET;
221         servaddr.sin_port = htons(listen_port);
222 
223         //监听所有公众ip的地址
224         if( !listen_ip )//未传过来ip
225         {
226                 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //将其ip地址设为INADDR_ANY
227         }
228         else //监听指定ip的地址
229         {
230                 if (inet_pton(AF_INET, listen_ip, &servaddr.sin_addr) <= 0)//将其ip转入四字节的sin.addr中去
231                 {
232                         printf("inet_pton() set listen IP address failure.\n");
233                         rv = -2;
234                         goto CleanUp;
235                 }
236         }
237         if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
238         {
239                 printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
240                 rv = -3;
241                 goto CleanUp;
242         }
243         if(listen(listenfd, 64) < 0)
244         {
245                 printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
246                 rv = -4;
247                 goto CleanUp;
248         }
249 CleanUp:
250         if(rv<0)
251                 close(listenfd);
252         else
253                 rv = listenfd;
254         return rv;
255 }
256 
257 /* Set open file description count to max */
258 void set_socket_rlimit(void)
259 {
260         struct rlimit limit = {0};
261 
262         getrlimit(RLIMIT_NOFILE, &limit );
263         limit.rlim_cur = limit.rlim_max;
264         setrlimit(RLIMIT_NOFILE, &limit );
265 
266         printf("set socket open fd max count to %ld\n", limit.rlim_max);
267 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值