【网络】高级IO——poll版本TCP服务器

目录

前言

一,poll函数

 1.1.参数一:fds

1.2.参数二,nfds

1.3.参数三,timeout

1.4.返回值

1.5.poll函数简单使用示例

1.6.select和poll的区别

二,poll版TCP服务器编写

2.1.编写

2.2.poll的优缺点

2.3.源代码


前言

由于select函数有下面几个特别明显的缺点,就推演出了改进版本——poll函数

  • 比如select监视的fd是有上限的,我的云服务器内核版本下最大上限是1024个fd,主要还是因为fd_set他是一个固定大小的位图结构,位图中的数组开辟之后不会在变化了,这是内核的数据结构,除非你修改内核参数,否则不会在变化了,所以一旦select监视的fd数量超过1024,则select会报错。
  • 除此之外,select大部分的参数都是输入输出型参数,用户和内核都会不断的修改这些参数的值,导致每次调用select前,都需要重新设置fd_set位图中的内容,这在用户层面上会带来很多不必要的遍历+拷贝的成本。

        poll接口主要解决了select接口的两个问题,一个是select监视的fd有上限,另一个是select每次调用前都需要借助第三方数组,向fd_set里面重新设置关心的fd。 

        select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。

只要我们理解了我们的select,poll也就不在话下

        poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。


 

一,poll函数

这个函数的功能和select一模一样。都是监视并等待多个文件描述符的属性变化

poll函数处理的还是IO过程里面的等待过程!!!

 1.1.参数一:fds

select()使用了基于文件描述符的三位掩码的解决方案,其效率不高;和它不同,poll() 使用了由 nfds 个 pollfd 结构体构成的数组,fds 指针指向该数组。

  • 每个 pollfd 结构体指定一个被监视的文件描述符。
  • 可以给 poll()传递多个 pollfd 结构体,使它能够监视多个文件描述符。
  • 每个结构体的events变量是要监视的文件描述符的事件的位掩码。用户可以设置该变量。r
  • events变量是该文件描述符的结果事 件的位掩码。内核在返回时会设置 revents变量。
  • events 变量中请求的所有事件都 可能在 revents 变量中返回。

