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
相关产品推荐
相关产品推荐

