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

【IO多路转接】epoll 高性能网络编程:从底层机制到服务器实战


请添加图片描述


半桔:个人主页

 🔥 个人专栏: 《IO多路转接》《手撕面试算法》《C++从入门到入土》

🔖世界上有太多孤独的人,害怕先踏出第一步。《绿皮书》

文章目录

  • 前言
  • 一. epoll模型的底层原理
    • 1.1 红黑树
    • 1.2 就绪队列
  • 二. epoll 接口
  • 三. epoll 服务器实现
    • 3.1 网络套接字接口封装
    • 3.2 对epoll接口进行封装
    • 3.3 设计epoll服务器的类
    • 3.4 进行初始化
    • 3.5 对任务进行派发
    • 3.6 服务器主循环函数
  • 四. epoll模型的优势

前言

在 Linux 网络编程领域,IO 多路复用是支撑高并发网络服务的核心技术基石。当服务端需要同时应对成百上千甚至数万级的客户端连接时,如何高效监控、响应这些连接的 IO 事件,直接决定了服务器的性能上限。

传统的selectpoll模型,受限于文件描述符管理效率、事件通知机制等缺陷(如select的文件描述符数量上限、每次轮询的线性遍历开销等),在高并发场景下逐渐力不从心。而epoll作为 Linux 专为解决高并发 IO 痛点设计的多路复用模型,凭借更高效的事件通知、更灵活的文件描述符管理,成为了 Nginx、Redis 等高性能中间件背后的 “性能引擎”。

为了帮助开发者系统掌握epoll,本文将从 “底层逻辑”“工程实现”“技术优势” 三个维度展开解析:

  1. 先深入epoll的底层原理,剖析红黑树、就绪队列的设计智慧与epoll接口的工作机制;
  2. epoll服务器的落地实践,从网络套接字封装、接口二次封装,到服务器类设计、初始化、任务派发与主循环的构建,逐步讲解高并发服务器的实现路径;
  3. 最后总结epoll相对于传统模型的核心优势,让你清晰理解它在高并发场景下的不可替代性。

希望通过本文,你能对epoll的 “原理 - 实现 - 价值” 形成完整认知,为高性能网络编程实践筑牢基础。

一. epoll模型的底层原理

epoll是效率最高的多路转接方案。
在了解epoll的接口之前,我们有必要先学习以下epoll的底层原理,其是如何实现最高效率的。

如下图所示就是一个epoll原理示意图:请添加图片描述

1.1 红黑树

  • epoll模型中,存在一颗红黑树,由操作系统进行管理和维护,内部存储了所有我们要进行等待的文件描述符以及需要进行等待的时间;
  • 当对应的文件中有数据了,就会向CPU发送中断信号,CPU会执行操作系统中的中断向量表中的方法,将内容进来,同时操作系统快速查找对应文件描述符在红黑树的位置,并将其添加到就绪队列中

1.2 就绪队列

操作系统会将所有读写时间就绪的文件描述符添加到就绪队列中,当上层调用epoll的时候,可以直接将该就绪队列拷贝出去就行了


通过上述就绪队列来让上层获取满足条件的文件描述符就可以避免像select,poll一样对整个数组进行访问,来看那些文件就绪。大大提高了效率。

整体步骤就是如下:

  1. 上层创建一个红黑树;
  2. 上层将要进行等待的文件描述符添加到红黑树中;
  3. 操作系统将就绪的文件描述符添加到就绪队列中;
  4. 上层要进行获取时,操作系统直接将就绪队列拷贝出去即可。

下面进行接口介绍,解释上述步骤在进行实现的时候要调用那些接口。

二. epoll 接口

首先就是创建一颗红黑树:

int epoll_create(int size)

  1. 参数:已经被弃用了,没有实际意义,传入一个大于0的数即可;
  2. 返回值:返回一个文件描述符,用来对epoll模型进行管理。

接下来就是向红黑树中增加文件,删除文件,以及进行修改,这三种使用的都是同一个接口:

int epoll_ctl(int epfd , int op , int fd , struct epoll_event *event)

  1. 参数一:要进行操作的epoll模型,就是在创建epoll模型时返回的文件描述符;
  2. 参数二:op表示要进行的操作,其中包含:EPOLL_CTL_ADD , EPOLL_CTL_MOD , EPOLL_CTL_DEL分别表示对红黑树进行增加节点,修改节点,删除节点;
  3. 参数三fd:表示要进行等待的文件描述符;

struct epoll_event是操作系统内提供的一个结构体:

