IO多路复用之select/epoll模型

概述

  • 大部分程序使用的I/O模型(传统的阻塞式I/O模型)都是单个进程每次只在一个文件描述符上执行I/O操作,每次I/O系统调用都会阻塞直到完成数据传输。
  • 但是,有些场景需要以非阻塞的方式检查文件描述符上是否可进行I/O操作。 同时检查多个文件描述符,看它们中的任何一个是否可以执行I/O操作。对应的解决方法是使用I/O多路复用技术。
  • I/O多路复用的目标:就是同时检查多个文件描述符的状态,查看I/O系统调用是否可以非阻塞地执行。文件描述符就绪状态的转化是通过一些I/O事件来触发的,而同时检查多个文件描述符的操作,不会执行实际的I/O操作,它只是告诉进程某个文件描述符已经处于就绪状态了,需要调用其他的系统调用来完成实际的I/O操作。

select接口

  • int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout);
    • 函数作用:通过select实现多路复用
    • 参数
      • nfds:委托内核检测的这三个集合中最大的文件描述符 + 1
        • 内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
        • 在Window中这个参数是无效的,指定为-1即可
      • readfds:文件描述符的集合, 内核只检测这个集合中文件描述符对应的读缓冲区
        • 传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据
      • writefds:文件描述符的集合, 内核只检测这个集合中文件描述符对应的写缓冲区
        • 传入传出参数,如果不需要使用这个参数可以指定为NULL
      • exceptfds:文件描述符的集合, 内核检测集合中文件描述符是否有异常状态
        • 传入传出参数,如果不需要使用这个参数可以指定为NULL
      • timeout:超时时长,用来强制解除select()函数的阻塞的
        •   struct timeval {
            	    time_t      tv_sec;         /* seconds */
            	    suseconds_t tv_usec;        /* microseconds */
            };
          
        • NULL:函数检测不到就绪的文件描述符会一直阻塞。
        • 等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回0
        • 不等待:函数不会阻塞,直接将该参数对应的结构体初始化为0即可。
  • void FD_CLR(int fd, fd_set *set);
    • 函数作用:将文件描述符fd从set集合中删除(将fd对应的标志位设置为0)
  • int FD_ISSET(int fd, fd_set *set);
    • 函数作用 :判断文件描述符fd是否在set集合中(读一下fd对应的标志位到底是0还是1)
  • void FD_SET(int fd, fd_set *set);
    • 函数作用 : 将文件描述符fd添加到set集合中(将fd对应的标志位设置为1)
  • void FD_ZERO(fd_set *set);
    • 函数作用:将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符

epoll接口

  • int epoll_create(int size);

    • 函数作用 : 创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
    • 参数
      • size : 创建文件描述符的个数,在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数即可。
    • 返回值
      • 失败:返回 -1
      • 成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll实例了
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    • 函数作用 : 管理红黑树实例上的节点,可以进行添加、删除、修改操作。
    • 参数
      • epfd:文件描述符,为epoll_create返回的参数
      • op : 枚举值,枚举值,控制通过该函数执行什么操作
        • EPOLL_CTL_ADD:往epoll模型中添加新的节点
        • EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
        • EPOLL_CTL_DEL:删除epoll模型中的指定的节点
      • fd :文件描述符,即要添加/修改/删除的文件描述符
      • event : epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
        •   // 联合体, 多个变量共用同一块内存        
            typedef union epoll_data {
             	void        *ptr;
            	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
            	uint32_t     u32;
            	uint64_t     u64;
            } epoll_data_t;
            
            struct epoll_event {
            	uint32_t     events;      /* Epoll events */
            	epoll_data_t data;        /* User data variable */
            };
          
        • events:委托epoll检测的事件
          • EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
          • EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪
          • EPOLLERR:异常事件
        • data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出
    • 返回值
      • 失败:返回-1
      • 成功:返回0
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    • 函数作用 : 检测创建的epoll实例中有没有就绪的文件描述符
    • 函数参数
      • epfd:epoll_create() 函数的返回值, 通过这个参数找到epoll实例
      • events:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息
      • maxevents:修饰第二个参数, 结构体数组的容量
      • timeout:如果检测的epoll实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位毫秒
        • 0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
        • 大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
        • -1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞
    • 函数返回值
      • 成功:
        • 等于0:函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符
        • 大于0:检测到的已就绪的文件描述符的总个数
      • 失败:返回-1

epoll工作模式

  • epoll有两种工作模式:水平模式和边沿模式
  • 水平模式(Level Triggered) :
    • 默认工作模式。
    • 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理,下次调用epoll_wait时会再次响应应用程序并通知此事件。
    • 比如客户端发送了1000个字节的数据,服务端只读取到500字节,下次调用epoll_wait时,读事件会继续触发,我们就可以继续读取剩余数据。
  • 边沿模式(Edge-Triggered) :
    • 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_event时,不会通知此事件。所以此模式下,需循环接收数据,直到接收完所有数据。
    • 比如客户端发送了1000个字节的数据,服务端只读取到500字节,下次调用epoll_wait时,读事件不会被触发。
  • 边沿触发模式很大程度上减少了epoll事件被触发的次数,效率比水平触发模式高。边沿触发模式,必须使用非阻塞接口。