polld 结构体定义如下:

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

              poll 函数是 Unix/Linux 系统中用于监视多个文件描述符(file descriptors)状态变化的一种机制。poll 函数能够同时监视多个文件描述符,以检查其上是否发生了感兴趣的事件(如可读、可写、错误等)。为了实现这一功能,poll 函数使用了一个 pollfd 结构体数组作为输入参数,每个 pollfd 结构体代表了一个被监视的文件描述符及其相关的事件。

    下面是对 pollfd 结构体各成员的详细解释:


    int fd;这个成员变量是一个整数,表示被监视的文件描述符。文件描述符是一个非负整数,它是一个索引值,指向内核中打开文件的表项。通过文件描述符,我们可以对打开的文件进行读写操作。在 poll 函数的上下文中,这个文件描述符可以是任何类型的文件、套接字(socket)或者管道(pipe)等。


    short events;:这个成员变量是一个位掩码(bitmask),用于指定我们想要监视的、在该文件描述符上可能发生的事件。

    这些事件可以是以下几种之一(或它们的组合,通过位或操作符 | 实现):

    以下是合法的 events值:

    • POLLIN

    有数据可读。

    • POLLRDNORM

    有普通数据可读。

    • POLLRDBAND

    有优先数据可读。

    • POLLPRI

    有高优先级数据可读。

    • POLLOUT

    写操作不会阻塞。

    • POLLWRNORM

    写普通数据不会阻塞。

    • POLLBAND

    写优先数据不会阻塞。

    • POLLMSG

    有SIGPOLL消息可用。


    short revents;这个成员变量在 poll 函数调用返回时被系统自动填充,它也是一个位掩码,表示在调用 poll 期间,实际发生在文件描述符上的事件。

    与 events 不同,revents 是由 poll 函数自动填写的,用户不需要(也不应该)在调用 poll 之前设置它。

    revents 的值可能包含 events 中指定的任何事件,或者由于某种原因(如错误或挂起)而包含其他事件。

    revents 的值可能会包含上面那些events的值里面的任意一个/些。也就是说

    下面这些合法的 events值中,revents也会可能会包含

    • POLLIN

    有数据可读。

    • POLLRDNORM

    有普通数据可读。

    • POLLRDBAND

    有优先数据可读。

    • POLLPRI

    有高优先级数据可读。

    • POLLOUT

    写操作不会阻塞。

    • POLLWRNORM

    写普通数据不会阻塞。

    • POLLBAND

    写优先数据不会阻塞。

    • POLLMSG

    有SIGPOLL消息可用。

    除了上面那些events可能返回的事件,revents变量还可能会返回如下事件:

    • POLLER

    给定的文件描述符出现错误。

    • POLLHUP

    给定的文件描述符有挂起事件。

    • POLLNVAL

    给定的文件描述符非法。

    对于events变量,传递这些revents特有的事件没有意义,events参数不要传递这些变量,它们会在revents变量中返回。


    poll()和select()不同,不需要显式请求异常报告。

    POLLIN | POLLPRI 等价于select()的读事件,而POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT等价于POLLWRNORM。

    举个例子,要监视某个文件描述符是否可读写,需要把events设置成POLLIN | POLLOUT。

    返回时,会检查revents中是否有相应的标志位。

    如果设置了POLLIN,文件描述符可非阻塞读;

    如果设置了POLLOUT,文件描述符可非阻塞写。

    标志位并不是相互排斥的:可以同时设置,表示可以在该文件描述符上读写,而且都不会阻塞。

      相比于select,poll将输入和输出事件进行了分离 

    比如我们希望内核帮我们关注 0号文件描述符上的读事件

    struct pollfd rfds;    
    rfds.fd = 0;              //希望内核帮我们关注0号文件描述符上的事件
    rfds.events |= POLLIN;    //希望内核关注0号文件描述符上的读事件
    //rfds.events = POLLIN | POLLOUT;    //希望既监听读事件又监听写事件
    rfds.revents = 0;         //这个参数用于内核通知我们有事件就绪了,让我们赶紧来取

    如果我们要判断读事件是否就绪

    if(rfds.revents & POLLIN){
        std::cout << "读事件就绪了..." << std::endl;
    }

      events和revents的取值都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。 

      • 在调用poll函数之前,可以通过“或”运算符将要检测的事件添加到events成员当中。
      • 在poll函数返回后,可以通过“与”运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。

      注意:每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件 

      • 使用 poll 函数时,你通常会创建一个 pollfd 结构体数组,每个元素代表一个你想要监视的文件描述符及其事件。
      • 然后,你调用 poll 函数,并将这个数组和数组的大小作为参数传递给它。
      • poll 函数会阻塞(或根据需要立即返回),直到一个或多个文件描述符上发生了请求的事件,或者超过了指定的超时时间。
      • 最后,你可以通过检查每个 pollfd 结构体的 revents 成员来确定哪些文件描述符上发生了哪些事件。

      1.2.参数二,nfds

      nfds_t类型是什么?

              nfds_t 类型是在 Unix/Linux 系统编程中,特别是在使用 poll、ppoll、select 等系统调用时,用来表示文件描述符数量的数据类型。这个类型的确切定义可能会根据不同的系统和库实现而有所不同,但通常它是一个足够大的整数类型,以容纳系统可能支持的最大文件描述符数量。

             在大多数现代 Unix-like 系统中,nfds_t 通常是 unsigned int 或 unsigned long 的别名,但这不是一个固定的规则,因此最好查看你的系统或库的文档以获取确切的定义。

      例如,在 glibc(GNU C Library)中,nfds_t 的定义可能看起来像这样(尽管这取决于 glibc 的版本和配置):

       typedef unsigned long nfds_t; 

      或者在某些情况下,它可能是:

       typedef unsigned int nfds_t; 

      当你使用 poll 函数时,你需要将一个 nfds_t 类型的值作为第一个参数传递给函数,这个值表示 pollfd 结构体数组中的元素数量。这个值应该大于或等于数组中实际元素的数量,因为 poll 会检查这个范围内的所有 pollfd 结构体。

       #include <poll.h>  
         int main() {  
       struct pollfd fds[2];  
       // 初始化 fds 数组...  
         nfds_t nfds = sizeof(fds) / sizeof(fds[0]);  
       int timeout = -1; // 无限等待  
         int ret = poll(fds, nfds, timeout);  
       // 处理 poll 的返回值...  
         return 0;  
       } 

      在这个例子中,nfds 被设置为 fds 数组的大小,这是调用 poll 时应该传递的正确值。注意,虽然在这个例子中 nfds 被显式地计算为数组的大小,但在许多情况下,你可能已经知道要监视的文件描述符数量,因此可以直接使用那个值。

      1.3.参数三,timeout

      这个参数和select差不多啊!只不过,这个不是结构体了啊!!

      timeout参数指定等待的时间长度,单位是毫秒,不论是否有I/O就绪,poll()调用都会返回。

      这个参数是一个输入型参数,单位是毫秒,代表阻塞等待的时间,超过该时间就会变为非阻塞等待。

      1. 如果timeout值为负数,表示永远等待;
      2. timeout = 0,t表示poll()调用立即返回,并给出所有I/O未就绪的文件描述符列表,不会等待更多事件。在这种情况下,poll()调用如同其名,轮询一次后立即返回。 
      3. timeout > 0,代表先阻塞等待 timeout 毫秒,超过这个时间变为非阻塞等待,poll函数返回

      1.4.返回值

      poll()调用成功时,返回revents变量不为0的所有文件描述符个数;

      如果没有任何事件发生且未超时,返回0。

      失败时,返回-1,并相应设置errno值如下:

      失败时,poll() 返回 -1,并设置 errno 为下列值之一:

      1. EBADF:一个或多个结构体中指定的文件描述符无效。
      2. EFAULT:fds 指针指向的地址超出进程的地址空间。
      3. EINTR:请求的事件之前产生一个信号,调用可以重新发起。
      4. EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
      5. ENOMEM:可用内存不足,无法完成请求。

      1.5.poll函数简单使用示例

      #include <stdio.h>  // 引入标准输入输出库,用于printf等函数  
      #include <stdlib.h> // 引入标准库,用于EXIT_FAILURE等宏定义  
      #include <unistd.h> // 引入POSIX操作系统API,用于open、close等函数  
      #include <fcntl.h>  // 引入文件控制选项,如O_RDONLY  
      #include <poll.h>   // 引入poll函数及其相关结构体pollfd  
        
      int main() {    
          // 尝试以只读模式打开名为"test.txt"的文件  
          int fd = open("test.txt", O_RDONLY);    
          if (fd < 0) {    
              // 如果文件打开失败,打印错误信息并返回失败状态  
              perror("Failed to open file");    
              return EXIT_FAILURE;    
          }    
          
          // 定义一个pollfd结构体数组,用于存储要监视的文件描述符及其事件  
          struct pollfd fds[1];    
          // 设置数组的第一个元素,指定要监视的文件描述符及其感兴趣的事件(POLLIN,表示可读)  
          fds[0].fd = fd;    
          fds[0].events = POLLIN;    
          
          // 设置poll函数的超时时间为5000毫秒(5秒)  
          int timeout = 5000; // 5 seconds    
          // 调用poll函数,监视fds数组中的文件描述符,超时时间为timeout  
          int ret = poll(fds, 1, timeout);    
          
          // 检查poll函数的返回值  
          if (ret == -1) {    
              // 如果poll调用失败,打印错误信息,关闭文件描述符,并返回失败状态  
              perror("poll failed");    
              close(fd);    
              return EXIT_FAILURE;    
          }    
          
          // 如果poll超时,没有任何事件发生  
          if (ret == 0) {    
              printf("No data within five seconds\n");    
          } else if (fds[0].revents & POLLIN) {    
              // 如果在文件描述符上发生了POLLIN事件(即可读)  
              char buf[1024];  // 定义一个缓冲区,用于存储读取的数据  
              ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1);  // 从文件描述符中读取数据到缓冲区  
              if (bytes_read > 0) {  // 如果成功读取到数据  
                  buf[bytes_read] = '\0';  // 在读取到的数据末尾添加字符串结束符'\0'  
                  printf("Read %zd bytes: %s\n", bytes_read, buf);  // 打印读取到的数据及其长度  
              }    
          }    
          
          // 关闭文件描述符,释放资源  
          close(fd);    
          // 程序正常结束  
          return EXIT_SUCCESS;    
      }

      这段代码展示了如何使用poll函数来监视一个文件描述符(在这个例子中是一个打开的文件)的状态,特别是检查文件是否有数据可读。如果文件在指定的超时时间内变得可读,程序将读取并打印文件内容;如果超时,则打印一条消息表示没有数据可读。

      如果上面那个看不懂,我们接着看下面这个

      这个例子是上一篇讲select的那个例子改过来的

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <poll.h>  // 注意头文件变化
      
      int main() {
          struct pollfd fds[1];  // poll 使用结构体数组
          char buffer[256];
      
          // 配置要监视的文件描述符(标准输入)
          fds[0].fd = STDIN_FILENO;     // 监视的文件描述符
          fds[0].events = POLLIN;       // 监视可读事件
      
          while (1) {
              printf("等待输入(5秒超时)...\n");
              
              // 调用 poll(超时时间 5000 毫秒)
              int ret = poll(fds, 1, 5000);
      
              if (ret == -1) {
                  perror("poll 错误");
                  exit(1);
              } else if (ret == 0) {
                  printf("超时,无输入。程序退出。\n");
                  break;
              } else {
                  // 检查标准输入是否就绪(通过 revents 字段)
                  if (fds[0].revents & POLLIN) {
                      // 读取输入并回显
                      fgets(buffer, sizeof(buffer), stdin);
                      printf("你输入了:%s", buffer);
                  }
              }
          }
      
          return 0;
      }

       

      连续输入1,2

       

      等5秒后

      这个实现的功能和上一篇是一模一样的

      1.6.select和poll的区别

      虽然 poll()和 select()完成相同的工作,但 poll()调用在很多方面仍然优于 select()调 用:

      1. poll()不需要用户计算最大文件描述符值加 1 作为参数传递给它。
      2. poll()对于值很大的文件描述符,效率更高。试想一下,要通过 select()监视一 个值为 900 的文件描述符,内核需要检查每个集合中的每个位,一直检查 900 个位。
      3. select()的文件描述符集合是静态的,需要对大小设置进行权衡:如果值很小, 会限制 select()可监视的最大文件描述符值;如果值很大,效率会很低。当值很大时,大的位掩码操作效率不高,尤其是当无法确定集合是否稀疏集合。对于 poll(),可以准确创建大小合适的数组。如果只需要监视一项,则仅传递一个结构体。
      4. 对于 select()调用,返回时会重新创建文件描述符集,因此每次调用都必须重新初始化。poll()系统调用会把输入(events 变量)和输出(revents 变量)分离开, 支持无需改变数组就可以重新使用。
      5. select()调用的 timeout 参数在返回时是未定义的。代码要支持可移植,需要重 新对它初始化。而对于 pselect(),不存在这些问题。

      不过,select()系统调用也有些优点:

      • select()可移植性更好,因为有些 UNIX 系统不支持 poll()。
      • select()提供了更高的超时精度:select()支持微秒级,poll()支持毫秒级。

      ppoll() 和 pselect()理论上都提供了纳秒级的超时精度,但是实际上,这两个调用的毫 秒级精度都不可靠。

      比起 poll()调用和 select()调用,epoll()调用更优,epoll()是 Linux 特有的 I/O 多路复 用解决方案,我们下次再讲。

      二,poll版TCP服务器编写

      2.1.编写

      我们这个poll版TCP服务器和那个select版本的差不多!!!所以我们基本就是在select版本上面的基础上进行修改

      PollServer.hpp初始版本

      #pragma once
      #include <iostream>
      #include "Socket.hpp"
      #include <sys/select.h>
      #include <sys/time.h>
      #include <poll.h> 
      
      const uint16_t default_port = 8877;       // 默认端口号
      const std::string default_ip = "0.0.0.0"; // 默认IP
      const int default_fd = -1;
      const int fd_num_max=64;
      const int non_event=0;
      
      class PollServer
      {
      public:
          PollServer(const uint16_t port = default_port, const std::string ip = default_ip)
              : ip_(ip), port_(port)
          {
              for(int i=0;i<fd_num_max;i++)
              {
                  event_fds[i].fd=default_fd;
                  event_fds[i].events=non_event;//暂时不关心
                  event_fds[i].revents=non_event;//暂时不关心
              }
          }
          ~PollServer()
          {
              listensock_.Close();
          }
      
          bool Init()
          {
              listensock_.Socket();
              listensock_.Bind(port_);
              listensock_.Listen();
      
              return true;
          }
      
          void Start()
          {
              
          }
      
      private:
          uint16_t port_;          // 绑定的端口号
          Sock listensock_;        // 专门用来listen的
          std::string ip_;         // ip地址
          struct pollfd event_fds[fd_num_max];
      };

      上面这些就是最最基本的东西,接下来我们就很容易写出下面这些东西!!

       void HandlerEvent()
          {
              for (int n = 0; n < fd_num_max; n++)
              {
                  int fd = event_fds[n].fd;
                  if (fd == default_fd) // 无效的
                      continue;
      
                  if (event_fds[n].revents&POLLIN) // fd套接字就绪了
                  {
                      // 1.是listen套接字就绪了
                      if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
                      {
                          Accept();
                      }
                      // 2.是通信的套接字就绪了,fd不是listen套接字
                      else // 读事件
                      {
                          Receiver(fd,n);
                      }
                  }
              }
          }
          
      
          void Start()
          {
              int listensock = listensock_.Fd();
              event_fds[0].fd=listensock;//把listen放到首个数组下标里面
              event_fds[0].events=POLLIN;//只关心读事件
              //revent可以不设置
              int timeout=3000;//3s
              for (;;)
              {
                  int n = poll(event_fds,fd_num_max,timeout);
      
                  switch (n)
                  {
                  case 0:
                      std::cout << "time out....." << std::endl;
                      break;
                  case -1:
                      std::cout << "poll error" << std::endl;
                      break;
                  default:
                      // 有事件就绪
                      std::cout << "get a new link" << std::endl;
                      HandlerEvent(); // 处理事件
                      break;
                  }
              }
          }

      接下来就是修改 Accept()和Receive()了

      void Accept()
          {
              // 我们的连接事件就绪了
              std::string clientip;
              uint16_t clientport;
      
              int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
              // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
              if (sockfd < 0)
                  return;
              else // 把新fd加入位图
              {
                  int i = 1;
                  for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
                  {
                      if (event_fds[i].fd != default_fd) // 没找到空位
                      {
                          continue;
                      }
                      else
                      { // 找到空位,但不能直接添加
                          break;
                      }
                  }
                  if (i != fd_num_max) // 没有满
                  {
                      event_fds[i].fd = sockfd; // 把新连接加入数组
                       event_fds[i].events=POLLIN ;//关心读事件
                       event_fds[i].revents = non_event;//重置一下
                       
                      Printfd();
                  }
                  else // 满了
                  {
                      close(sockfd); // 处理不了了,可以直接选择关闭连接
                      //当然,如果想要连接更多链接,我们可以在这里进行扩容操作,不过我们这里就不做了
                  }
              }
          }
           void Printfd()
          {
              std::cout << "online fd list: ";
              for (int i = 0; i < fd_num_max; i++)
              {
                  if (event_fds[i].fd == default_fd)
                      continue;
                  else
                  {
                      std::cout << event_fds[i].fd << " ";
                  }
              }
              std::cout << std::endl;
          }
      
      
          void Receiver(int fd, int i)
          {
              char in_buff[1024];
              int n = read(fd, in_buff, sizeof(in_buff) - 1);
              if (n > 0)
              {
                  in_buff[n] = 0;
                  std::cout << "get message: " << in_buff << std::endl;
              }
              else if (n == 0) // 客户端关闭连接
              {
                  close(fd);               // 我服务器也要关闭
                  event_fds[i].fd = default_fd; // 重置数组内的值
              }
              else
              {
                  close(fd);               // 我服务器也要关闭
                  event_fds[i].fd = default_fd; // 重置数组内的值
              }
          }

       PollServer.hpp

      #pragma once
      #include <iostream>
      #include "Socket.hpp"
      #include <sys/select.h>
      #include <sys/time.h>
      #include <poll.h> 
      
      const uint16_t default_port = 8877;       // 默认端口号
      const std::string default_ip = "0.0.0.0"; // 默认IP
      const int default_fd = -1;
      const int fd_num_max=64;
      const int non_event=0;
      
      class PollServer
      {
      public:
          PollServer(const uint16_t port = default_port, const std::string ip = default_ip)
              : ip_(ip), port_(port)
          {
              for(int i=0;i<fd_num_max;i++)
              {
                  event_fds[i].fd=default_fd;
                  event_fds[i].events=non_event;//暂时不关心
                  event_fds[i].revents=non_event;//暂时不关心
              }
          }
          ~PollServer()
          {
              listensock_.Close();
          }
      
          bool Init()
          {
              listensock_.Socket();
              listensock_.Bind(port_);
              listensock_.Listen();
      
              return true;
          }
      
          void Accept()
          {
              // 我们的连接事件就绪了
              std::string clientip;
              uint16_t clientport;
      
              int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
              // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
              if (sockfd < 0)
                  return;
              else // 把新fd加入位图
              {
                  int i = 1;
                  for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
                  {
                      if (event_fds[i].fd != default_fd) // 没找到空位
                      {
                          continue;
                      }
                      else
                      { // 找到空位,但不能直接添加
                          break;
                      }
                  }
                  if (i != fd_num_max) // 没有满
                  {
                      event_fds[i].fd = sockfd; // 把新连接加入数组
                       event_fds[i].events=POLLIN ;//关心读事件
                       event_fds[i].revents = non_event;//重置一下
                       
                      Printfd();
                  }
                  else // 满了
                  {
                      close(sockfd); // 处理不了了,可以直接选择关闭连接
                      //当然,如果想要连接更多链接,我们可以在这里进行扩容操作,不过我们这里就不做了
                  }
              }
          }
           void Printfd()
          {
              std::cout << "online fd list: ";
              for (int i = 0; i < fd_num_max; i++)
              {
                  if (event_fds[i].fd == default_fd)
                      continue;
                  else
                  {
                      std::cout << event_fds[i].fd << " ";
                  }
              }
              std::cout << std::endl;
          }
      
      
          void Receiver(int fd, int i)
          {
              char in_buff[1024];
              int n = read(fd, in_buff, sizeof(in_buff) - 1);
              if (n > 0)
              {
                  in_buff[n] = 0;
                  std::cout << "get message: " << in_buff << std::endl;
              }
              else if (n == 0) // 客户端关闭连接
              {
                  close(fd);               // 我服务器也要关闭
                  event_fds[i].fd = default_fd; // 重置数组内的值
              }
              else
              {
                  close(fd);               // 我服务器也要关闭
                  event_fds[i].fd = default_fd; // 重置数组内的值
              }
          }
      
          void HandlerEvent()
          {
              for (int n = 0; n < fd_num_max; n++)
              {
                  int fd = event_fds[n].fd;
                  if (fd == default_fd) // 无效的
                      continue;
      
                  if (event_fds[n].revents&POLLIN) // fd套接字就绪了
                  {
                      // 1.是listen套接字就绪了
                      if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
                      {
                          Accept();
                      }
                      // 2.是通信的套接字就绪了,fd不是listen套接字
                      else // 读事件
                      {
                          Receiver(fd,n);
                      }
                  }
              }
          }
          
      
          void Start()
          {
              int listensock = listensock_.Fd();
              event_fds[0].fd=listensock;//把listen放到首个数组下标里面
              event_fds[0].events=POLLIN;//只关心读事件
              //revent可以不设置
              int timeout=3000;//3s
              for (;;)
              {
                  int n = poll(event_fds,fd_num_max,timeout);
      
                  switch (n)
                  {
                  case 0:
                      std::cout << "time out....." << std::endl;
                      break;
                  case -1:
                      std::cout << "poll error" << std::endl;
                      break;
                  default:
                      // 有事件就绪
                      std::cout << "get a new link" << std::endl;
                      HandlerEvent(); // 处理事件
                      break;
                  }
              }
          }
      
      private:
          uint16_t port_;          // 绑定的端口号
          Sock listensock_;        // 专门用来listen的
          std::string ip_;         // ip地址
          struct pollfd event_fds[fd_num_max];
      };

      我们再链接一个看看

      非常完美了啊!!!

      2.2.poll的优缺点

      • poll不是也用了一个数组吗?他是怎么解决了fd有上限的问题?

      凭的是这个数组的大小是我们自己设置的,而select的那个数组是已经固定了的。

              其实poll的优点就是解决了select支持的fd有上限,以及用户输入信息和内核输出信息耦合的两个问题。

              但poll的缺点其实在上面的代码已经体现出来了一部分,内核在检测fd是否就绪时,需要遍历整个结构体数组检测events的值,同样用户在处理就绪的fd事件时,也需要遍历整个结构体数组检测revents的值,当rfds结构体数组越来越大时,每次遍历数组其实就会降低服务器的效率,为此,内核提供了epoll接口来解决这样的问题。

              与select相同的是,poll也需要用户自己维护一个第三方数组来存储用户需要关心的fd及事件,只不过poll不需要在每次调用前都重新设置关心的fd,因为用户的输入和内核的输出是分离的,分别在结构体的events和revents的两个字段,做到了输入和输出分离。

      2.3.源代码

      PollServer.hpp

      #pragma once
      #include <iostream>
      #include "Socket.hpp"
      #include <sys/select.h>
      #include <sys/time.h>
      #include <poll.h> 
      
      const uint16_t default_port = 8877;       // 默认端口号
      const std::string default_ip = "0.0.0.0"; // 默认IP
      const int default_fd = -1;
      const int fd_num_max=64;
      const int non_event=0;
      
      class PollServer
      {
      public:
          PollServer(const uint16_t port = default_port, const std::string ip = default_ip)
              : ip_(ip), port_(port)
          {
              for(int i=0;i<fd_num_max;i++)
              {
                  event_fds[i].fd=default_fd;
                  event_fds[i].events=non_event;//暂时不关心
                  event_fds[i].revents=non_event;//暂时不关心
              }
          }
          ~PollServer()
          {
              listensock_.Close();
          }
      
          bool Init()
          {
              listensock_.Socket();
              listensock_.Bind(port_);
              listensock_.Listen();
      
              return true;
          }
      
          void Accept()
          {
              // 我们的连接事件就绪了
              std::string clientip;
              uint16_t clientport;
      
              int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
              // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
              if (sockfd < 0)
                  return;
              else // 把新fd加入位图
              {
                  int i = 1;
                  for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
                  {
                      if (event_fds[i].fd != default_fd) // 没找到空位
                      {
                          continue;
                      }
                      else
                      { // 找到空位,但不能直接添加
                          break;
                      }
                  }
                  if (i != fd_num_max) // 没有满
                  {
                      event_fds[i].fd = sockfd; // 把新连接加入数组
                       event_fds[i].events=POLLIN ;//关心读事件
                       event_fds[i].revents = non_event;//重置一下
                       
                      Printfd();
                  }
                  else // 满了
                  {
                      close(sockfd); // 处理不了了,可以直接选择关闭连接
                      //当然,如果想要连接更多链接,我们可以在这里进行扩容操作,不过我们这里就不做了
                  }
              }
          }
           void Printfd()
          {
              std::cout << "online fd list: ";
              for (int i = 0; i < fd_num_max; i++)
              {
                  if (event_fds[i].fd == default_fd)
                      continue;
                  else
                  {
                      std::cout << event_fds[i].fd << " ";
                  }
              }
              std::cout << std::endl;
          }
      
      
          void Receiver(int fd, int i)
          {
              char in_buff[1024];
              int n = read(fd, in_buff, sizeof(in_buff) - 1);
              if (n > 0)
              {
                  in_buff[n] = 0;
                  std::cout << "get message: " << in_buff << std::endl;
              }
              else if (n == 0) // 客户端关闭连接
              {
                  close(fd);               // 我服务器也要关闭
                  event_fds[i].fd = default_fd; // 重置数组内的值
              }
              else
              {
                  close(fd);               // 我服务器也要关闭
                  event_fds[i].fd = default_fd; // 重置数组内的值
              }
          }
      
          void HandlerEvent()
          {
              for (int n = 0; n < fd_num_max; n++)
              {
                  int fd = event_fds[n].fd;
                  if (fd == default_fd) // 无效的
                      continue;
      
                  if (event_fds[n].revents&POLLIN) // fd套接字就绪了
                  {
                      // 1.是listen套接字就绪了
                      if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
                      {
                          Accept();
                      }
                      // 2.是通信的套接字就绪了,fd不是listen套接字
                      else // 读事件
                      {
                          Receiver(fd,n);
                      }
                  }
              }
          }
          
      
          void Start()
          {
              int listensock = listensock_.Fd();
              event_fds[0].fd=listensock;//把listen放到首个数组下标里面
              event_fds[0].events=POLLIN;//只关心读事件
              //revent可以不设置
              int timeout=3000;//3s
              for (;;)
              {
                  int n = poll(event_fds,fd_num_max,timeout);
      
                  switch (n)
                  {
                  case 0:
                      std::cout << "time out....." << std::endl;
                      break;
                  case -1:
                      std::cout << "poll error" << std::endl;
                      break;
                  default:
                      // 有事件就绪
                      std::cout << "get a new link" << std::endl;
                      HandlerEvent(); // 处理事件
                      break;
                  }
              }
          }
      
      private:
          uint16_t port_;          // 绑定的端口号
          Sock listensock_;        // 专门用来listen的
          std::string ip_;         // ip地址
          struct pollfd event_fds[fd_num_max];
      };

      Socket.hpp

      #pragma once  
        
      #include <iostream>  
      #include <string>  
      #include <unistd.h>  
      #include <cstring>  
      #include <sys/types.h>  
      #include <sys/stat.h>  
      #include <sys/socket.h>  
      #include <arpa/inet.h>  
      #include <netinet/in.h>  
       
        
      // 定义一些错误代码  
      enum  
      {  
          SocketErr = 2,    // 套接字创建错误  
          BindErr,          // 绑定错误  
          ListenErr,        // 监听错误  
      };  
        
      // 监听队列的长度  
      const int backlog = 10;  
        
      class Sock  //服务器专门使用
      {  
      public:  
          Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字  
          {  
          }  
          ~Sock()  
          {  
              // 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源  
          }  
        
          // 创建套接字  
          void Socket()  
          {  
              sockfd_ = socket(AF_INET, SOCK_STREAM, 0);  
              if (sockfd_ < 0)  
              {  
                  printf("socket error, %s: %d", strerror(errno), errno); //错误  
                  exit(SocketErr); // 发生错误时退出程序  
              } 
              int opt=1;
              setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //服务器主动关闭后快速重启
          }  
        
          // 将套接字绑定到指定的端口上  
          void Bind(uint16_t port)  
          {  
              //让服务器绑定IP地址与端口号
              struct sockaddr_in local;  
              memset(&local, 0, sizeof(local));//清零  
              local.sin_family = AF_INET;  // 网络
              local.sin_port = htons(port);  // 我设置为默认绑定任意可用IP地址
              local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口  
        
              if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)  //让自己绑定别人
              {  
                  printf("bind error, %s: %d", strerror(errno), errno);  
                  exit(BindErr);  
              }  
          }  
        
          // 监听端口上的连接请求  
          void Listen()  
          {  
              if (listen(sockfd_, backlog) < 0)  
              {  
                  printf("listen error, %s: %d", strerror(errno), errno);  
                  exit(ListenErr);  
              }  
          }  
        
          // 接受一个连接请求  
          int Accept(std::string *clientip, uint16_t *clientport)  
          {  
              struct sockaddr_in peer;  
              socklen_t len = sizeof(peer);  
              int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);  
              
              if(newfd < 0)  
              {  
                  printf("accept error, %s: %d", strerror(errno), errno);  
                  return -1;  
              }  
              
              char ipstr[64];  
              inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));  
              *clientip = ipstr;  
              *clientport = ntohs(peer.sin_port);  
        
              return newfd; // 返回新的套接字文件描述符  
          }  
        
          // 连接到指定的IP和端口——客户端才会用的  
          bool Connect(const std::string &ip, const uint16_t &port)  
          {  
              struct sockaddr_in peer;//服务器的信息  
              memset(&peer, 0, sizeof(peer));  
              peer.sin_family = AF_INET;  
              peer.sin_port = htons(port);
       
              inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));  
        
              int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));  
              if(n == -1)   
              {  
                  std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;  
                  return false;  
              }  
              return true;  
          }  
        
          // 关闭套接字  
          void Close()  
          {  
              close(sockfd_);  
          }  
        
          // 获取套接字的文件描述符  
          int Fd()  
          {  
              return sockfd_;  
          }  
        
      private:  
          int sockfd_; // 套接字文件描述符  
      };

      main.cc

      #include"PollServer.hpp"
      #include<memory>
      
      int main()
      {
          std::unique_ptr<PollServer> svr(new PollServer());
          svr->Init();
          svr->Start();
      }

      makefile

      poll_server:main.cc
      	g++ -o $@ $^ -std=c++11
      .PHONY:clean
      clean:
      	rm -rf poll_server

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

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

      抵扣说明:

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

      余额充值