学习C++项目——select模型,poll模型和epoll模型

学习计算机网络编程

一、思路和学习方法

  本文学习于:C语言技术网(www.freecplus.net),在 b 站学习于 C 语言技术网,并加以自己的一些理解和复现,如有侵权会删除。
  接下来应该是网络编程部分最难也是最常用的部分,同时在这一章我会全部学习完毕。

二、网络编程继续深入

2.1 I/O复用

  多进程/多线程网络服务端在创建进程/线程CPU和内存开销很大。多线程/多进程并发模型,为每个socket分配一个进程/线程。I/O多路复用,采用单个进/线程就可以管理多个socket。I/O复用有三种方案:select,poll,epoll。接下来跟着进行学习。

2.2 select 模型

  select也是一个服务端程序设计的方法,其中流程图如下,
在这里插入图片描述
  在之前的网络服务中,网络服务中服务端是阻塞在 accept() 部分,形成一个客户端等待队列,在这里是阻塞在 select() 部分,客户端有连接断开请求时,会更新 socket 集合,并重新加入 select() 阻塞队列。其中两个主要程序如下,首先是客户端程序,命名为 selectSever.cpp ,其源代码如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net)
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

// 初始化服务端的监听端口
int initserver(int port);

int main(int argc, char *argv[]){
   if(argc != 2){
     printf("usage:./selectServer port\n"); return -1;
    }
     
    // 初始化服务端用于监听的 socket
   int listensock = initserver(atoi(argv[1]));
   printf("listensock = %d\n", listensock);

   if(listensock < 0){
     printf("initserver() failed.\n"); return -1;
    }
    
   fd_set readfdset; // 读事件的集合,包括监听socket和客户端连接上来的socket.
   int maxfd; // readfdset 中 socket 的最大值

    // 初始化结构体,把 listensock 添加到集合中
   FD_ZERO(&readfdset);
   FD_SET(listensock, &readfdset);

   maxfd = listensock;

   while(1){
        // 调用 select 函数时,会改变 socket 集合的内容,所以要把 socket 集合保存下来,传一个临时的给 select
      fd_set tmpfdset = readfdset;

      int infds = select(maxfd + 1, &tmpfdset, NULL, NULL, NULL);
      // printf("select infds = %d\n", infds);
    
        // 返回失败。
      if(infds < 0){
        printf("select() failed.\n"); perror("select()"); break;
        }

        // 超时,在程序中,select最后一个参数为空,不存在超时的情况
      if(infds == 0){
         printf("select() timeout.\n"); continue;
        }

        // 检查有事情发生的 socket,包括监听和客户端连接的 socket
        // 这里是客户端的 socket 事件,每次都要遍历整个集合,因为可能有多个 socket 有事件
      for(int eventfd = 0; eventfd <= maxfd; eventfd++){
         if(FD_ISSET(eventfd, &tmpfdset) <= 0) continue;

         if(eventfd == listensock){
                // 如果发生事件的是 listensock,表示有新的客户端连上来
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
            if(clientsock < 0){printf("accept() failed.\n"); continue;}

            printf("client(socket = %d) connected ok.\n", clientsock);
  
               // 把新的客户端 socket 加入集合
            FD_SET(clientsock, &readfdset);

            if(maxfd < clientsock) maxfd = clientsock; 
            continue;
          }else{
               // 客户端有数据过来或客户端的 socket 连接被断开
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));

               // 读取客户端的数据
            ssize_t isize = read(eventfd, buffer, sizeof(buffer));

               // 发生了错误或 socket 被对方关闭
            if(isize <= 0){
               printf("client(event = %d) disconnected.\n", eventfd);
               close(eventfd);  // 关闭客户端的 socket
               FD_CLR(eventfd, &readfdset);  // 从集合中移去客户端的 socket
                   // 重新计算 maxfd 的值,注意,只有当 evntfd = maxfd 时才需要计算
               if(eventfd = maxfd){
                  for(int ii = maxfd; ii > 0; ii--){
                     if(FD_ISSET(ii, &readfdset)){
                        maxfd = ii; break;
                           }
                       }
                  printf("maxfd = %d\n", maxfd);
                   }
               continue;
                }
             printf("recv(eventfd = %d, size = %d):%s\n", eventfd, isize, buffer);

                 // 把收到的报文发回给客户端
             write(eventfd, buffer, strlen(buffer));
             }
        }
    }
}

