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

2023-11-15

学习计算机网络编程

一、思路和学习方法

  本文学习于: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++ 网络编程基础就学习完成了,后面学习数据库内容。继续学习,继续理解,争取融会贯通。近期面了小米的嵌入式软件工程师岗,第一次技术面1个小时答的比较好,第二次技术面半个小时,算法题做的不太好,可能是急缺吧,HR口头让我过了,让我考虑是否去小米然后再给我一份正式书面 offer。第二天我让 HR 给我一份书面 offer,他也说可以的,结果现在也没等到,估计是被备胎了,但是我问什么时候给正式书面 offer,过了两天HR也没给回复,感觉不太爽,一点回应都没有就被鸽了。现在考虑先待在海康威视,后面继续学习再找下家。最近也去参加了国考,感觉题目类型很固定,这个也打算继续准备准备,可能以后会用到行测和申论知识。继续加油!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

学习C++项目——select模型,poll模型和epoll模型 的相关文章

随机推荐

  • 凌乱的日期

    import java util ArrayList import java util Arrays import java util Collections import java util HashSet import java uti
  • ubuntu安装驱动 USB转转串口 232串口 PL2303 DB9

    ubuntu安装驱动 USB转串口 RS232串口 PL2303 DB9 工具 安装软件minicom minicom功能快捷键 minicom的使用 cutecom软件安装 cutecom使用 工具 因为我使用的是rs232串口设备芯片为
  • 6.Docker容器底层实现了解与安全机制

    原文地址 点击直达 文章目录 0x00 底层实现 基本架构 简述I O设备 1 命名空间 2 控制组 子系统之Devices 子系统之cpuset 子系统之cpu 子系统之cpuacct 子系统之memory 子系统之blki
  • 一看就懂TCP/IP协议族全析

    TCP IP协议族 计算机与网络设备要相互通信 双方就必须基于相同的方法 如何探测到通信目标 由那一边现发起通信 使用哪种语言 使用那种操作系统 怎样结束通信等都需要事先确定 不同的硬件 操作系统之间的相互通信 所有的这一切都需要一种规则
  • QDialog、QWidget、QMainWindow的区别

    QDialog QWidget QMainWindow的区别 dialog有exec函数 如果是dialog窗口 后边的窗口是不可选的 widget和dialog都有show函数 如果通过这个函数显示这两种类型的窗口 则两个窗口都是可选的
  • 国际带宽市场的发展趋势和前景

    随着全球互联网的不断普及和云计算 人工智能等新技术的迅猛发展 国际带宽市场在不断扩大 成为了全球信息通信领域的重要组成部分 本文将就国际带宽市场的发展趋势和前景进行探讨 一 市场概况 国际带宽是指一个国家或地区连接国际互联网所拥有的带宽资源
  • VMware Workstation Pro 16.2.4安装教程

    VMware Workstation Pro 16 2 4 安装教程 文章目录 VMware Workstation Pro 16 2 4 安装教程 下载最新版VMware 双击打开 自定义安装位置 把 去掉 点击下一步 等待安装 点击许可
  • 主从服务器 共用文件,Docker+keepalived+nginx实现主从热备的方法示例

    前言 为解决单点故障 我们需要配置主从热备方案 服务器数量有限 故使用Docker模拟安装配置 本次配置默认已经安装了Docker 配置环境 centos7 64位 docker版本 Docker version 17 12 1 ce bu
  • Apache Storm之集群安装

    1 环境 storm 1 2 1 Java 7 storm 1 x对java 7 和 java 8均进行了测试 Pytho 2 6 6 Python 3 x理论上也可以 但是并不作为storm的测试 以上是storm的环境 如果不匹配的ja
  • python-异常处理汇总

    1 需求 当程序遇到问题时 不让程序结束 而越过错误继续向下执行 格式 try 语句t except 错误码 语句1 except 错误码 语句2 except 错误码 语句3 except 错误码 语句n else 语句e 注意 else
  • 数字IC秋招面试专题(二)verilog的signed和unsigned

    数字IC秋招面试专题 二 verilog的signed和unsigned 前言 一 右值按signed还是unsigned 二 signed的自动扩位 三 系统函数 signed 和 unsigned 总结 声明 未经作者允许 禁止转载 推
  • ​【UI界面】Foobar2000 FlatLite 整合版

    Foobar2000 是一款本地音乐播放器 这里我就不再做介绍了 不懂的请自行了解 此 Foobar2000 FlatLite 主题包为整合版本 下载既可用 主题界面是模块化的 JS 面板 每个面板都是独立的 面板里的图标也是由 JS 脚本
  • JAVA字符串的两种定义方式的区别

    关于JAVA中两种字符串定义方式的区别 第一次写 就当复习总结一下 希望能帮到需要的人吧 我们知道在JAVA中 对于字符串的实例化方式有两种 直接赋值 String str Hello World 构造方法实例化 String str ne
  • 编写dylib_如何使用第三方的dylib

    展开全部 所谓dylib 就是bsd风格的动态库 基本可以认为等价于e5a48de588b63231313335323631343130323136353331333339663331windows的dll和linux的so mac基于bs
  • car

    本篇介绍如何检验回归结果是否符合模型假设 以及样本中是否存在异常点 本篇使用的主要工具包是car 包名是Companion to Applied Regression的缩写 该包提供了许多用于模型检验的函数 初始模型如下 library c
  • HTML快速参考指南

    目录 HTML 基础文档 文本元素 逻辑样式 物理样式 链接 锚 图像元素 无序列表 有序列表 定义列表 表格 框架 表单 实体 其他元素 HTML 基础文档 Visible text goes here 文本元素 p This is a
  • windows下编写shell脚本完美解决换行问题$'\r': command not found

    前面已经写过另外一边关于解决编写shell脚本换行的问题 即在Linux下执行命令 sed i s r g 文件名称 比如 sed i s r g host1 sh 今天是另外一种方式解决 使用ultraEdit软件编写shell脚本 新建
  • 【mysql】出现 slow sql 问题及建议

    文章目录 1 SQL 执行什么情况下会变慢 2 影响 SQL 语句执行效率的主要因素有哪些 3 慢 SQL 是如何拖垮数据库的 4 最佳实践建议 1 SQL 执行什么情况下会变慢 数据量增加 数据库中的数据量可能会逐渐增加 导致查询变慢 查
  • Python 常用写法

    时间 1 now import datetime now datetime now nowStr now strftime now Y m d H M S list 1 分割最后一个 arr str rsplit 1 2 数组 加索引 en
  • 学习C++项目——select模型,poll模型和epoll模型

    学习计算机网络编程 一 思路和学习方法 本文学习于 C语言技术网 www freecplus net 在 b 站学习于 C 语言技术网 并加以自己的一些理解和复现 如有侵权会删除 接下来应该是网络编程部分最难也是最常用的部分 同时在这一章我