You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EPOLLET模式epoll异步服务器:EPOLLOUT使用及相关问题咨询

问题解答

1. EPOLLOUT监听方案的正确性

你的方案是标准且正确的,这是epoll边缘触发模式下处理大文件发送的常规实践:

  • 当send返回EAGAIN/EWOULDBLOCK时,内核发送缓冲区已满,此时不应循环重试(会空转浪费CPU),而是通过epoll_ctl给该socket添加EPOLLOUT事件监听,退出当前发送循环。
  • 当epoll_wait触发EPOLLOUT事件时,说明内核缓冲区有可用空间,此时继续发送剩余数据。
  • 发送完成后必须通过epoll_ctl移除EPOLLOUT标记,避免epoll持续触发不必要的事件(边缘触发下,缓冲区只要非空就会触发一次,若不移除,后续可能因其他原因反复触发)。

注意:边缘触发模式下,触发EPOLLOUT后要尽可能多发送数据,直到再次返回EAGAIN或数据全部发送完毕,避免遗漏发送机会。

2. 客户端独立缓冲区的内存优化与生产策略

全局复用缓冲区的做法在并发场景下完全不可行,会导致多客户端数据互相覆盖。每个客户端维护独立未发送数据是必要的,但可通过以下方式优化内存效率:

  • 零拷贝发送(核心优化):使用sendfile()系统调用,直接将文件描述符的数据发送到socket,无需将文件内容读到用户态缓冲区,彻底避免用户态内存占用,同时减少内核态到用户态的拷贝开销。这是Nginx、Apache等生产级服务器处理静态文件的核心方案。
  • 动态缓冲区管理:不为每个客户端预先分配固定大小的缓冲区,而是根据实际未发送数据量动态分配内存,发送完成后立即释放;或使用内存池复用缓冲区,减少频繁malloc/free的开销和内存碎片。
  • 分块传输与流量控制:对于超大文件,采用HTTP分块传输(Transfer-Encoding: chunked),每次发送一小段数据,避免一次性占用大量内存;同时结合TCP窗口大小做流量控制,匹配客户端接收能力。
  • 连接与资源限制:合理设置最大并发连接数,对单个客户端的最大响应大小做限制,避免极端场景下内存过载。

3. Wireshark出现HTTP/1.1 200 OK (text/css)Continuation的原因

这是TCP分段传输的正常现象,和epoll异步发送的实现方式直接相关:

  • 当发送的数据量超过TCP的MSS(最大分段大小,通常为1460字节左右)时,内核会将数据拆分成多个TCP段发送。Wireshark会将第一个TCP段解析为HTTP响应头,后续的TCP段因为没有完整的HTTP头,会标记为Continuation,表示这是前一个HTTP响应的后续数据。
  • 普通非阻塞socket场景下,你可能使用了阻塞式send或者一次性将全部数据写入内核缓冲区(内核自动完成分段),应用层感知不到分段;而epoll异步发送时,你分多次调用send(每次发送部分数据),每次send对应一个或多个TCP段,Wireshark会明确标记后续的分段为Continuation。
  • 另外,HTTP/1.1默认开启持久连接(Connection: keep-alive),多个响应可能在同一个TCP连接上传输,后续的响应数据也可能被标记为Continuation,这完全符合HTTP和TCP协议规范,并非错误。

附参考代码

结构体定义

#define RECV_SIZE 2048
typedef struct server_ds {
  int sockfd;
  struct sockaddr_in info;
  char *reply;
} server_t;

server_t server;

事件循环代码

