如何为基于边缘触发epoll()的C语言HTTP服务器实现长连接?
边缘触发epoll实现连接超时管理方案
问题场景
我基于边缘触发的epoll实现服务器,需要实现以下逻辑:
process_request(client); client_timestamp = curr_time; // 执行其他操作,可能再次调用process_request(client)并更新时间戳 when (curr_time - client_timestamp > TIMEOUT) close(client);
但存在两个限制:
- 不能用
sleep(),避免阻塞影响性能 - 不能为每个连接单独开线程,防止连接过多时耗尽线程资源
核心解决方案:统一定时器集合+epoll_wait超时控制
不用额外线程或sleep,而是把所有连接的超时事件集中管理,结合epoll的超时等待机制,让epoll_wait要么在有IO事件时返回,要么在到了最早超时时间时返回,一次性处理所有超时和IO事件。
具体实现步骤
- 为每个连接维护活跃时间戳:给每个客户端连接结构体添加
last_active字段,每次调用process_request后更新该时间戳。 - 用有序结构管理超时队列:把所有连接按「超时时间(last_active + TIMEOUT)」排序,比如用最小堆、红黑树或有序链表,快速找到最早要超时的连接。
- 动态设置epoll_wait的超时参数:每次epoll_wait前,计算当前最早超时时间与当前时间的差值,作为epoll_wait的超时参数。这样epoll不会无意义阻塞,一旦到了最早超时时间就会返回。
- 先处理超时,再处理IO事件:epoll_wait返回后,先遍历超时队列,关闭所有超时的连接并从epoll和队列中移除;再处理触发的IO事件,更新对应连接的时间戳并调整其在超时队列中的位置。
代码示例(简化版)
定义客户端结构体与超时链表
#include <sys/epoll.h> #include <time.h> #include <stdlib.h> #include <unistd.h> #define TIMEOUT 300 // 超时时间,单位:秒(示例为5分钟) #define MAX_EVENTS 1024 // 客户端连接结构体 typedef struct client { int fd; time_t last_active; // 最后活跃时间戳 // 其他连接相关数据:比如接收缓冲区、请求状态等 } client_t; // 超时链表节点(按last_active+TIMEOUT升序排列) typedef struct timeout_node { client_t *client; struct timeout_node *next; } timeout_node_t; timeout_node_t *timeout_head = NULL; int listen_fd; // 监听套接字,需提前初始化
辅助函数:插入/移除超时链表节点
// 将客户端插入超时链表的正确位置,保持链表按超时时间升序 void insert_to_timeout_list(client_t *c) { timeout_node_t *new_node = malloc(sizeof(timeout_node_t)); new_node->client = c; new_node->next = NULL; if (timeout_head == NULL) { timeout_head = new_node; return; } time_t new_timeout = c->last_active + TIMEOUT; // 插入到头部 if (new_timeout < timeout_head->client->last_active + TIMEOUT) { new_node->next = timeout_head; timeout_head = new_node; return; } // 遍历找到插入位置 timeout_node_t *curr = timeout_head; while (curr->next != NULL && curr->next->client->last_active + TIMEOUT <= new_timeout) { curr = curr->next; } new_node->next = curr->next; curr->next = new_node; } // 从超时链表中移除指定客户端 void remove_from_timeout_list(client_t *c) { if (timeout_head == NULL) return; timeout_node_t *prev = NULL; timeout_node_t *curr = timeout_head; while (curr != NULL && curr->client != c) { prev = curr; curr = curr->next; } if (curr == NULL) return; if (prev == NULL) { timeout_head = curr->next; } else { prev->next = curr->next; } free(curr); } // 根据文件描述符查找客户端(需自己实现,比如用哈希表) client_t *find_client_by_fd(int fd) { // 示例:实际可通过哈希表快速查找,这里简化返回 timeout_node_t *curr = timeout_head; while (curr != NULL) { if (curr->client->fd == fd) { return curr->client; } curr = curr->next; } return NULL; }
主事件循环
int main() { int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } // 注册监听套接字到epoll(边缘触发) struct epoll_event listen_ev; listen_ev.events = EPOLLIN | EPOLLET; listen_ev.data.fd = listen_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_ev) == -1) { perror("epoll_ctl add listen_fd"); exit(EXIT_FAILURE); } struct epoll_event events[MAX_EVENTS]; while (1) { time_t now = time(NULL); int wait_ms = -1; // 计算epoll_wait的超时时间 if (timeout_head != NULL) { time_t earliest_timeout = timeout_head->client->last_active + TIMEOUT; if (earliest_timeout > now) { wait_ms = (int)(earliest_timeout - now) * 1000; // 转毫秒 } else { wait_ms = 0; // 已有超时连接,立即返回处理 } } // 等待IO事件或超时 int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, wait_ms); if (nfds == -1) { perror("epoll_wait"); continue; } // 第一步:处理所有超时连接 now = time(NULL); while (timeout_head != NULL) { client_t *c = timeout_head->client; if (now - c->last_active > TIMEOUT) { // 超时,关闭连接 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, c->fd, NULL); close(c->fd); // 从链表移除并释放资源 timeout_node_t *tmp = timeout_head; timeout_head = timeout_head->next; free(tmp->client); free(tmp); } else { // 后续连接都未超时,退出循环 break; } } // 第二步:处理IO事件 for (int i = 0; i < nfds; i++) { if (events[i].events & EPOLLIN) { int fd = events[i].data.fd; if (fd == listen_fd) { // 处理新连接 int client_fd = accept(listen_fd, NULL, NULL); if (client_fd == -1) { perror("accept"); continue; } client_t *new_client = malloc(sizeof(client_t)); new_client->fd = client_fd; new_client->last_active = now; // 注册客户端套接字到epoll(边缘触发) struct epoll_event client_ev; client_ev.events = EPOLLIN | EPOLLET; client_ev.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_ev) == -1) { perror("epoll_ctl add client_fd"); close(client_fd); free(new_client); continue; } // 插入超时链表 insert_to_timeout_list(new_client); } else { // 处理客户端请求 client_t *c = find_client_by_fd(fd); if (c == NULL) continue; // 处理请求(边缘触发需一次性读完数据) int ret = process_request(c); if (ret == 0) { // 请求处理成功,更新活跃时间戳 remove_from_timeout_list(c); c->last_active = now; insert_to_timeout_list(c); } else { // 处理失败或客户端断开,关闭连接 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, c->fd, NULL); close(c->fd); remove_from_timeout_list(c); free(c); } } } // 可扩展处理EPOLLOUT等其他事件 } } return 0; }
优化建议
- 替换有序链表为高效数据结构:示例中的链表插入/删除是O(n)复杂度,高并发场景下建议用最小堆(O(logn))或红黑树,甚至可以用Linux内核的
rbtree实现。 - 结合timerfd简化定时器管理:创建
timerfd并加入epoll,每次计算最早超时时间后调用timerfd_settime更新定时器,当timerfd触发可读事件时,批量处理所有超时连接,无需自己维护超时队列。 - 用哈希表快速查找客户端:示例中
find_client_by_fd用链表遍历效率低,实际可通过哈希表(比如uthash库)实现O(1)复杂度的查找。
内容的提问来源于stack exchange,提问作者Sgg8
相关产品推荐
相关产品推荐