typedef union epoll_data {
void *ptr;
int fd;                 
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;struct epoll_event {
__uint32_t events;   // 要进行等待的事件,读/写/异常
epoll_data_t data;   // 用户数据变量,可以存储触发事件的文件描述符相关的数据。
};
  1. 参数四:存储对应的文件描述符的等待事件,以及用户数据变量,可以存储触发事件的文件描述符相关的数据。
  2. 返回值:表示有多少个文件描述符资源已经就绪。

最后一个接口就是让epoll模型返回就绪队列:
int epoll_wait(int epfd , struct epoll_event *evemts , int maxevents , int timeout)

  1. 参数一:文件描述符;
  2. 参数二:表明将就绪队列放在哪里;
  3. 参数三:参数二的长度,外界最多可以获取多少就绪的文件描述符;
  4. 参数四:进行等待的时间,当时间到/有文件就绪就进行返回;
  5. 返回值:表示有多少文件资源已经就绪。

三. epoll 服务器实现

此处我们只进行一个简单的服务器实现,不进行过多设计,只是简单使用一下对应接口。此处我们在进行设计的时候假设:接收到的TCP报文是完整的。

3.1 网络套接字接口封装

关于网络套接字接口的封装,此处就不再展开说明了,有兴趣的可以看我之前关于TCP的文章;此处直接贴实现:

const std::string defaultip_ = "0.0.0.0";
enum SockErr
{SOCKET_Err,BIND_Err,
};class Sock
{
public:Sock(uint16_t port): port_(port),listensockfd_(-1){}void Socket(){listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (listensockfd_ < 0){Log(Fatal) << "socket fail";exit(SOCKET_Err);}Log(Info) << "socket sucess";}void Bind(){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port_);inet_pton(AF_INET, defaultip_.c_str(), &server.sin_addr);if (bind(listensockfd_, (struct sockaddr *)&server, sizeof(server)) < 0){Log(Fatal) << "bind fail";exit(BIND_Err);}Log(Info) << "bind sucess";}void Listen(){if (listen(listensockfd_, 10) < 0){Log(Warning) << "listen fail";}Log(Info) << "listen sucess";}int Accept(){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(listensockfd_ , (sockaddr*)&client , &len);if(fd < 0){Log(Warning) << "accept fail";}return fd;}int Accept(std::string& ip , uint16_t& port){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(listensockfd_ , (sockaddr*)&client , &len);if(fd < 0){Log(Warning) << "accept fail";}port = ntohs(client.sin_port);char bufferip[64];inet_ntop(AF_INET , &client.sin_addr , bufferip , sizeof(bufferip) - 1);ip = bufferip;return fd;}int Get_fd(){return listensockfd_;}~Sock(){close(listensockfd_);}private:uint16_t port_;int listensockfd_;
};

3.2 对epoll接口进行封装

为了我们后续方便使用,我们此处对epoll的相关接口进行简单封装:

enum EpollErr
{CREAR_Err,
};class Epoll
{
public:Epoll(){// 创建epoll模型_epfd = epoll_create(1);if (_epfd < 0){Log(Fatal) << "epoll_create fail";exit(CREAR_Err);}Log(Info) << "epoll create sucess ";}void Add_fd(int fd, uint32_t event){// 添加文件描述符到红黑树中struct epoll_event epevt;epevt.events = event;epevt.data.fd = fd;if (epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &epevt) < 0){Log(Warning) << "epoll add error : " << strerror(errno);}Log(Info) << "epoll add sucess , fd : " << fd ;}void Del_fd(int fd){// 删除要进行等待的文件描述符if (epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) < 0){Log(Warning) << "epoll del error : " << strerror(errno);}Log(Info) << "epoll del sucess  , fd : " << fd;}void Mod_fd(int fd, uint32_t event){// 对文件描述符的事件进行修改struct epoll_event epevt;epevt.events = event;epevt.data.fd = fd;if (epoll_ctl(_epfd, EPOLL_CTL_MOD, fd, &epevt) < 0){Log(Warning) << "epoll mod error : " << strerror(errno);}Log(Info) << "epoll mod sucess , fd : " << fd ;}int Wait(struct epoll_event *ep_array, int max_size, int timeout){// 进行等待return epoll_wait(_epfd, ep_array, max_size, timeout);}private:int _epfd;
};

3.3 设计epoll服务器的类

  1. Sock对象,进行网络通信;
  2. Epoll对象,来使用epoll的接口;
  3. 一个struct epoll_event的数组,在调用epoll_wait接口时进行使用。
class Epollserver
{static const int default_array_num = 1024;
public:Epollserver(uint16_t port):_epoll_ptr(new Epoll) , _sock_ptr(new Sock(port)){}private:std::shared_ptr<Epoll> _epoll_ptr;std::shared_ptr<Sock> _sock_ptr;struct epoll_event _ep_array[default_array_num];
};

3.4 进行初始化

  1. 创建套接字
  2. 进行绑定
  3. 设置监听
  4. 将网络套接字,加入到epol1模型中
    void Init(){// 1. 创建套接字// 2. 进行绑定// 3. 设置监听// 4. 将网络套接字 ,加入到epoll模型中_sock_ptr->Socket();_sock_ptr->Bind();_sock_ptr->Listen();_epoll_ptr->Add_fd(_sock_ptr->Get_fd() , EPOLLIN);}

3.5 对任务进行派发

因为存在两种文件描述符,因此我们需要对不同文件的处理分开:

  1. 对于网络套接字,就获取连接;
  2. 对于普通文件描述符,就将数据读取上来,在进行返回。
    void Sockfd_Ready(){// 网络套接字就绪// 1. 获取建立文件描述符// 2. 将文件描述符加入到epoll模型中int newfd = _sock_ptr->Accept();_epoll_ptr->Add_fd(newfd, EPOLLIN);}void Normalfd_Ready(int fd){// 普通文件描述符就绪// 1. 将数据读取上来// 2. 向用户端返回信息char buffer[1024];int n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::string ret = "server got a message : ";ret += buffer;write(fd, ret.c_str(), ret.size());}else if (n == 0){// 对方将文件关闭// 1. 此处我们也就不需要在进行等待了, 从epoll模型中移除// 2. 关闭文件描述符_epoll_ptr->Del_fd(fd);close(fd);}else{// 出错了// 1. 打印日志// 2. 将文件描述符从epoll模型中移除// 3. 关闭文件描述符Log(Warning) << "read fail";_epoll_ptr->Del_fd(fd);close(fd);}}void Dispatcher(int n){int listensock = _sock_ptr->Get_fd();for (int i = 0; i < n; i++){int fd = _ep_array[i].data.fd;if (fd == listensock){Sockfd_Ready();}else{Normalfd_Ready(fd);}}}

3.6 服务器主循环函数

此时就可以根据上述接口进行编写主循环函数了,主循环函数只需要负责将epoll模型中的就绪队列中的数据拿出来就行了。

    void Run(){while (1){int n = _epoll_ptr->Wait(_ep_array, default_array_num, -1);if (n > 0){Dispatcher(n);}else if( n == 0){Log(Info) << "no file is ready";}else{Log(Warning) << "epoll wait fail";}}}

四. epoll模型的优势

  1. 检测是否存在就绪的文件时间复杂度为O(1),当然获取的还是要进行拷贝;
  2. epoll模型会帮我们维护要进行检测的文件描述符,不需要我们再设计函数自己维护;
  3. epoll模型返回的就是就绪的文件,不需要再进行判断是否满足条件了;
  4. 要进行检测的文件数量没有限制。
http://www.dtcms.com/a/576928.html

相关文章:

  • python --两个文件夹文件名比对(yolo 图和label标注比对检查)
  • 北京网站建设1000zhu建站之星模板怎么设置
  • wordpress+企业站模版做论坛app网站
  • 社群时代下的商业变革:“开源AI智能名片链动2+1模式S2B2C商城小程序”的应用与影响
  • 深入理解浏览器渲染流程:从HTML/CSS到像素的奇妙旅程
  • Photoshop - Photoshop 工具栏(24)磁性套索工具
  • 抓取QNX的RAMdump数据如何操作
  • RabbitMQ Quorum 队列与classic队列关系
  • ubuntu摄像头型号匹配不上_11-6
  • Design Compiler:时钟树在综合时的特性
  • 阿里云 icp app备案
  • 算法基础篇:(二)基础算法之高精度:突破数据极限
  • 香港100G高防服务器的防御力如何?
  • 网站文章怎么做分享qq网站建设步骤详解视频教程
  • 开发者实践:机器人集群的 API 对接与 MQTT 边缘调度解耦
  • 百日挑战——单词篇(第十五天)
  • 中国SIP中继类型
  • Kubernetes 原生滚动更新(Rolling Update)完整实践指南
  • 沈阳做企业网站哪家好网架提升公司
  • [N_151]基于微信小程序校园学生活动管理平台
  • Stager贴花工作流:告别Painter的“烘焙式”贴图
  • Linux 开发语言选择指南:不同场景该用哪种?
  • h5网站动画怎么做的重庆企业网络推广价格
  • 免费创建网站带咨询的免费企业网站程序asp
  • css 宽度屏幕50%,高度等于宽度的50%,窗口变化,比例不变(宽度百分比,高度等比例自适应)
  • Photoshop通道的应用
  • VUE3+element-plus 循环列表中图标由后台动态添加
  • LangFlow前端源码深度解析:核心模块与关键实现
  • 从 Rust 到 Flutter:嵌入式图形与构建工具全景指南
  • 转折·融合·重构——2025十大新兴技术驱动系统变革与全球挑战应对