// 初始化服务端的监听端口
int initserver(int port){
   int sock = socket(AF_INET, SOCK_STREAM, 0);
   if(sock < 0){
     printf("socket() failed.\n"); return -1;
    }

   // linux 如下
   int opt = 1; unsigned int len = sizeof(opt);
   setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
   setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len);

   struct sockaddr_in servaddr;
   servaddr.sin_family = AF_INET;
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
   servaddr.sin_port = htons(port);

   if(bind(sock, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
     printf("bind() failed.\n"); close(sock); return -1;
    }

   if(listen(sock, 5) != 0){
     printf("listen() failed.\n"); close(sock); return -1;
    }

   return sock;
}

  接着是服务端程序如下,命名为 client.cpp ,其源代码如下,

/*
 * 程序名:book247.cpp,此程序用于演示用C++的方法封装socket客户端
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>

int main(int argc, char *argv[]){
  if(argc != 3){
    printf("usage:./tcpclient ip port\n"); return -1;
   }

  int sockfd;
  struct sockaddr_in servaddr;
  char buf[1024];

  if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("socket() failed.\n"); return -1; }

  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2]));
  servaddr.sin_addr.s_addr = inet_addr(argv[1]);

  if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0){
    printf("connect(%s:%s) failed.\n", argv[1], argv[2]); close(sockfd); return -1; 
   }

  printf("connect ok.\n");

  for(int ii = 0; ii < 10000; ii++){
    memset(buf, 0, sizeof(buf));
    printf("please input:"); scanf("%s", buf);
     // sprintf(buf, "11111111111ii = %08d", ii);

    if(write(sockfd, buf, strlen(buf)) <= 0){
      printf("write() failed.\n"); close(sockfd); return -1;
     }

    memset(buf, 0, sizeof(buf));
    if(read(sockfd, buf, sizeof(buf)) <= 0){
      printf("read() failed.\n"); close(sockfd); return -1;
     }

    printf("recv: %s\n", buf);

     // close(sockfd); break;
   }
}

  这两个程序主要功能是,客户端和服务端建立连接后,客户端在键盘上输入什么,并且输入的数据会按照空格符进行分割,然后依次发送出去,服务端就可以显示什么,并且服务端会显示其 socket 文件号,发送数据的大小,其运行结果如下,
在这里插入图片描述
  接下来,对客户端和服务端程序进行一一分解学习,然后了解其原理。首先是客户端程序,主要理解IO部分,

for(int ii = 0; ii < 10000; ii++){
  memset(buf, 0, sizeof(buf));
  printf("please input:"); scanf("%s", buf);
   // sprintf(buf, "11111111111ii = %08d", ii);

  if(write(sockfd, buf, strlen(buf)) <= 0){
    printf("write() failed.\n"); close(sockfd); return -1;
   }

  memset(buf, 0, sizeof(buf));
  if(read(sockfd, buf, sizeof(buf)) <= 0){
    printf("read() failed.\n"); close(sockfd); return -1;
   }

  printf("recv: %s\n", buf);

   // close(sockfd); break;
}

这部分和正常的客户端程序基本类似,主要特点是通过 scanf 向 buf 里面写入数据,然后依
次通过 sock 发送数据,接着再接受返回的数据,并打印出来。

  接着对服务端程序进行理解,这部分是 IO 复用的核心服务端程序代码,这部分代码和上图 select 模型相比较进行学习,理解如下,
在这里插入图片描述

1. int initserver(int port) 
这部分是对服务端 sock 进行基本的配置,和前面学习的服务端程序一样。

2. fd_set readfdset; // 1. 创建socket的集合fd_set,对应程序第一步
int maxfd; // readfdset 中 socket 的最大值

FD_ZERO(&readfdset); // 2. 把监听的socket加入到集合中,对应程序第二步
FD_SET(listensock, &readfdset);
maxfd = listensock;
这部分主要针对 select 函数进行设置,因为每次调用 select 函数时,会改变 socket 集
合的内容,所以要把 socket 集合保存下来,传一个临时的给 select。每次都要重复这个动作

3. 
// 3. 阻塞在 select 这里,对应程序的第三步
select(maxfd, fd_set, NULL, NULL, NULL)中会修改fd_set的内容,所以需要
fd_set tmpfdset = readfdset; // 用临时的 tmpfdset 做备份使用
// 这一步 select 就会阻塞,与 accept 函数相类似
int infds = select(maxfd + 1, &tmpfdset, NULL, NULL, NULL); 

4. 
// 4. 用这个方法判断哪个socket有事件发生,前面循环起到对事件遍历的作用,对应程序第四步
if(FD_ISSET(eventfd, &tmpfdset) <= 0) continue; 

5. // 5. 这部分假如监听的socket有事件发生,表示有新客户端连接请求,对应程序第五步左边部分
if(eventfd == listensock){
   // 如果发生事件的是 listensock,表示有新的客户端连上来
   struct sockaddr_in client;
   socklen_t len = sizeof(client);
   // 这里会返回客户端的连接 socket
   int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
   if(clientsock < 0){printf("accept() failed.\n"); continue;}

   printf("client(socket = %d) connected ok.\n", clientsock);

   // 把新的客户端 socket 加入集合
   FD_SET(clientsock, &readfdset);
   // 更新 maxfd
   if(maxfd < clientsock) maxfd = clientsock; 
   continue;
}

6. // 6. 客户端socket有事件,有数据可读,socket 连接断开,对应程序第五步右边部分
else{
 // 客户端有数据过来或客户端的 socket 连接被断开
 char buffer[1024];
 memset(buffer, 0, sizeof(buffer));

 // 读取客户端的数据,返回值小于0是失败,等于0是连接关闭,大于0是读取数据大小
 ssize_t isize = read(eventfd, buffer, sizeof(buffer));

 // 发生了错误或 socket 被对方关闭
 if(isize <= 0){
    printf("client(event = %d) disconnected.\n", eventfd);
    close(eventfd);  // 关闭客户端的 socket
    FD_CLR(eventfd, &readfdset);  // 从集合中移去客户端的 socket
    // 重新计算 maxfd 的值,注意,只有当 evntfd = maxfd 时才需要计算
    if(eventfd = maxfd){
       for(int ii = maxfd; ii > 0; ii--){
          if(FD_ISSET(ii, &readfdset)){
             maxfd = ii; break;
                }
            }
       printf("maxfd = %d\n", maxfd);
       }
    continue;
  }
  // 接收成功进行这一步
  printf("recv(eventfd = %d, size = %d):%s\n", eventfd, isize, buffer);

  // 把收到的报文发回给客户端
  write(eventfd, buffer, strlen(buffer));
}

  通过上面的分析,基本上把 select 模型的架构过了一遍,明白了相关函数使用方法,下面是关于 linux 的 gdb 调试的一般方法,如下

// 编译好的cpp文件,使用gdb方式打开
gdb selectServer

// 设置程序参数
set args 5005

// 运行程序 run
r

// 设置程序断点,定位在某一行,比如 21 列
b 21

// 跳转到下一行 next
n

// 查看某一行的参数情况,查看到 maxfd 参数的值
p maxfd

// 循环继续 continue
c

// 退出调试界面
q

  在 IO 复用中,使用 select 函数阻塞以后,在运行 accept 函数以后不会再发生阻塞。

2.3 位图 bitmap 的原理

  继续深入理解 select 的工作原理。在这个定义中

fd_set readfset

其中 fd_set 是一个结构体,与之相关的 FD 函数都是对这个结构体操作,从而实现相应的功能。定义如下

typedef struct{
     unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

如果哪一个位是 x ,那就把哪一个位置为 1 ,就类似图一样,大小为 1024 ,一般不会改变,因此 select 最大集合为 1024 . 其中相关函数

FD_SET
FD_ISSET
FD_CLR
等等相关函数
...
都是对 fd 位图进行判断和操作

因此对于 select 函数,

int infds = select(maxfd + 1, &tmpfdset, NULL, NULL, NULL); 
就通过 tmpfdset 来判断,使用 select 来监听哪一个 socket 有事件,在执行完成后会
修改 tmpfdset 位图,只有一个起作用,这里理解可以看视频,不好解释出来,因此在后面执
行中

FD_SET 使用的是原位图加入 socket 标记
FD_CLR 清除使用的是原位图,清楚 socket 标记

FD_ISSET 在判断的时候,使用的是拷贝位图,只有当下的起作用,其余都被清除了。
2.4 select 模型代码详解

  其中 select 函数定义如下

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds   指集合中所有描述符的最大值 + 1
readfds  监视是否有新的 socket 连接,或者现有描述符可读
writefds  是否可以向描述符中写入数据
exceptfds  监视描述符的异常
timeout  设置超时机制

返回值:
0 表示超时
小于 0 表示返回错误
大于 0 表示返回的事件个数

其中超时机制是常用的方法。而且 pselect 函数可以屏蔽某些不想要的信号,比 select 多出这一项功能。

2.5 select 模型会丢失事件和数据吗?

  select 的水平触发,其作用是如果报告了 fd 后事件没有被处理或者数据没有被全部读取,那么下次 select 会再次报告该 fd。现在进行测试理解,比如在服务端程序读取数据部分改为,

ssize_t isize = read(eventfd, buffer, sizeof(buffer));

这部分改为如下,这样数据读取是不完全的

ssize_t isize = read(eventfd, buffer, 10);

  这样就可以直观的感受它的效果,
在这里插入图片描述

2.6 测试select 模型的性能和优缺点

  现在测试 select 的服务性能,观察其效果。这里我主要观察 up 主进行测试,自己没有进行操作。以后自己用到再继续学习,因为现在对这些理解感觉还不是特别到位。
  select 函数的缺点:
  1. select 支持的文件描述符数量太小了,默认 1024 , 虽然可以调整,但是描述符数量越大,效率越低;
  2.每次调用 select,都需要把 fdset 从用户态拷贝到内核;
  3.同时在线的大量客户端有事件发生的可能很少,但还是需要遍历 fdset,因此随着监视的描述符数量的增长,效率也会下降。
  在 linux 世界里,一切皆文件,文件就是一串二进制流,不管 socket 、管道、终端、设备等都是文件,一切都是流。在信息交换的过程中,都是对这些流进行数据的收发操作,简称 I/O 操作,往流中读出数据,系统调用 read,写入数据,系统调用 write。
  select 是 I/O 复用函数,除了用于网络通信,还可以用于文件、管道、终端、设备等操作,但开发场景比较少。
  多测试,认真思考,up 主给的建议!

2.7 poll模型

  poll 和 select 在本质上没有差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll 没有最大文件描述符数量的限制。
  select 中 fdset 采用 bitmap,poll 采用数组。
  poll 和 select 同样存在一个缺点就是,文件描述符的数组被整体复制于用户态和内核态的地址空间之间,而不论这些文件描述符是否有时间,它的开销随着文件描述符的数量增大而增大。
  还有 poll 返回后,也需要遍历整个描述符的数组才能得到有事件的描述符。对于 poll 的函数

int poll(struct pollfd * fds, nfds_t nfds, int timeout);
fds  fd 数组
nfds  fd 数组中最大个数
timeout  超时时间
struct pollfd{
    int fd;   // 文件描述符
    short events;   // 请求时间
    short revents;   // 返回事件
}

  在 poll 函数返回值中,只修改 pollfd.revents 的值,其他不变。在 poll 模型中,只是 poll 使用的是数组形式,select 使用的是位图形式,服务端程序如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net)
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#include <sys/fcntl.h>

// ulimit -n
#define MAXNFDS 1024

// 初始化服务端的监听端口
int initserver(int port);

int main(int argc, char *argv[]){
   if(argc != 2){
     printf("usage:./selectServer port\n"); return -1;
    }
     
    // 初始化服务端用于监听的 socket
   int listensock = initserver(atoi(argv[1]));
   printf("listensock = %d\n", listensock);

   if(listensock < 0){
     printf("initserver() failed.\n"); return -1;
    }

   int maxfd; // fds 数组中需要监视的 socket 的大小
   struct pollfd fds[MAXNFDS];  // fds 存放需要监视的 socket
   
   for(int ii = 0; ii < MAXNFDS; ii++) fds[ii].fd = -1;  //  初始化数组,把全部 fd 设置为 -1

   fds[listensock].fd = listensock;
   fds[listensock].events = POLLIN;  // 有数据可读事件,包括新客户端的连接,客户端 socket 有数据可读和客户端 socket 断开的三种情况

   maxfd = listensock;

   while(1){
      int infds = poll(fds, maxfd + 1, -1);
      // printf("select infds = %d\n", infds);

        // 返回失败
      if(infds < 0){
        printf("poll() failed.\n"); perror("poll()"); break;
        }

        // 超时,在程序中,select最后一个参数为空,不存在超时的情况
      if(infds == 0){
         printf("poll() timeout.\n"); continue;
        }

        // 检查有事情发生的 socket,包括监听和客户端连接的 socket
        // 这里是客户端的 socket 事件,每次都要遍历整个集合,因为可能有多个 socket 有事件
      for(int eventfd = 0; eventfd <= maxfd; eventfd++){
         if(fds[eventfd].fd < 0) continue;

         if(fds[eventfd].revents & POLLIN == 0) continue;

         fds[eventfd].revents = 0;  // 先把revents清空。

         if(eventfd == listensock){
                // 如果发生事件的是 listensock,表示有新的客户端连上来
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
            if(clientsock < 0){printf("accept() failed.\n"); continue;}

            printf("client(socket = %d) connected ok.\n", clientsock);

            if(clientsock > MAXNFDS){
               printf("clientsock(%d) > MAXNFDS(%d)\n", clientsock, MAXNFDS); 
               close(clientsock); 
               continue;
               }

            fds[clientsock].fd = clientsock;
            fds[clientsock].events = POLLIN;
            fds[clientsock].revents = 0;

            if(maxfd < clientsock) maxfd = clientsock; 
            printf("maxfd = %d\n", maxfd);
            continue;
          }else{
               // 客户端有数据过来或客户端的 socket 连接被断开
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));

               // 读取客户端的数据
	     ssize_t isize = read(eventfd, buffer, sizeof(buffer));

               // 发生了错误或 socket 被对方关闭
            if(isize <= 0){
               printf("client(event = %d) disconnected.\n", eventfd);
               close(eventfd);  // 关闭客户端的 socket
               fds[eventfd].fd = -1;
                   // 重新计算 maxfd 的值,注意,只有当 evntfd = maxfd 时才需要计算
               if(eventfd == maxfd){
                  for(int ii = maxfd; ii > 0; ii--){
                     if(fds[ii].fd != -1){
                        maxfd = ii; break;
                           }
                       }
                  printf("maxfd = %d\n", maxfd);
                   }
               continue;
                }
             printf("recv(eventfd = %d, size = %d):%s\n", eventfd, isize, buffer);

                 // 把收到的报文发回给客户端
             write(eventfd, buffer, strlen(buffer));
             }
        }
    }
}

// 初始化服务端的监听端口
int initserver(int port){
   int sock = socket(AF_INET, SOCK_STREAM, 0);
   if(sock < 0){
     printf("socket() failed.\n"); return -1;
    }

   // linux 如下
   int opt = 1; unsigned int len = sizeof(opt);
   setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
   setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len);

   struct sockaddr_in servaddr;
   servaddr.sin_family = AF_INET;
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
   servaddr.sin_port = htons(port);

   if(bind(sock, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
     printf("bind() failed.\n"); close(sock); return -1;
    }

   if(listen(sock, 5) != 0){
     printf("listen() failed.\n"); close(sock); return -1;
    }

   return sock;
}

  接下来进行分析,

1. int infds = poll(fds, maxfd + 1, -1);
在这里不用像 select 备份位图,所以不用备份它的数组。其中返回值为 0 是超时,小于 0 是失败。

2. if(fds[eventfd].fd < 0) continue;
if(fds[eventfd].revents & POLLIN == 0) continue;
如果没有事件发生或者数组小于 0 ,那就继续遍历

3. if(eventfd == listensock){
       // 如果发生事件的是 listensock,表示有新的客户端连上来
   struct sockaddr_in client;
   socklen_t len = sizeof(client);
   int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
   if(clientsock < 0){printf("accept() failed.\n"); continue;}

   printf("client(socket = %d) connected ok.\n", clientsock);

   if(clientsock > MAXNFDS){
      printf("clientsock(%d) > MAXNFDS(%d)\n", clientsock, MAXNFDS); 
      close(clientsock); 
      continue;
      }
   // 如果有事件发生,把 clientsock加入到数组里面,
   fds[clientsock].fd = clientsock;
   fds[clientsock].events = POLLIN;
   fds[clientsock].revents = 0;

   if(maxfd < clientsock) maxfd = clientsock; 
   printf("maxfd = %d\n", maxfd);
   continue;
如果有事件发生,把 clientsock加入到数组里面,然后继续遍历。

4. else{
  // 客户端有数据过来或客户端的 socket 连接被断开
 char buffer[1024];
 memset(buffer, 0, sizeof(buffer));

    // 读取客户端的数据
ssize_t isize = read(eventfd, buffer, sizeof(buffer));

    // 发生了错误或 socket 被对方关闭
 if(isize <= 0){
    printf("client(event = %d) disconnected.\n", eventfd);
    close(eventfd);  // 关闭客户端的 socket
    fds[eventfd].fd = -1;
        // 重新计算 maxfd 的值,注意,只有当 evntfd = maxfd 时才需要计算
    if(eventfd == maxfd){
       for(int ii = maxfd; ii > 0; ii--){
          if(fds[ii].fd != -1){
             maxfd = ii; break;
                }
            }
       printf("maxfd = %d\n", maxfd);
        }
    continue;
     }
  printf("recv(eventfd = %d, size = %d):%s\n", eventfd, isize, buffer);

      // 把收到的报文发回给客户端
  write(eventfd, buffer, strlen(buffer));
  }
如果客户端有数据过来时,并且判断eventfd是否被关闭,如果被关闭对数组进行重置,如果没有,那进行读写。

  其运行效果和 select 方式一样。

2.7 epoll模型的原理及实现

  epoll 解决了 select 和 poll 所有的问题(fdset拷贝和轮询),采用了最合理的设计和实现方案。epoll 中主要有三个函数,

1. 创建 epoll 的句柄,它本身就是一个 fd
int epoll_create(int size);

2. 注册需要监视 fd 和事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

3. 等待事件发生
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

  其服务端程序如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net)
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>

#define MAXEVENTS 100  // 事件结果数组的大小

// 初始化服务端的监听端口
int initserver(int port);

int main(int argc, char *argv[]){
   if(argc != 2){
     printf("usage:./selectServer port\n"); return -1;
    }
     
    // 初始化服务端用于监听的 socket
   int listensock = initserver(atoi(argv[1]));
   printf("listensock = %d\n", listensock);

   if(listensock < 0){
     printf("initserver() failed.\n"); return -1;
    }
    
   int epollfd;
   
   char buffer[1024];
   memset(buffer, 0, sizeof(buffer));
   
    // 创建一个描述符
   epollfd = epoll_create(1);
   
    // 添加监听描述符事件
   struct epoll_event ev;
   ev.data.fd = listensock;
   ev.events = EPOLLIN;
   epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &ev);
   
   while(1){
      struct epoll_event events[MAXEVENTS]; // 存放有事件发生的结构数组
      
        // 等待监视的 socket 有事件发生
      int infds = epoll_wait(epollfd, events, MAXEVENTS, -1);
      // printf("epoll_wait infds = %d\n", infds);

        // 返回失败
      if(infds < 0){
        printf("epoll_wait() failed.\n"); perror("epoll_wait()"); break;
        }

        // 超时
      if(infds == 0){
         printf("epoll_wait() timeout.\n"); continue;
        }

        // 遍历有事件发生的结构数组
      for(int ii = 0; ii < infds; ii++){
         if((events[ii].data.fd == listensock) && (events[ii].events & EPOLLIN)){
                // 如果发生事件的是 listensock,表示有新的客户端连上来
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
            if(clientsock < 0){printf("epoll() failed.\n"); continue;}
               
               // 把新的客户端添加到 epoll 中
            memset(&ev, 0, sizeof(struct epoll_event));
            ev.data.fd = clientsock;
            ev.events = EPOLLIN;
            epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &ev);

            printf("client(sock = %d) connected ok.\n", clientsock);
            continue;
          }else if(events[ii].events & EPOLLIN){
               // 客户端有数据过来或客户端的 socket 连接被断开
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));

               // 读取客户端的数据
	          ssize_t isize = read(events[ii].data.fd, buffer, sizeof(buffer));

               // 发生了错误或 socket 被对方关闭
            if(isize <= 0){
               printf("client(event = %d) disconnected.\n", events[ii].data.fd);
               
                   // 把已断开的客户端从 epoll 中删除
               memset(&ev, 0, sizeof(struct epoll_event));
               ev.events = EPOLLIN;
               ev.data.fd = events[ii].data.fd;
               epoll_ctl(epollfd, EPOLL_CTL_DEL, events[ii].data.fd, &ev);
               close(events[ii].data.fd);
               continue;
                }
             printf("recv(eventfd = %d, size = %d):%s\n", events[ii].data.fd, isize, buffer);

                 // 把收到的报文发回给客户端
             write(events[ii].data.fd, buffer, strlen(buffer));
             }
        }
    }
   close(epollfd);
   
   return 0;
}

// 初始化服务端的监听端口
int initserver(int port){
   int sock = socket(AF_INET, SOCK_STREAM, 0);
   if(sock < 0){
     printf("socket() failed.\n"); return -1;
    }

   // linux 如下
   int opt = 1; unsigned int len = sizeof(opt);
   setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
   setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len);

   struct sockaddr_in servaddr;
   servaddr.sin_family = AF_INET;
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
   servaddr.sin_port = htons(port);

   if(bind(sock, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
     printf("bind() failed.\n"); close(sock); return -1;
    }

   if(listen(sock, 5) != 0){
     printf("listen() failed.\n"); close(sock); return -1;
    }

   return sock;
}

// 把 socket 设置为非阻塞的方式
int setnonblocking(int sockfd){
   if(fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK) == -1) return -1;
   return 0;
}

  接下来对 epoll 进行分析,

1. // 创建一个描述符
epollfd = epoll_create(1);
// 添加监听描述符事件
struct epoll_event ev;
ev.data.fd = listensock;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &ev);
创建文件描述符,然后把 epoll 文件描述符添加到 ev 结构体里面

2. struct epoll_event events[MAXEVENTS]; // 存放有事件发生的结构数组
// 等待监视的 socket 有事件发生
int infds = epoll_wait(epollfd, events, MAXEVENTS, -1);
// printf("epoll_wait infds = %d\n", infds);
存放有事件发生的数组,接着监听事件发生情况

3. // 遍历有事件发生的结构数组
for(int ii = 0; ii < infds; ii++){
   if((events[ii].data.fd == listensock) && (events[ii].events & EPOLLIN)){
          // 如果发生事件的是 listensock,表示有新的客户端连上来
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
      if(clientsock < 0){printf("epoll() failed.\n"); continue;}
         
         // 把新的客户端添加到 epoll 中
      memset(&ev, 0, sizeof(struct epoll_event));
      ev.data.fd = clientsock;
      ev.events = EPOLLIN;
      epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &ev);

      printf("client(sock = %d) connected ok.\n", clientsock);
      continue;
}
遍历时间发生的结构体数组,这里和 select 和 poll 区别是,前面的需要遍历所有的集合,
而 epoll 是发生的事件都在前面,所以不用遍历全部,只用遍历发生的就行。infds是事件数。如果满足条件,则通过epoll_ctl把它们添加进去。


4. else if(events[ii].events & EPOLLIN){
// 客户端有数据过来或客户端的 socket 连接被断开
char buffer[1024];
memset(buffer, 0, sizeof(buffer));

// 读取客户端的数据
ssize_t isize = read(events[ii].data.fd, buffer, sizeof(buffer));

// 发生了错误或 socket 被对方关闭
if(isize <= 0){
   printf("client(event = %d) disconnected.\n", events[ii].data.fd);
   
   // 把已断开的客户端从 epoll 中删除
   memset(&ev, 0, sizeof(struct epoll_event));
   ev.events = EPOLLIN;
   ev.data.fd = events[ii].data.fd;
   epoll_ctl(epollfd, EPOLL_CTL_DEL, events[ii].data.fd, &ev);
   close(events[ii].data.fd);
   continue;
    }
 printf("recv(eventfd = %d, size = %d):%s\n", events[ii].data.fd, isize, buffer);

     // 把收到的报文发回给客户端
 write(events[ii].data.fd, buffer, strlen(buffer));
 }
如果发生错误和连接断开,通过 epoll_ctl 把它从数组中删除,否则继续进行数据传输

  运行结果如下,
在这里插入图片描述

2.8 epoll的水平触发和边缘触发

  水平触发:如果报告了 fd 后事件没有被处理或数据没有被全部读取,那么 epoll 会立即再报告该 fd。
  边缘触发:如果报告了 fd 后事件没有被处理或数据没有被全部读取,那么 epoll 会下次再报告该 fd。
这里演示主要看 up 主的视频,在 b 站 c++ 网络编程部分最后几节。

三、总结

  对 c++ 网络编程学习完成以后,前面部分比较好理解,后面部分,因为没有应用的背景,对它描述的功能还是一知半解,不过也学习到了程序架构,实现部署的方法,也知道怎么查阅相关功能,这为以后应用打下了基础。后面如果用到,不懂的地方再仔细查阅学习理解了,暂时就这样吧。
  现在关于 c++ 网络编程基础就学习完成了,后面学习数据库内容。继续学习,继续理解,争取融会贯通。

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: selectpollepoll都是用来处理多路复用IO的方法。 - select: 是最早出现的多路复用IO方泏,它能同时处理多个文件描述符,但是它存在最大文件描述符数量和每次调用时间复杂度的限制。 - poll: 与select相比,它能同时处理更多的文件描述符,并且每次调用的时间复杂度也更低。 - epoll: 是Linux下特有的多路复用IO方法,它的性能比selectpoll更优,能同时处理更多的文件描述符。 总结来说,epoll相比selectpoll性能更优,能处理更多的文件描述符。 ### 回答2: selectpollepoll都是Linux中用来实现I/O多路复用的函数。I/O多路复用的主要作用是实现在一个进程中同时监听多个文件描述符的可读、可写和异常事件,以减少进程的系统调用数量,提高程序的并发性能。 select是最早出现的I/O多路复用函数,其调用方式比较简单,只需要用一个fd_set集合存储要监听的文件描述符,然后调用select函数。但是select有一些缺点,因为它使用位图来存储文件描述符,当监听的文件描述符数量增加时,位图的大小也会增加,导致性能下降。 poll是一种改进的I/O多路复用函数,它使用一个pollfd结构来存储文件描述符和事件,并在同一个结构体数组中存储所有需要监听的文件描述符,这样的话,无论监听的文件描述符数量增加,只需要重新分配一个更大的数组即可,可以提高性能。 epoll是Linux中最新、最高效的I/O多路复用函数,它使用事件驱动模型实现,可以处理大量的文件描述符。epoll用一个epoll_create函数创建一个epoll文件描述符,然后使用epoll_ctl函数向内核注册需要监听的文件描述符和事件类型,最后使用epoll_wait函数等待文件描述符上的事件。epoll最大的优点是可以支持水平触发和边缘触发两种模式,水平触发模式只要有数据可读或可写就会返回一个事件,而边缘触发模式只有当描述符上数据有变化时才会返回一个事件。 综上所述,selectpollepoll都是Linux中用来实现I/O多路复用的函数,它们的主要区别在于使用的数据结构不同,以及性能方面的优化不同。epoll是最高效的I/O多路复用函数,性能比selectpoll要高,并且支持水平触发和边缘触发两种模式。 ### 回答3: selectpollepoll均是Linux下常见的网络编程I/O多路复用技术。这些技术的核心是将多个I/O操作以非阻塞的方式同时处理,从而提高程序的性能。虽然三者都实现了I/O多路复用,但是实现方式却有所区别。 select是最古老的I/O多路复用技术,在大多数操作系统上都有实现。它的操作过程是:将要监视的文件描述符(包括输入和输出)分别存入一个数组中,并调用select函数开始监听,在有文件描述符就绪(有数据可读或者数据可写)的时候,返回一个事件的集合,可以通过遍历fd_set来得到哪些文件可以读/写。缺点是select函数的时间复杂度是O(n),随着监视的文件描述符数量增加,时间复杂度会越来越高,同时select也有FD_SETSIZE的限制(默认是1024)。 poll是在select的基础上进行了改进,可以监视的文件描述符数目不受限制。与select相同的是,都需要调用轮询函数来等待事件,当某个文件描述符就绪时,内核会将就绪的文件存在一个链表中并返回给应用程序。每次都需要遍历整个被监视的描述符集合,找到哪些描述符有数据可读,效率也不是非常理想。 而epoll是为了解决selectpoll的效率低下、不易扩展的问题而设计的,它利用了操作系统内核的支持,并支持边缘触发和水平触发两种工作模式。epoll会将每个文件描述符对应的文件表项都放在一个红黑树中,然后在树中搜索已经就绪的文件来获取就绪的文件列表。与selectpoll的轮询方式不同,epoll是基于事件驱动方式的,当注册的文件描述符已经准备好时,内核会通过事件通知方式来通知应用程序。相比之前的两种方法,epoll的效率更高,也更容易扩展。但是它的实现比较复杂,需要大量调用系统API函数。 总之,selectpollepoll这三种I/O多路复用方式各有优点缺点,不同场景选择不同的方式来处理I/O更加合适。在所监视的文件描述符数量较少时,selectpoll比较合适;而对于应用程序并发性能要求较高的,epoll方式可以获得更好的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值