• IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力


    上篇线程/进程并发服务器中提到,提高服务器性能在IO层需要关注两个地方,一个是文件描述符处理,一个是线程调度。

    IO复用是什么?IO即Input/Output,在网络编程中,文件描述符就是一种IO操作。

    为什么要IO复用?

    1.网络编程中非常多函数是阻塞的,如connect,利用IO复用可以以非阻塞形式执行代码。

    2.之前提到listen维护两个队列,完成握手的队列可能有多个就绪的描述符,IO复用可以批处理描述符。

    3.有时候可能要同时处理TCP和UDP,同时监听多个端口,同时处理读写和连接等。

    为什么epoll效率要比select高?

    1.在连接数量较大的场景,select遍历需要每个描述符,epoll由内核维护事件表,只需要处理有响应的描述符。

    2.select本身处理文件描述符受到限制,默认1024。

    3.效率并不是绝对的,当连接率高,断开和连接频繁时,select不一定比epoll差。所以要根据具体场合使用。

    epoll的两种模式,电平触发和边沿触发。

    1.电平触发效率较边沿触发低,电平触发模式下,当epoll_wait返回的事件没有全部相应处理完毕,内核缓冲区还存在数据时,会反复通知,直到处理完成。epoll默认使用这种模式。

    2.边沿触发效率较高,内核缓冲区事件只通知一次。

    一个epoll实现demo

      1 #include <iostream>
      2 #include <sys/socket.h>
      3 #include <sys/epoll.h>
      4 #include <netinet/in.h>
      5 #include <arpa/inet.h>
      6 #include <fcntl.h>
      7 #include <unistd.h>
      8 #include <stdio.h>  
      9 #include <stdlib.h>  
     10 #include <string.h>  
     11 #include <errno.h>
     12 
     13 using namespace std;
     14 
     15 #define MAXLINE 5
     16 #define OPEN_MAX 100
     17 #define LISTENQ 20
     18 #define SERV_PORT 5000
     19 #define INFTIM 1000
     20 
     21 int main(int argc, char* argv[])
     22 {
     23     int listen_fd, connfd_fd, socket_fd, epfd, nfds;
     24     ssize_t n;
     25     char line[MAXLINE];
     26     socklen_t clilen;
     27 
     28     //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
     29     struct epoll_event ev,events[20];
     30     //生成用于处理accept的epoll专用的文件描述符
     31     epfd=epoll_create(5);
     32     struct sockaddr_in clientaddr;
     33     struct sockaddr_in serveraddr;
     34     listen_fd = socket(AF_INET, SOCK_STREAM, 0);
     35     //设置与要处理的事件相关的文件描述符
     36     ev.data.fd = listen_fd;
     37     //设置要处理的事件类型
     38     ev.events=EPOLLIN|EPOLLET;
     39     //注册epoll事件
     40     epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&ev);
     41     
     42     memset(&serveraddr, 0, sizeof(serveraddr));  
     43     serveraddr.sin_family = AF_INET;  
     44     serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
     45     serveraddr.sin_port = htons(SERV_PORT);
     46     
     47     if (bind(listen_fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1)
     48     {  
     49         printf("bind socket error: %s(errno: %d)
    ",strerror(errno),errno);  
     50         exit(0);  
     51     }  
     52     
     53     if (listen(listen_fd, LISTENQ) == -1)
     54     {  
     55         exit(0);  
     56     }   
     57     
     58     for ( ; ; ) 
     59     {
     60         //等待epoll事件的发生
     61         nfds = epoll_wait(epfd,events,20,500);
     62         //处理所发生的所有事件
     63         for (int i = 0; i < nfds; ++i)
     64         {
     65             if (events[i].data.fd == listen_fd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
     66 
     67             {
     68                 connfd_fd = accept(listen_fd,(sockaddr *)&clientaddr, &clilen);
     69                 if (connfd_fd < 0){
     70                     perror("connfd_fd < 0");
     71                     exit(1);
     72                 }
     73                 char *str = inet_ntoa(clientaddr.sin_addr);
     74                 cout << "accapt a connection from " << str << endl;
     75                 //设置用于读操作的文件描述符
     76                 ev.data.fd = connfd_fd;
     77                 //设置用于注测的读操作事件
     78                 ev.events = EPOLLIN|EPOLLET;
     79                 //注册ev
     80                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd_fd,&ev);
     81             }
     82             else if (events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
     83             {            
     84                 memset(&line,'', sizeof(line));  
     85                 if ( (socket_fd = events[i].data.fd) < 0)
     86                     continue;
     87                 if ( (n = read(socket_fd, line, MAXLINE)) < 0) {
     88                     if (errno == ECONNRESET) {
     89                         close(socket_fd);
     90                         events[i].data.fd = -1;
     91                     } else
     92                         std::cout<<"readline error"<<std::endl;
     93                 } else if (n == 0) {
     94                     close(socket_fd);
     95                     events[i].data.fd = -1;
     96                 }                
     97                 cout << line << endl;
     98                 //设置用于写操作的文件描述符
     99                 ev.data.fd = socket_fd;
    100                 //设置用于注测的写操作事件
    101                 ev.events = EPOLLOUT|EPOLLET;
    102                 //修改socket_fd上要处理的事件为EPOLLOUT
    103                 //epoll_ctl(epfd,EPOLL_CTL_MOD,socket_fd,&ev);
    104             }
    105             else if (events[i].events&EPOLLOUT) // 如果有数据发送
    106             {
    107                 socket_fd = events[i].data.fd;
    108                 write(socket_fd, line, n);
    109                 //设置用于读操作的文件描述符
    110                 ev.data.fd = socket_fd;
    111                 //设置用于注测的读操作事件
    112                 ev.events = EPOLLIN|EPOLLET;
    113                 //修改socket_fd上要处理的事件为EPOLIN
    114                 epoll_ctl(epfd,EPOLL_CTL_MOD,socket_fd,&ev);
    115             }
    116         }
    117     }
    118     return 0;
    119 }

    执行效果如下:

    QQ图片20160512223244

    第一次学epoll时,容易错误的认为epoll也可以实现并发,其实正确的话是epoll可以实现高性能并发服务器,epoll只是提供了IO复用,在IO“并发”,真正的并发只能通过线程进程实现。

    那为什么可以同时连接两个客户端呢?实际上这两个客户端都是在一个进程上运行的,前面提到过各个描述符之间是相互不影响的,所以是一个进程轮循在处理多个描述符。

    Reactor模式:

    Reactor模式实现非常简单,使用同步IO模型,即业务线程处理数据需要主动等待或询问,主要特点是利用epoll监听listen描述符是否有相应,及时将客户连接信息放于一个队列,epoll和队列都是在主进程/线程中,由子进程/线程来接管各个描述符,对描述符进行下一步操作,包括connect和数据读写。主程读写就绪事件。

    大致流程图如下:

    image

    Preactor模式:

    Preactor模式完全将IO处理和业务分离,使用异步IO模型,即内核完成数据处理后主动通知给应用处理,主进程/线程不仅要完成listen任务,还需要完成内核数据缓冲区的映射,直接将数据buff传递给业务线程,业务线程只需要处理业务逻辑即可。

    大致流程如下:

    image

  • 相关阅读:
    OC
    提取AppDelegate.m中的"RDVTabBarController"第三方框架的方法
    spring_aop
    spring_xml配置&依赖注入
    关于idea运行web项目时出现的浏览器问题
    Java中main方法参数类型个人粗略理解
    函数式编程_lambda
    反射_注解
    pl/sql使用小技巧
    触发器&索引&视图
  • 原文地址:https://www.cnblogs.com/binchen-china/p/5487795.html
Copyright © 2020-2023  润新知