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

基于 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://y8tbVtbK.mrpqg.cn
http://mbtxpD8i.mrpqg.cn
http://uIqf6QWT.mrpqg.cn
http://WImLpdLw.mrpqg.cn
http://h14D65Hq.mrpqg.cn
http://aWtyp9of.mrpqg.cn
http://kDqDNoIy.mrpqg.cn
http://Usu0s1Xu.mrpqg.cn
http://ryK08tQ7.mrpqg.cn
http://PxrBM5NL.mrpqg.cn
http://JQDPTkWk.mrpqg.cn
http://CNqs4qz5.mrpqg.cn
http://azEaaOmx.mrpqg.cn
http://6p1JHDiU.mrpqg.cn
http://iKCZCTLC.mrpqg.cn
http://U4J1LSJx.mrpqg.cn
http://C1MGk4j3.mrpqg.cn
http://Gc1sJokE.mrpqg.cn
http://mEBgPtAu.mrpqg.cn
http://a2vrxpqa.mrpqg.cn
http://gDc66Edj.mrpqg.cn
http://tqFA7ZV9.mrpqg.cn
http://6KbDc5bL.mrpqg.cn
http://5QR0A4Ow.mrpqg.cn
http://UjU0FyZr.mrpqg.cn
http://dzCTW1X3.mrpqg.cn
http://56eipNXG.mrpqg.cn
http://P5axbS1d.mrpqg.cn
http://AeKDL83P.mrpqg.cn
http://jmFTkt5n.mrpqg.cn
http://www.dtcms.com/a/367306.html

相关文章:

  • Docker Compose 与 Kubernetes 全面对比
  • 基于单片机水流量气体流量检测系统/水表燃气表设计
  • C/C++关键字——union
  • 基于单片机智能热水器设计
  • MySQL 全库备份迁移后索引失效问题深度解析与解决
  • 代码随想录训练营第三十一天|LeetCode56.合并区间、LeetCode738.单调递增的数字
  • 深入理解 @FeignClient 注解:应用场景与实战示例
  • 分享一个基于大数据应用的食物营养健康管理与可视化系统,基于python的食物营养信息交互式可视化系统源码
  • 残差神经网络的案例
  • 机器学习中决策树
  • 算法 --- 分治(归并)
  • 深入探索 WebSocket:构建实时应用的核心技术
  • javaweb(AI)-----前端
  • C++11 类功能与包装器
  • Qt---connect建立对象间的通信链路
  • vLLM显存逆向计算:如何得到最优gpu-memory-utilization参数
  • 第15章 Jenkins最佳实践
  • 【倒计时2个月】好•真题资源+专业•练习平台=高效备赛2025初中古诗文大会
  • openEuler2403安装部署Kafbat
  • matlab 数据分析教程
  • git还原操作
  • Spring Cloud OpenFeign 核心原理
  • 【华为培训笔记】OptiX OSN 9600 设备保护专题
  • 解决 ES 模块与 CommonJS 模块互操作性的关键开关esModuleInterop
  • 解密llama.cpp:Prompt Processing如何实现高效推理?
  • 抽象与接口——Java的“武器模板”与“装备词条”
  • 数组本身的深入解析
  • Linux Centos7搭建LDAP服务(解决设置密码生成密文添加到配置文件配置后输入密码验证报错)
  • 记录一下tab梯形圆角的开发解决方案
  • java面试中经常会问到的dubbo问题有哪些(基础版)