epoll+et是高性能网络编程基础,但需配合非阻塞socket、低水位调优、用户态缓冲区管理、内存池及连接级背压控制,否则无法应对慢客户端导致的内存耗尽与事件循环阻塞。

为什么 epoll + ET 模式是基础,但光用它不够
单纯靠 epoll 边缘触发(EPOLLET)能提升单连接吞吐,但无法解决背压——当客户端消费慢、接收缓冲区积压时,服务器仍会持续读、持续发,最终耗尽内存或触发 send() 阻塞/失败。关键不是“能不能收发”,而是“要不要现在收发”。
- 必须配合 socket 的
SOCK_NONBLOCK和SO_SNDLOWAT/SO_RCVLOWAT调优,否则低水位默认值(通常 1 字节)会让epoll过早就绪 -
EPOLLET下,一次read()必须循环到EAGAIN,否则漏数据;一次write()若返回EAGAIN,得立刻停发、注册EPOLLOUT,等可写再续 - 别在
epoll_wait()外部做任何阻塞操作(比如同步日志、磁盘 IO),否则整个事件循环卡住
write() 返回 EAGAIN 后怎么安全挂起连接
这不是简单把连接扔进等待队列就行。核心是:挂起时必须确保已写数据全部进入内核发送缓冲区,且下次可写事件到来前,不能重复注册 EPOLLOUT,也不能遗漏重试逻辑。
- 每次
write()前检查连接的输出缓冲区(用户态 buffer)是否为空;非空则说明上次没发完,直接跳过本次 write 尝试 - 收到
EAGAIN后,把待发数据追加进该连接的output_buffer(需自己维护,不能依赖 TCP 缓冲区),然后调用epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev),其中ev.events = EPOLLIN | EPOLLOUT | EPOLLET - 务必在
EPOLLOUT就绪回调里清空output_buffer并尝试重发;发完后要主动移除EPOLLOUT(用EPOLL_CTL_MOD改回EPOLLIN | EPOLLET),否则会持续触发空就绪
连接数暴涨时,内存分配和连接管理怎么不拖垮吞吐
每连接一个 input_buffer + output_buffer,若用 std::vector 或 new 动态分配,百万连接下 malloc 频次和碎片会吃掉大量 CPU。背压机制本身也依赖快速判断“谁堵了”,不能遍历所有连接。
- 用内存池预分配固定大小的 buffer(如 4KB/8KB),按需切片,避免每次 new/delete;释放时不归还系统,只放回池中
- 连接对象(
struct Connection)建议用std::vector或boost::container::static_vector存储,避免指针跳转;成员变量排布按访问频次从高到低(如fd、state、input_size放前面) - 不要为每个连接单独开线程或协程;用单线程 event loop +
epoll是底线;若真需要多核,用进程间负载分片(如 SO_REUSEPORT),而非线程间共享连接状态
背压信号怎么传给业务逻辑层
业务代码(比如协议解析、DB 查询回调)如果不知道当前连接已拥塞,可能继续往 output_buffer 塞数据,导致缓冲区无限膨胀。不能靠全局开关,得让每个连接自己说话。
立即学习“C++免费学习笔记(深入)”;
- 在连接结构体里加
bool write_blocked{false},每次write()返回EAGAIN时置 true,发完清空 buffer 后置 false - 对外暴露
can_write(Connection* conn)接口,业务层调用前先查这个 flag;或者更彻底:只提供try_write(conn, data),内部自动判断并缓存 - 警惕“假成功”:即使
write()返回 >0,也不代表全发出去了——TCP 只保证进了内核缓冲区,不代表对方收到了。真正的背压终点是对方 ACK 滞后,但这层一般不感知,所以重点守住本机发送缓冲区水位
真正难的不是实现某个函数,而是所有连接共用一套事件循环时,任意一个慢连接的 output_buffer 膨胀,都会悄悄吃掉其他连接的内存配额。buffer 大小、回收时机、event loop 响应延迟,三者得一起调。








