当前位置: 首页 > backend >正文

基于 epoll 的高并发服务器原理与实现(对比 select 和 poll)

在 Linux 网络编程中,我们经常会遇到一个问题:如何同时管理大量客户端的连接?
如果你只用 accept + recv 的最简单方式,每来一个客户端就 accept 一次,然后阻塞在 recv 上,那么同时支持的客户端数量就会非常有限。

为了解决这个问题,Linux 提供了 I/O 多路复用机制,常见的有三种:

  • select

  • poll

  • epoll

本文将通过一个简单的 C 语言服务器代码,结合 select/poll/epoll 三种方式的实现,重点讲清楚 epoll 的原理,并对比它和 select/poll 的区别。

一、先看一个最简单的服务器

最朴素的写法就是这样:

int clientfd = accept(sockfd, ...);
recv(clientfd, buffer, ...);
send(clientfd, buffer, ...);

这种方式有个致命缺陷:
服务器只能处理 一个客户端,因为 recv 会阻塞等待数据,如果客户端不发数据,服务器就卡住了。

二、select 的原理

select 的思想很直观:

  • 你告诉内核:“我关心这些 socket(fd_set)上是否有事件(可读/可写/异常)”。

  • 内核会帮你一个个去检查,然后告诉你 哪些 fd 上有事件

  • 你再去处理对应的 fd。

缺点:

  1. fd_set 有上限(1024),不能同时监听太多连接。

  2. 每次调用 select 都要把整个 fd_set 从用户态复制到内核态,效率低。

  3. 内核帮你检查完毕后,还得你自己在用户态用循环一个个找出来。

三、poll 的原理

pollselect 类似,改进点在于:

  • 使用了一个 pollfd 数组,没有 1024 的上限。

  • 但是依旧需要 每次把整个数组拷贝进内核,然后再返回给用户态。

  • 事件通知方式还是“轮询”——你得一个个去检查 revents

换句话说,poll 本质上是“加强版的 select”,但性能上并没有质变。

四、epoll 的原理

epoll 是 Linux 提供的一套高效 I/O 事件通知机制,用来“在一个线程里同时监控大量文件描述符(socket 等),并只把真正就绪的那部分交给用户程序处理”,从而避免 select/poll 在大量被监控 fd 上的 O(n) 全表扫描开销。

epoll 的核心思想是:

  1. 事件驱动(不再需要轮询所有 fd)

    • 当某个 socket 上有事件发生时,内核主动把它放到一个就绪队列里。

    • 你只需要从就绪队列里取就行,不用自己一个个遍历。

  2. 内核与用户态共享事件表

    • 通过 epoll_ctl 注册监听的 fd(一次性告诉内核),以后不需要每次都拷贝。

    • epoll_wait 只会返回真正有事件的 fd,效率大幅提升。

  3. 更适合高并发场景

    • 即使有 10 万个连接,只有少量活跃,epoll 只返回活跃的部分,性能几乎不会下降。

五、文字流程图(epoll 工作流程)

服务器启动
    ↓
创建监听 socket(sockfd)
    ↓
epoll_create 创建 epoll 实例
    ↓
epoll_ctl(ADD, sockfd) 将 sockfd 加入监听
    ↓
进入循环 epoll_wait
    ↓
[事件1] sockfd 有新连接 → accept → epoll_ctl(ADD, clientfd)
    ↓
[事件2] clientfd 有数据 → recv → send
    ↓
[事件3] clientfd 断开 → close → epoll_ctl(DEL, clientfd)
    ↓
回到 epoll_wait 等待下一个事件

六、select / poll / epoll 区别总结

特点selectpollepoll
fd 数量限制1024无固定上限无固定上限
用户态/内核态拷贝每次都要每次都要只需一次(注册时)
时间复杂度O(n)O(n)O(1)(只返回就绪 fd)
并发性能一般一般高效(适合上万连接)

七、epoll服务器核心代码讲解

1. 创建 epoll 实例

int epfd = epoll_create(1);
  • epoll_create(1) 创建一个 epoll 实例,返回一个文件描述符 epfd,它就像是一个“事件管理器”。

  • 参数 1 其实没用(Linux 内核忽略它),随便填个大于 0 的值即可。

可以理解为:我们有了一个 “待办事件表”,之后把需要关注的 socket 都放进去。

2. 把监听套接字放入 epoll

struct epoll_event ev;
ev.events = EPOLLIN;      // 关心读事件(有新连接到来)
ev.data.fd = sockfd;      // 保存文件描述符信息
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
  • epoll_event 结构体描述要监听的事件。

    • EPOLLIN:表示关心 可读事件(有新数据或者新连接)。

    • ev.data.fd = sockfd:把 sockfd(监听 socket)存进去,后面可以识别事件来源。

  • epoll_ctl:向 epfd注册一个新的事件,相当于“告诉 epoll,我要关注这个 sockfd 的可读事件”。

