I/O多路复用epoll

epoll() 是 Linux 内核提供的高效的 I/O 多路复用机制。相对于 select() 和 poll(),它在处理大量文件描述符时更加高效和稳定,因此在高并发网络编程中得到了广泛应用。
epoll() 的原型定义如下:

#include 

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epf, struct epoll_event *events, int maxevents, int timeout);

其中,各个参数的含义如下:

size:是创建新的 epoll 实例时指定的红黑树节点数量,目前已经没有实际作用;
epfd:是 epoll 实例的描述符;
op:是对 epoll 实例中的文件描述符进行添加、修改或删除的操作类型,包括 EPOLL_CTL_ADD,EPOLL_CTL_MOD 和 EPOLL_CTL_DEL;
fd:是需要添加、修改或删除的文件描述符;
event:是对需要操作的文件描述符对应的事件进行设置;
events:是用于返回已经发生事件的文件描述符的数组;
maxevents:是最多返回的事件数目;
timeout:是等待事件的超时时间,单位是毫秒。

epoll_event 结构体类型用于描述一个文件描述符和需要监听的事件类型。其原型定义如下:

struct epoll_event {
    uint32_t     events;      /* 需要监听的事件类型 */
    epoll_data_t data;        /* 用户数据 */
};

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

其中,events 是需要监听的事件类型,包括 EPOLLIN(可读)、EPOLLOUT(可写)和 EPOLLERR(错误)等;data 是用户可以自定义的数据,可以是文件描述符句柄,也可以是指针等任意用户数据类型。

这三个函数作用分别如下:

epoll_create(int size)
创建一个 epoll 实例,并返回其描述符。参数 size 指定了 epoll 实例中要处理的文件描述符的上限,但现在已经没有实际的用途。

epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
将指定的文件描述符和其关联的事件添加到 epoll 实例中。其中,参数 op 指定了添加操作的类型,有 EPOLL_CTL_ADD,EPOLL_CTL_MOD 和 EPOLL_CTL_DEL 三种操作。epfd 参数是 epoll 实例描述符,fd 参数是要添加或删除的文件描述符,event 参数指定了要监听的事件类型和需要传递的用户数据。

epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
等待指定的文件描述符中的 I/O 事件并返回。其中,epfd 参数是 epoll 实例描述符,events 参数是用于返回已经发生事件的文件描述符及其事件类型的数组,maxevents 参数是 events 数组的最大长度,timeout 参数是等待事件的超时时间。epoll_wait() 函数会阻塞当前线程,直到有事件发生或超时。

总体来说,epoll_create() 函数是创建 epoll 实例,epoll_ctl() 函数是往 epoll 实例中添加、修改或删除文件描述符和事件,epoll_wait() 函数是等待事件并返回已经发生事件的文件描述符和事件类型,这三个函数一起组成了 epoll I/O 多路复用机制的核心。

使用 epoll() 函数的步骤如下:

调用 epoll_create() 函数创建一个 epoll 实例描述符;
构建 epoll_event 结构体,初始化需要监听的文件描述符和对应的 I/O 操作类型(读、写和异常);
调用 epoll_ctl() 函数将需要监听的文件描述符和对应的事件类型添加到 epoll 实例中;
调用 epoll_wait() 函数进行监听;
根据 epoll_wait() 返回的结果和实际发生的事件类型,进行相应的处理。

以下是一个简单的示例程序,演示了如何使用 epoll() 函数等待多个文件描述符上的事件:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_EVENTS 1024
#define BUF_SIZE 1024

int main() {
    // 创建一个 TCP 套接字
    int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_sock < 0) {
        perror("socket");
        exit(1);
    }

    // 将套接字设置为非阻塞模式
    int flags = fcntl(server_sock, F_GETFL, 0);
    fcntl(server_sock, F_SETFL, flags | O_NONBLOCK);

    // 绑定地址和端口
    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr);
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(1);
    }

    // 设置为监听状态,等待连接
    if (listen(server_sock, SOMAXCONN) < 0) {
        perror("listen");
        exit(1);
    }

    // 创建 epoll 实例
    int epollfd = epoll_create1(0);
    if (epollfd < 0) {
        perror("epoll_create");
        exit(1);
    }

    // 添加监听事件
    struct epoll_event event, events[MAX_EVENTS];
    event.data.fd = server_sock;
    event.events = EPOLLIN | EPOLLET;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_sock, &event)) {
        perror("epoll_ctl");
        exit(1);
    }

    while (true) {
        // 等待事件
        int num_events = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (num_events == -1) {
            perror("epoll_wait");
            exit(1);
        }

        // 处理事件
        for (int i = 0; i < num_events; ++i) {
            if (events[i].data.fd == server_sock) {
                // 有新客户端连接
                int client_sock = accept4(server_sock, nullptr, nullptr, SOCK_NONBLOCK);
                if (client_sock == -1) {
                    perror("accept");
                    exit(1);
                }

                // 将新客户端添加到监听事件中
                event.data.fd = client_sock;
                event.events = EPOLLIN | EPOLLET;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sock, &event) == -1) {
                    perror("epoll_ctl");
                    exit(1);
                }

                std::cout << "New connection: " << client_sock << std::endl;

            } else {
                // 有客户端发送数据
                int fd = events[i].data.fd;
                char buf[BUF_SIZE] = {0};
                int len = recv(fd, buf, BUF_SIZE, 0);
                if (len <= 0) {
                    close(fd);
                    std::cout << "Connection closed: " << fd << std::endl;
                    continue;
                }

                std::cout << "Receive message from client " << fd << ": " << buf << std::endl;
            }
        }
    }

    close(server_sock);
    return 0;
}

在这个例子中,我们首先创建一个 TCP 套接字,并将其设置为非阻塞模式。然后,我们绑定了一个地址和端口,并通过 listen() 函数将其设置为监听状态。

接下来,我们创建了一个 epoll 实例,并通过 epoll_ctl() 函数将需要监听的文件描述符添加到 epoll 实例中。在事件循环中,我们使用 epoll_wait() 函数等待事件,并根据事件类型进行相应的处理。如果是服务器套接字的请求,我们接受新客户端连接,并将其添加到监听事件中。如果是某个客户端的数据到达,我们接收数据并进行处理。

值得注意的是,由于我们将套接字设置为非阻塞模式,因此当没有新数据到达时,recv() 函数可能会返回 -1(表示错误)或 0(表示对端已关闭连接)。在这种情况下,我们需要关闭套接字并继续处理下一个事件。

服务端和客户端示例代码

server

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXEVENTS 100

// 把socket设置为非阻塞的方式。
int setnonblocking(int sockfd);

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

int main(int argc,char *argv[])
{
  if (argc != 2)
  {
    printf("usage:./tcpepoll 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("accept() 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(socket=%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(eventfd=%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;  
}  

client

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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,"1111111111111111111111ii=%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;
  }
} 

I/O多路复用epoll_第1张图片

你可能感兴趣的:(c++,网络,linux)