select代码

  •   #include 
      #include 
      #include 
      #include 
      #include 
      
      int main()
      {
          // 1. 创建监听的fd
          int lfd = socket(AF_INET, SOCK_STREAM, 0);
      
          // 2. 绑定
          struct sockaddr_in addr;
          addr.sin_family = AF_INET;
          addr.sin_port = htons(9999);
          addr.sin_addr.s_addr = INADDR_ANY;
          bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
      
          // 3. 设置监听
          listen(lfd, 128);
      
          // 将监听的fd的状态检测委托给内核检测
          int maxfd = lfd;
          // 初始化检测的读集合
          fd_set rdset;
          fd_set rdtemp;
          // 清零
          FD_ZERO(&rdset);
          // 将监听的lfd设置到检测的读集合中
          FD_SET(lfd, &rdset);
          // 通过select委托内核检测读集合中的文件描述符状态, 检测read缓冲区有没有数据
          // 如果有数据, select解除阻塞返回
          // 应该让内核持续检测
          while(1)
          {
              // 默认阻塞
              // rdset 中是委托内核检测的所有的文件描述符
              rdtemp = rdset;
              int num = select(maxfd+1, &rdtemp, NULL, NULL, NULL);
              // rdset中的数据被内核改写了, 只保留了发生变化的文件描述的标志位上的1, 没变化的改为0
              // 只要rdset中的fd对应的标志位为1 -> 缓冲区有数据了
              // 判断
              // 有没有新连接
              if(FD_ISSET(lfd, &rdtemp))
              {
                  // 接受连接请求, 这个调用不阻塞
                  struct sockaddr_in cliaddr;
                  int cliLen = sizeof(cliaddr);
                  int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &cliLen);
      
                  // 得到了有效的文件描述符
                  // 通信的文件描述符添加到读集合
                  // 在下一轮select检测的时候, 就能得到缓冲区的状态
                  FD_SET(cfd, &rdset);
                  // 重置最大的文件描述符
                  maxfd = cfd > maxfd ? cfd : maxfd;
              }
      
              // 没有新连接, 通信
              for(int i=0; i<maxfd+1; ++i)
              {
      			// 判断从监听的文件描述符之后到maxfd这个范围内的文件描述符是否读缓冲区有数据
                  if(i != lfd && FD_ISSET(i, &rdtemp))
                  {
                      // 接收数据
                      char buf[10] = {0};
                      // 一次只能接收10个字节, 客户端一次发送100个字节
                      // 一次是接收不完的, 文件描述符对应的读缓冲区中还有数据
                      // 下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次
                      // 	循环会一直持续, 知道缓冲区数据被读完位置
                      int len = read(i, buf, sizeof(buf));
                      if(len == 0)
                      {
                          printf("客户端关闭了连接...\n");
                          // 将检测的文件描述符从读集合中删除
                          FD_CLR(i, &rdset);
                          close(i);
                      }
                      else if(len > 0)
                      {
                          // 收到了数据
                          // 发送数据
                          write(i, buf, strlen(buf)+1);
                      }
                      else
                      {
                          // 异常
                          perror("read");
                      }
                  }
              }
          }
      
          return 0;
      }
    

epoll代码

水平触发模式

  •   #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      
      // server
      int main(int argc, const char* argv[])
      {
      		
          // 创建监听的套接字
          int lfd = socket(AF_INET, SOCK_STREAM, 0);
          if(lfd == -1)
          {
              perror("socket error");
              exit(1);
          }
      
          // 绑定
          struct sockaddr_in serv_addr;
          memset(&serv_addr, 0, sizeof(serv_addr));
          serv_addr.sin_family = AF_INET;
          serv_addr.sin_port = htons(9999);
          serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本地多有的IP
          
          // 设置端口复用
          int opt = 1;
          setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      
          // 绑定端口
          int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
          if(ret == -1)
          {
              perror("bind error");
              exit(1);
          }
      
          // 监听
          ret = listen(lfd, 64);
          if(ret == -1)
          {
              perror("listen error");
              exit(1);
          }
      
          // 现在只有监听的文件描述符
          // 所有的文件描述符对应读写缓冲区状态都是委托内核进行检测的epoll
          // 创建一个epoll模型
          int epfd = epoll_create(100);
          if(epfd == -1)
          {
              perror("epoll_create");
              exit(0);
          }
      
          // 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
          struct epoll_event ev;
          ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
          ev.data.fd = lfd;
          ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
          if(ret == -1)
          {
              perror("epoll_ctl");
              exit(0);
          }
      
          struct epoll_event evs[1024];
          int size = sizeof(evs) / sizeof(struct epoll_event);
          // 持续检测
          while(1)
          {
              // 调用一次, 检测一次
              int num = epoll_wait(epfd, evs, size, -1);
              for(int i=0; i<num; ++i)
              {
                  // 取出当前的文件描述符
                  int curfd = evs[i].data.fd;
                  // 判断这个文件描述符是不是用于监听的
                  if(curfd == lfd)
                  {
                      // 建立新的连接
                      int cfd = accept(curfd, NULL, NULL);
                      // 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
                      ev.events = EPOLLIN;    // 读缓冲区是否有数据
                      ev.data.fd = cfd;
                      ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                      if(ret == -1)
                      {
                          perror("epoll_ctl-accept");
                          exit(0);
                      }
                  }
                  else
                  {
                      // 处理通信的文件描述符
                      // 接收数据
                      char buf[5] = {0};
                      int len = recv(curfd, buf, sizeof(buf), 0);
                      if(len == 0)
                      {
                          printf("客户端已经断开了连接\n");
                          // 将这个文件描述符从epoll模型中删除
                          epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                          close(curfd);
                      }
                      else if(len > 0)
                      {
                          printf("客户端say: %s\n", buf);
                          send(curfd, buf, len, 0);
                      }
                      else
                      {
                          perror("recv");
                          exit(0);
                      } 
                  }
              }
          }
      
          return 0;
      }
    

