用epoll实现高并发I/O的核心是将socket设为非阻塞并配合epoll_create1、epoll_ctl和epoll_wait构建事件驱动循环;需用RAII封装epoll fd,监听socket注册EPOLLIN,客户端socket注册EPOLLIN|EPOLLET,并循环recv直至EAGAIN/EWOULDBLOCK。

用 epoll 实现高并发 I/O,在 C++ 中核心是把 socket 设置为非阻塞模式,配合 epoll_create、epoll_ctl 和 epoll_wait 构建事件驱动循环,避免为每个连接开线程或进程。
创建和管理 epoll 实例
调用 epoll_create1(0) 创建一个 epoll 实例,返回的 fd 是后续操作的句柄。建议用 RAII 封装,确保析构时自动 close()。注意不要重复创建多个 epoll 实例——一个线程一个就够了,多线程可考虑每个线程独立 epoll + 独立事件循环,或用主从 reactor 模式。
- 创建后立即设置为
EPOLL_CLOEXEC,防止 fork 后意外继承 - 监听 socket(如 server socket)需先
bind+listen,再用epoll_ctl(EPOLL_CTL_ADD)注册EPOLLIN - 对新 accept 的 client socket,同样要设为非阻塞,并注册
EPOLLIN | EPOLLET(边缘触发更高效)
非阻塞 socket 与边缘触发(ET)配合
ET 模式下,epoll 只在 fd 状态变化时通知一次,必须一次性读完所有可用数据(直到 recv 返回 EAGAIN 或 EWOULDBLOCK),否则可能丢事件。因此每个 socket 对应的缓冲区要自己管理,不能依赖一次 read 就清空。
- 用
fcntl(fd, F_SETFL, O_NONBLOCK)设置非阻塞 - 读数据时循环
recv,检查返回值:>0 正常,==0 表示对端关闭,errno == EAGAIN 才算读完 - 写数据若一次没发完,需把剩余数据缓存,并注册
EPOLLOUT,等可写时继续发送,发完再删掉EPOLLOUT
事件分发与连接生命周期管理
每次 epoll_wait 返回一批就绪事件,需遍历处理。关键是要把 fd 和业务逻辑(如连接对象、读写缓冲区、状态机)绑定起来。推荐用 std::unordered_map 或更安全的 std::map(fd 为 key),避免裸指针悬挂。
立即学习“C++免费学习笔记(深入)”;
- accept 到新连接,new 一个 Connection 对象,存入 map,同时注册到 epoll
- 收到
EPOLLIN,调用 Connection::handle_read();收到EPOLLOUT,调用 handle_write() - 出错或对端关闭时,主动
epoll_ctl(EPOLL_CTL_DEL),close fd,从 map 移除并 delete 对象 - 避免在回调中直接 erase map 元素——先标记待删除,循环结束后统一清理,防止迭代器失效
性能与健壮性要点
epoll 本身不解决粘包、协议解析、超时、内存泄漏等问题,这些都得自己补全。一个稳定的服务至少要:
- 实现心跳或 read timeout:记录每个连接最后活跃时间,定期扫描超时连接
- 限制单连接最大缓冲区大小,防内存耗尽;小包可合并收,大包需分片处理
- 错误日志打全:
errno、fd、当前状态,方便定位ECONNRESET、EMFILE等问题 - 编译加
-O2 -D_GLIBCXX_DEBUG(调试期);生产环境用SO_REUSEPORT配合多进程提升吞吐