这就让 epoll 开始监听服务器的主 socket,随时准备接收新连接。

3. 进入事件循环

while(1){struct epoll_event events[1024] = {0};int nready = epoll_wait(epfd, events, 1024, -1);
  • epoll_wait 就是 等待事件发生

  • 参数解释:

    • events[1024]:用来存储返回的就绪事件。

    • 1024:最多监听 1024 个事件(实际数量 ≤ 1024)。

    • -1:表示阻塞等待,直到有事件发生才返回。

  • 返回值 nready:本次有多少事件就绪。

可以理解为:epoll_wait 就像一个 事件闹钟,有事件发生时会通知我们。

4. 处理事件

for(int i = 0; i < nready; i++){int connfd = events[i].data.fd;
  • 遍历所有就绪事件,一个一个处理。

  • 通过 events[i].data.fd 拿到事件对应的文件描述符。

5. 新客户端连接

if (connfd == sockfd){  // 新客户端连接int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);printf("accept finished: %d\n", clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
}
  • 如果 connfd == sockfd,说明是 监听 socket 触发 → 有新客户端来连接。

  • 调用 accept 拿到新的客户端 clientfd

  • clientfd 也加入 epoll,关心它的 EPOLLIN(可读事件)。

这样以后 epoll 就会帮我们监控这个客户端的收发数据。

6. 客户端发来消息

}else if(events[i].events & EPOLLIN) {  // 客户端发来消息char buffer[1024] = {0};int count = recv(connfd,buffer,1024,0);
  • 如果触发的是 EPOLLIN,并且不是 sockfd,说明是 某个客户端发来数据

  • recv 把数据读出来。

7. 客户端断开连接

if(count == 0){ // 客户端断开printf("client disconnect: %d\n",connfd);close(connfd);epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);continue;
}
  • 如果 recv 返回 0,表示客户端主动断开。

  • 我们需要:

    1. close(connfd) 关闭连接。

    2. epoll_ctl(..., EPOLL_CTL_DEL, ...) 从 epoll 里移除这个 fd,避免继续监听它。

8. 回显消息

printf("RECV: %s\n",buffer);
send(connfd,buffer,count,0);

如果收到数据,就打印出来,并用 send 回发给客户端(回显服务器)。

0voice · GitHub

http://www.xdnf.cn/news/19923.html

相关文章:

  • 十七、单线程 Web 服务器
  • (自用)PowerShell常用命令自查文档
  • AI重构出海营销:HeadAI如何用“滴滴模式”破解红人营销效率困局?
  • Flink 网络消息队列 PrioritizedDeque
  • C52单片机独立按键模块,中断系统,定时器计数器以及蜂鸣器
  • OpenLayers常用控件 -- 章节三:鼠标位置坐标显示控件教程
  • 多线程入门到精通系列: 从操作系统到 Java 线程模型
  • 快鹭云业财一体化系统技术解析:低代码+AI如何破解数据孤岛难题
  • 飞算JavaAI开发在线图书借阅平台全记录:从0到1的实践指南
  • 【C++】详解形参和实参:别再傻傻分不清
  • Android adb shell命令分析应用内存占用
  • 2025全国大学生数学建模C题保姆级思路模型(持续更新):NIPT 的时点选择与胎儿的异常判定
  • Trae + MCP : 一键生成专业封面——从概念到落地的全链路实战
  • java对接物联网设备(一)——使用okhttp网络工具框架对接标准API接口
  • SVN和Git两种版本管理系统对比
  • Hunyuan-MT-7B模型介绍
  • 使用Vue.js和WebSocket打造实时库存仪表盘
  • window使用ffmep工具,加自定义脚本执行视频转码成h264(运营人员使用)
  • P13929 [蓝桥杯 2022 省 Java B] 山 题解
  • 第三方网站测评:【WEB应用文件包含漏洞(LFI/RFI)的测试步骤】
  • 神经网络模型介绍
  • LeetCode 3132.找出与数组相加的整数2
  • 机器学习算法在Backtrader策略稳定性中的作用分析
  • pytorch可视化工具(训练评估:Tensorboard、swanlab)
  • c#编写的应用程序调用不在同一文件夹下的DLL
  • OpenLayers 入门篇教程 -- 章节三 :掌控地图的视野和交互
  • 下一代自动驾驶汽车系统XIL验证方法
  • 【Doris入门】Doris数据表模型使用指南:核心注意事项与实践
  • select, poll, epoll
  • PyTorch 损失函数与优化器全面指南:从理论到实践