边沿触发模式

  •   #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      
      // server
      int main(int argc, const char* argv[])
      {
          // 创建监听的套接字
          int lfd = socket(AF_INET, SOCK_STREAM, 0);
          if(lfd == -1)
          {
              perror("socket error");
              exit(1);
          }
      
          // 绑定
          struct sockaddr_in serv_addr;
          memset(&serv_addr, 0, sizeof(serv_addr));
          serv_addr.sin_family = AF_INET;
          serv_addr.sin_port = htons(9999);
          serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本地多有的IP
          // 127.0.0.1
          // inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
          
          // 设置端口复用
          int opt = 1;
          setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      
          // 绑定端口
          int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
          if(ret == -1)
          {
              perror("bind error");
              exit(1);
          }
      
          // 监听
          ret = listen(lfd, 64);
          if(ret == -1)
          {
              perror("listen error");
              exit(1);
          }
      
          // 现在只有监听的文件描述符
          // 所有的文件描述符对应读写缓冲区状态都是委托内核进行检测的epoll
          // 创建一个epoll模型
          int epfd = epoll_create(100);
          if(epfd == -1)
          {
              perror("epoll_create");
              exit(0);
          }
      
          // 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
          struct epoll_event ev;
          ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
          ev.data.fd = lfd;
          ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
          if(ret == -1)
          {
              perror("epoll_ctl");
              exit(0);
          }
      
      
          struct epoll_event evs[1024];
          int size = sizeof(evs) / sizeof(struct epoll_event);
          // 持续检测
          while(1)
          {
              // 调用一次, 检测一次
              int num = epoll_wait(epfd, evs, size, -1);
              printf("==== num: %d\n", num);
      
              for(int i=0; i<num; ++i)
              {
                  // 取出当前的文件描述符
                  int curfd = evs[i].data.fd;
                  // 判断这个文件描述符是不是用于监听的
                  if(curfd == lfd)
                  {
                      // 建立新的连接
                      int cfd = accept(curfd, NULL, NULL);
                      // 将文件描述符设置为非阻塞
                      // 得到文件描述符的属性
                      int flag = fcntl(cfd, F_GETFL);
                      flag |= O_NONBLOCK;
                      fcntl(cfd, F_SETFL, flag);
                      // 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
                      // 通信的文件描述符检测读缓冲区数据的时候设置为边沿模式
                      ev.events = EPOLLIN | EPOLLET;    // 读缓冲区是否有数据
                      ev.data.fd = cfd;
                      ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                      if(ret == -1)
                      {
                          perror("epoll_ctl-accept");
                          exit(0);
                      }
                  }
                  else
                  {
                      // 处理通信的文件描述符
                      // 接收数据
                      char buf[5] = {0};
                      // 循环读数据
                      while(1)
                      {
                          int len = recv(curfd, buf, sizeof(buf), 0);
                          if(len == 0)
                          {
                              // 非阻塞模式下和阻塞模式是一样的 => 判断对方是否断开连接
                              printf("客户端断开了连接...\n");
                              // 将这个文件描述符从epoll模型中删除
                              epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                              close(curfd);
                              break;
                          }
                          else if(len > 0)
                          {
                              // 通信
                              // 接收的数据打印到终端
                              write(STDOUT_FILENO, buf, len);
                              // 发送数据
                              send(curfd, buf, len, 0);
                          }
                          else
                          {
                              // len == -1
                              if(errno == EAGAIN)
                              {
                                  printf("数据读完了...\n");
                                  break;
                              }
                              else
                              {
                                  perror("recv");
                                  exit(0);
                              }
                          }
                      }
                  }
              }
          }
      
          return 0;
      }
    

参考

  • IO多路转接(复用)之epoll
  • IO多路转接(复用)之select

你可能感兴趣的:(网络协议,服务器,tcp/ip,网络协议)