C++ 网络编程怎么实现?socket/epoll/多路复用详解
C++ 网络编程的核心是套接字(socket)加 I/O 多路复用。TCP 服务端流程:socket() 创建监听套接字 → bind() 绑定地址端口 → listen() 开始监听 → accept() 取出已连接套接字 → recv()/send() 读写数据。UDP 不需要 listen/accept,直接 recvfrom()/sendto() 收发数据报。当并发连接数上来后,需要 I/O 多路复用:select 用位图监控 fd 集合,每次调用都要重新构建,有 1024 个 fd 的上限;poll 用数组替代位图,没有数量限制,但仍然是 O(n) 遍历;epoll 是 Linux 专属方案,内核维护就绪队列,epoll_wait 只返回就绪的 fd,O(1) 复杂度,配合 EPOLLET 边缘触发模式可以进一步减少系统调用次数。生产环境几乎都选 epoll(Linux)或 kqueue(macOS/FreeBSD)。
追问
select、poll、epoll 各自适用什么场景?
select 的优势是跨平台,连接数少于 100 且短连接为主时够用。poll 去掉了 1024 限制,但和 select 一样每次调用都要把全部 fd 从用户态拷贝到内核态,fd 数量到几千时性能明显下降。epoll 适合 C10K 甚至 C100K 场景:epoll_ctl 只在 fd 增删时调用一次,epoll_wait 只拷贝就绪 fd,不遍历全部。但 epoll 是 Linux 专有 API,跨平台项目用 Boost.Asio 或 libuv 封装层更好。
LT(水平触发)和 ET(边缘触发)有什么区别?
LT 模式下,只要 socket 上有数据没读完,epoll_wait 每次都会返回该 fd——你可以每次只读一部分,下次继续。ET 模式只在 socket 状态变化时通知一次,如果没读完数据,下次 epoll_wait 不会再返回,必须循环 recv 直到 EAGAIN。ET 模式效率更高(减少 epoll_wait 调用次数),但编程难度大:必须设非阻塞 + 循环读到底,漏读就会丢数据。Nginx 用 ET 模式,Redis 用 LT 模式。
非阻塞 I/O 为什么要配合多路复用?
单独设 O_NONBLOCK 后,recv 没数据时立刻返回 EAGAIN 而不是阻塞等待。但你不该用忙轮询来等数据——这会 100% 占满 CPU。多路复用的作用就是替你等待:epoll_wait 阻塞直到某个 fd 有数据可读,然后你调一次 recv 就能拿到数据。非阻塞 + epoll 的组合意义在于:epoll 通知你数据到了,但 recv 可能只读了一部分,非阻塞模式下你可以安全地循环读,不会因为没有更多数据而卡住。
线程池模型怎么设计比较合理?
常见模型:主线程 epoll_wait 检测到新连接,把已连接 fd 分发给工作线程处理。更好的方案是 One Loop Per Thread(muduo 模型):每个线程各自跑一个 epoll 循环,主线程 accept 后通过 eventfd 唤醒某个工作线程注册该 fd,线程间无共享 fd 集合,扩展性更好。线程数一般设为 CPU 核心数,I/O 密集型可以翻倍。