MST
星途 面试题库

面试题:C语言非阻塞I/O与事件驱动结合的场景应用

假设你正在开发一个网络服务器,使用C语言和Linux系统,阐述如何将非阻塞I/O与事件驱动模型相结合来高效处理多个客户端连接,并且说明这样做相较于传统阻塞I/O模型的优势在哪里。
35.4万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

1. 非阻塞I/O与事件驱动模型结合实现多客户端连接处理

  1. 设置非阻塞套接字: 在创建套接字后,使用fcntl函数将套接字设置为非阻塞模式。例如:
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    
  2. 事件驱动模型 - 使用epoll(以epoll为例,selectpoll也类似原理)
    • 创建epoll实例
      int epollfd = epoll_create1(0);
      if (epollfd == -1) {
          perror("epoll_create1");
          exit(EXIT_FAILURE);
      }
      
    • 添加监听套接字到epoll实例
      struct epoll_event event;
      event.data.fd = sockfd;
      event.events = EPOLLIN | EPOLLET; // 边缘触发模式,也可以用水平触发EPOLLIN
      if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
          perror("epoll_ctl: listen_sock");
          exit(EXIT_FAILURE);
      }
      
    • 事件循环
      struct epoll_event events[10]; // 一次最多处理10个事件
      while (1) {
          int n = epoll_wait(epollfd, events, 10, -1);
          for (int i = 0; i < n; ++i) {
              if (events[i].data.fd == sockfd) {
                  // 处理新连接
                  struct sockaddr_in client_addr;
                  socklen_t client_addr_len = sizeof(client_addr);
                  int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
                  if (clientfd == -1) {
                      if (errno != EAGAIN && errno != EWOULDBLOCK) {
                          perror("accept");
                      }
                      continue;
                  }
                  // 将新客户端套接字设置为非阻塞
                  int client_flags = fcntl(clientfd, F_GETFL, 0);
                  fcntl(clientfd, F_SETFL, client_flags | O_NONBLOCK);
                  // 将新客户端套接字添加到epoll实例
                  event.data.fd = clientfd;
                  event.events = EPOLLIN | EPOLLET;
                  if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &event) == -1) {
                      perror("epoll_ctl: client_sock");
                      close(clientfd);
                  }
              } else {
                  // 处理客户端数据
                  int clientfd = events[i].data.fd;
                  char buffer[1024];
                  ssize_t nread = read(clientfd, buffer, sizeof(buffer));
                  if (nread == -1) {
                      if (errno != EAGAIN && errno != EWOULDBLOCK) {
                          perror("read");
                          // 错误处理,例如从epoll中移除并关闭套接字
                          epoll_ctl(epollfd, EPOLL_CTL_DEL, clientfd, NULL);
                          close(clientfd);
                      }
                  } else if (nread == 0) {
                      // 客户端关闭连接
                      epoll_ctl(epollfd, EPOLL_CTL_DEL, clientfd, NULL);
                      close(clientfd);
                  } else {
                      // 处理读取到的数据
                      // 例如回显数据
                      write(clientfd, buffer, nread);
                  }
              }
          }
      }
      

2. 优势

  1. 提高并发性
    • 传统阻塞I/O模型在处理一个客户端连接时,若该客户端没有数据发送,服务器会阻塞在readaccept等函数上,无法处理其他客户端连接。而非阻塞I/O与事件驱动模型结合,服务器可以在等待数据到达时,继续处理其他有事件发生的客户端连接,大大提高了服务器处理多个并发连接的能力。
  2. 资源利用更高效
    • 传统阻塞I/O模型可能会因为阻塞而浪费大量CPU时间片。在非阻塞I/O与事件驱动模型下,只有当有事件发生时(如可读、可写等),才会进行相应处理,CPU资源不会被无效地阻塞等待,提高了资源利用率。
  3. 响应更及时
    • 对于多个客户端连接,非阻塞I/O与事件驱动模型能够及时响应每个客户端的事件,不会因为某个客户端的长时间无操作而影响其他客户端的交互。例如,当一个客户端发送数据时,服务器能迅速检测到并进行处理,而不像传统阻塞I/O模型那样,可能会因为其他客户端的阻塞操作而延迟响应。