for (;;) {
    // printf("before wait\n");
    nfds = epoll_wait(event->epollfd, event->events, MAX_EVENTS, -1);
    // printf("after wait\n");

    if (nfds == -1) {
      perror("epoll_wait()");
      return -1;
    }

    for (int i = 0; i < nfds; i++) {
      if (event->events[i].events == EPOLLHUP) {
        fprintf(stderr, "got EPOLLUP\n");
      }
      if (event->events[i].events == EPOLLERR) {
        fprintf(stderr, "got EPOLLERR\n");
      }
      if (event->events[i].data.fd == server.sockfd &&
          event->events[i].events & EPOLLIN) {
        // we recieved a new connection on the socket that the server is
        // listening on accept all the incoming new connections
        while (1) {
          memset(&in_addr, 0, sizeof(in_addr));
          in_len = sizeof(in_addr);

          int connfd =
              accept(server.sockfd, (struct sockaddr *)&in_addr, &in_len);

          if (connfd == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
              // we have accepted all the connections that we could
              // no more connection, break out of this loop
              break;
            }
            perror("accept()");
            break;
          }

          printf("New client %d connection!\n", connfd);
          if (make_socket_nonblocking(connfd) == -1) {
            continue;
          }

          event->ev.events = EPOLLIN | EPOLLET | EPOLLOUT ;
          event->ev.data.fd = connfd;

          if (epoll_ctl(event->epollfd, EPOLL_CTL_ADD, connfd, &event->ev) <
              0) {
            fprintf(stderr, "epoll set insertion error: fd=%d", connfd);
            close(connfd);
            continue;
          }
        }

      // }else if (event->events[i].events & EPOLLOUT ){
      //     printf("recieved : EPOLLOUT \n");
      //     event->events[i].events = EPOLLIN | EPOLLET;
      //     if( epoll_ctl(event->epollfd,EPOLL_CTL_MOD,event->events[i].data.fd,&event->events[i]) < 0 ){
      //       perror("epoll_ctl()");
            
      //     }
      }else if (event->events[i].events & EPOLLIN) {
        // client has send some data to the socket
        // recieve all the data at once
        int client_fd = event->events[i].data.fd;
        char buffer[RECV_SIZE];
        int client_closed = 0;

        while (1) {
          char *ptr = buffer;
          ssize_t bytes = recv(client_fd, ptr, RECV_SIZE - 1, 0);
          if (bytes == 0) {
            // client has closed the connection
            // close the client socket and remove from the epoll_loop_t
            printf("Client %d closed connection\n", client_fd);
            client_closed = 1;
            close(client_fd);
            break;
          } else if (bytes == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
              // we have read all we could
              // break from the while loop
              break;
            }
            perror("recv()");
            break;
          } else {
            ptr[bytes] = '\0';
            ptr += bytes;
          }
        }
        // if client has not closed the socket
        // only then send the reply
        //printf("client closed var : %d\n", client_closed);
        if (!client_closed) {
          http_t request;
          // parse_request returns -1 when the request is something other than
          // GET request. In that case we send the default NOT implemented
          // message to the client
          if (parse_request(buffer, strlen(buffer), &request) == -1) {
            // send_all returns -1 only when send() call returns with EAGAIN or
            // EWOULDBLOCK this indicates that the send buffers are full retry
            // it again later
            if (send_all(client_fd, strlen(not_implemented_reply),
                         not_implemented_reply) == -1) {
              // for now continue
              // to be implemented later
              continue;
            }
          }

          printf("sending response...\n");
          send_response(&request, client_fd);
          printf("sent OK reply\n");

          // free the server reply which was created using malloc
          if (server.reply != NULL) {
            free(server.reply);
            server.reply = NULL;
          }
        }
      }
    }
  }

send_all()函数

int send_all(int sockfd, size_t len, const char *reply) {
  ssize_t bytes = 0;
  int send_size = 4028;
  size_t total_sent = 0;
  // printf("file size: %ld\n", len);
  while (len) {
    bytes = send(sockfd, reply, len, 0);
    printf("b sent : %ld\n",bytes);
    if (bytes == -1) {
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // the send() buffer is full, retry it later
        // printf("got EAGAIN\n");
        continue;
      }
    }
    total_sent += bytes;
    reply += bytes;
    len -= bytes;
  }
  printf("total file size sent : %ld\n", total_sent);
  return 0;
}

内容的提问来源于stack exchange,提问作者Cardinal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 12:37:35