『 Linux 』高级IO (三) - Epoll模型的封装与EpollEchoServer服务器

文章目录

    • 前情提要
    • Epoll 的封装
      • Epoll封装完整代码(供参考)
    • Epoll Echo Server
      • Epoll Echo Server 测试及完整代码


前情提要

在上一篇博客『 Linux 』高级IO (二) - 多路转接介绍并完成了两种多路转接方案的介绍以及对应多路转接方案代码的编写,分别为SelectServer服务器与PollServer服务器;

同时在该篇博客中介绍了继select()poll()多路转接方案之后所提出的Epoll多路转接方案;

此处不再赘述;

而在上文中并未对Epoll多路转接方案的代码进行编写,此篇博客进行补充;


Epoll 的封装

在上一篇博客中对Epoll多路转接方案进行了介绍;

本质上Epoll通过内核所维护的三种机制实现多路转接方案;

  • 系统内核内部为Epoll所维护的的红黑树
  • 系统内核内部为Epoll已就绪事件所维护的就绪队列
  • 系统内核内部因Epoll所提供的回调机制

其中红黑树用来管理正在被监听的文件描述符,就绪队列用来管理已触发的文件描述符,回调机制用于检测并推送事件触发的结果到就绪队列中;

其对应的核心函数为epoll_create(),epoll_wait()epoll_ctl();

与操作系统内核相应:

  • epoll_create()

    创建Epoll模型并返回Epoll模型的文件描述符;

  • epoll_ctl()

    控制内核中为Epoll所维护的红黑树的增删改操作;

  • epoll_wait()

    用于进行等待,并返回就绪队列中相应数量的就绪事件给用户提前准备的空间,本质是关心就绪队列本身;

Epoll的函数较为分散,为简化Epoll操作,可以对Epoll模型进行封装;

整体的封装采用RAII(构造即初始化,析构即释放)的风格;

  • 整体结构

    为了防止Epoll模型封装类被拷贝,可以将拷贝构造与拷贝赋值设置为delete,或者创建不可被拷贝的基类(同样设为delete),将Epoll封装设置为其派生类以防止Epoll封装类被拷贝;

    • nocopy.hpp

      /* nocopy.hpp */class nocopy
      {
      private:
      public:nocopy(){};nocopy(const nocopy &) = delete;const nocopy& operator=(const nocopy&) = delete;~nocopy(){};
      };
      
    • Epoller.hpp

      /* Epoller.hpp */class Epoller : public nocopy // 防止拷贝 继承防拷贝类 class nocopy
      {
      public:Epoller(){}~Epoller(){}private:int _epfd; // Epoll 的描述符static const int _timeout = 3000;
      };
      

    此处使用继承防拷贝类的方式进行防拷贝;

    主要在类中定义了两个成员变量,当epoll_create()被调用时,将返回一个文件描述符,这个文件描述符是Epoll模型的文件描述符,应当进行保存;

    此外_timeout成员为默认定义的timeout时间,可酌情调整;

  • RAII

    RAII为构造即初始化,析构即释放;

    因此Epoll模型创建与释放需要分别在构造函数与析构函数中;

    class Epoller : public nocopy // 防止拷贝 继承防拷贝类 class nocopy
    {static const int _size = 1024;
    public:Epoller(){int size = 128;_epfd = epoll_create(_size); // 创建 epoll 模型if (_epfd == -1)             // 创建失败{lg(FATAL, "epoll_create error: %s", strerror(errno));}else // 创建成功{lg(INFO, "epoll_create sucess, fd: %d", _epfd); // 创建成功查看对应文件描述符}}~Epoller(){if (_epfd >= 0){close(_epfd);}}private:int _epfd; // Epoll 的描述符static const int _timeout = 3000;
    };
    

    由于epoll_create()的参数已经被废弃,因此该处参数设置为1024(无意义);

    当析构时只需关闭对应Epoll模型的文件描述符即可;

  • epoll_wait()封装

    epoll_wait()函数在Epoll模型中用来关心就绪队列中是否存在已就绪事件;

    class Epoller : public nocopy // 防止拷贝 继承防拷贝类 class nocopy
    {
    public:int EpollerWait(struct epoll_event revents[], int num){// 等待操作在Epoll模型中为将就绪队列中的// 已就绪事件文件描述符拷贝至用户预设空间int n = epoll_wait(_epfd, revents, num, _timeout);return n;}
    private:int _epfd; // Epoll 的描述符static const int _timeout = 3000;
    };
    

    对应的将其封装为EpollerWait()函数,在参数上需要传递一个用户预设的空间;

    这段用户预设的空间用于epoll_wait()函数将就绪队列中已就绪的事件拷贝至用户层;

    传入的num表示用户预设空间每次能够接受多少就绪队列中的已就绪事件;

    • Ps:

      当内核就绪队列中已就绪事件大于用户所预设空间时,就绪队列本次只会传递用户预设空间响应数量的就绪事件,余下的就绪事件将继续存放至内核就绪队列当中;

  • epoll_ctl()封装

    epoll_ctl()函数在Epoll模型中,主要用于对内核红黑树进行增删改操作;

    其函数声明为:

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

    操作根据参数的传递主要分为两种:

    • 删除操作

      当操作为删除操作时,参数event不需要传参;

    • 增加/修改操作

      当操作不为删除操作时,参数都需要填写,其中op用于表明具体操作,如EPOLL_CTL_MOD修改操作或EPOLL_CTL_ADD增加操作;

      event表示操作所关心的具体事件以及对应的文件描述符(以结构体形式存储);

    class Epoller : public nocopy // 防止拷贝 继承防拷贝类 class nocopy
    {static const int _size = 1024;public:int EpollerUpdate(int op, int sock, uint32_t event){int n = 0;if (op == EPOLL_CTL_DEL){// 删除操作n = epoll_ctl(_epfd, op, sock, nullptr); // 删除if (n != 0){lg(WARNING, "delete epoll_ctl error");}}else{// 非删除操作即新增或修改struct epoll_event ev;ev.events = event;ev.data.fd = sock;n = epoll_ctl(_epfd, op, sock, &ev); // 注册进内核if (n != 0){lg(WARNING, "EpollerUpdate Error: %s", strerror(errno));}}return n;}
    private:int _epfd; // Epoll 的描述符static const int _timeout = 3000;
    };
    

Epoll封装完整代码(供参考)

[半介莽夫 - Gitee For half-intermediate-mangfu/IO/AdvancedIO/EpollEncapsulation]


Epoll Echo Server

同样可以利用Epoll实现一个Echo服务器;

且其实现方式较SelectPoll两种多路转接方案还要简单(尤其是在对Epoll进行封装后);

  • 整体结构与初始化

    同样的Epoll多路转接的Echo服务器作为一款服务器主要分为初始化与运行两个部分;

    const uint32_t EVENT_IN = EPOLLIN;
    const uint32_t EVENT_OUT = EPOLLOUT;
    const uint32_t EVENT_DEL_OP = 0;
    class EpollServer : public nocopy
    {
    public:EpollServer(uint16_t port) // 此处使用智能指针 因此在初始化列表中使用 new 实例化: _port(port), _listensocket(new NetSocket), _epoller(new Epoller){}void Init(){ // 正常的创建 绑定 监听三件套_listensocket->Socket();_listensocket->Bind(_port);_listensocket->Listen();lg(INFO, "Create listen socket sucess, fd: %d", _listensocket->GetFd());}void Start(){}~EpollServer(){// 析构函数关闭套接字 (内置封装)_listensocket->Close();}private:std::shared_ptr<NetSocket> _listensocket; // 使用智能指针std::shared_ptr<Epoller> _epoller;uint16_t _port;
    };
    

    既然是网络服务器那么必须使用对应的网络接口,同样这里使用预先封装过的Socket接口;

    成员变量主要为如下:

    • _listensocket

      表示监听套接字对应的实例,监听套接字用来监听新连接的到来;

    • _epoller

      表示Epoll模型,此处使用上文所封装的Epoll模型;

    • _port

      表示监听套接字所绑定的端口号;

    监听套接字与Epoll模型皆采用智能指针使其更加安全与便利;

    定义了三个uint32_t类型常量,主要因为在该程序中为配合Epoll模型封装中的EpollerUpdate()函数进行使用;

    分别用于关心与判断,设置读写事件或是删除操作;

    在初始化列表中分别对三个成员变量进行初始化,并在Init()中对监听套接字进行"三板斧"操作,即创建套接字,绑定端口与设置监听;

    在析构函数中调用封装的Socket中的close()对监听套接字文件描述符进行关闭从而完成监听套接字的清理;

  • Start()运行函数

    在该函数中主要是循环调用epoll_wait()函数传入用户预设空间实现对多个事件进行监听;

    在设置timeout的情况下,该函数的返回值(n)有三种情况:

    • n > 0

      n > 0时表示有n个就绪事件从就绪队列被推送至用户预设空间中;

    • n == 0

      n == 0时表示timeout时间到期,没有事件就绪(就绪队列中没有就绪事件,因此不会有就绪事件被推送至用户预设的事件空间中);

    • n < 0

      n < 0时则表示该函数调用失败;

    由于已经对epoll_wait()函数进行封装,因此只需调用Epoll模型实例中对应的成员函数EpollerWait()即可;

    class EpollServer : public nocopy
    {static const int _num = 64;// 表示用户预设就绪事件空间单次最大读取就绪事件数量
    public:void Start(){// 在进行循环前 第一次调用必须保证监听套接字被添加至epoll当中// 这里本质是将监听套接字与其所关心的事件添加至内核epoll模型的红黑树当中_epoller->EpollerUpdate(EPOLL_CTL_ADD, _listensocket->GetFd(), EVENT_IN);struct epoll_event revs[_num];for (;;) // 运行过程中采用循环{int n = _epoller->EpollerWait(revs, _num);/*n 为返回值多少个所传入的revs数组为输出型参数_num表示每次最多从就绪队列中取多少个*/if (n > 0) // 表有事件就绪{Dispatcher(revs, n); // 进行事件派发}else if (n == 0){lg(INFO, "time out...");}else{lg(WARNING, "EpollerWait error...");}}}
    private:std::shared_ptr<NetSocket> _listensocket; // 使用智能指针std::shared_ptr<Epoller> _epoller;uint16_t _port;
    };
    

    在该函数中设置了一个_num = 64的常量用于设置用户预设空间(数组)的大小;

    用户预设空间可直接采用struct epoll_event结构体数组的形式;

    这里还有一个细节,第一个被关心事件的文件描述符必然是监听套接字文件描述符;

    当监听套接字文件描述符事件就绪后,对应的事件将被推送至用户预设空间中,之后才能将监听套接字文件描述符中的新连接进行获取并将新连接fd注册进系统内核的红黑树当中(设置观察);

    因此在第一次进行EpollWait()前需要调用EpollerUpdate()将监听套接字文件描述符注册进操作系统内核的文件描述符中;

    根据不同返回值进行下一步决策,当返回值n>0时表示n个就绪事件被推送至用户预设空间(数组)中,但无法在当前情况判断所就绪事件具体是什么事件,因此下一步进行事件派发Dispatcher();

  • Dispatcher()事件派发

    当对应epoll_wait()操作返回值>0时表示有对应就绪事件被推送至用户层;

    但并不清楚就绪事件具体属性,因此需要对事件进行区分且根据具体事件进行事件派发;

    class EpollServer : public nocopy
    {
    public:void Dispatcher(struct epoll_event revs[], int num) // 进行事件派发{for (int i = 0; i < num; ++i){uint32_t events = revs[i].events; // 获取事件int fd = revs[i].data.fd;         // 获取文件描述符if (events & EVENT_IN) // 其他{if (fd == _listensocket->GetFd()) // 监听套接字读事件就绪{// Accepter() 获取连接}else // 其他读事件就绪{// Recver() 读取数据}}else if (events & EVENT_OUT) // 写事件{// 暂时不考虑}else // 其他{// 暂时不考虑}}}
    private:std::shared_ptr<NetSocket> _listensocket; // 使用智能指针std::shared_ptr<Epoller> _epoller;uint16_t _port;
    };
    

    在该程序中主要观察读写两个事件,其中读写事件是方便为了区分,此程序中不对写事件进行处理;

    该函数中的num参数表示EpollWait()函数的返回值,即由就绪队列推送至用户层的就绪事件个数,参数revs则为用户预设的空间(空间内已因EpollWait()存在num个就绪事件);

    对事件进行派发的前提为了解对应事件具体事件,根据num个数循环遍历revs数组即可获得当前已就绪事件;

    以此获取就绪事件对应的文件描述符与具体事件,根据具体事件进行判断;

    若是事件为读事件就绪,其可能性为如下:

    • 监听套接字监听到新连接
    • 其他文件描述符获取到可读数据

    当就绪事件的文件描述符为监听套接字文件描述符时则表示需要调用accept()获取新连接,并将新连接的文件描述符注册至Epoll模型中的红黑树;

    当就绪事件不为监听套接字描述符时则表示其他文件描述符的可读数据已经就绪,需要将数据通过文件描述符拷贝至用户层;

  • Accepter()连接管理器

    当事件为新连接到来时将调用对应的accept()函数获取新连接,并将新连接的文件描述符以关心事件为读事件注册进操作系统Epoll模型的红黑树当中;

    此处直接调用封装的Socket接口完成连接的获取;

    class EpollServer : public nocopy
    {
    public:void Accepter(){std::string clientip;uint16_t clientport;int newfd = _listensocket->Accept(&clientip, &clientport);if (newfd < 0){lg(WARNING, "New fd Accept error: %s", strerror(errno));}lg(INFO, "New fd Accept Sucess, fd: %d", newfd);_epoller->EpollerUpdate(EPOLL_CTL_ADD, newfd, EVENT_IN);}private:std::shared_ptr<NetSocket> _listensocket; // 使用智能指针std::shared_ptr<Epoller> _epoller;uint16_t _port;
    };
    
  • Recver()信息获取与回响

    当读事件对应的描述符不为监听套接字描述符时则表示需要将数据由对应文件描述符中获取,并将数据回响写回客户端上;

    class EpollServer : public nocopy
    {
    public:void Recver(int fd){char inbuff[1024];int n = read(fd, inbuff, sizeof(inbuff) - 1);if (n > 0){inbuff[n] = 0;printf("Fd %d Get a message: %s", fd, inbuff);std::string echo_str = "Server Echo @ ";echo_str += inbuff;write(fd, echo_str.c_str(), echo_str.size());}else if (n == 0){printf("Fd %d Closed, Me too...\n", fd);_epoller->EpollerUpdate(EPOLL_CTL_DEL, fd, EVENT_DEL_OP); // 在进行删除操作时确保文件描述符为一个有效的文件描述符close(fd);}else{lg(WARNING, "Read error...\n");_epoller->EpollerUpdate(EPOLL_CTL_DEL, fd, EVENT_DEL_OP);close(fd);}}
    private:std::shared_ptr<NetSocket> _listensocket; // 使用智能指针std::shared_ptr<Epoller> _epoller;uint16_t _port;
    };
    

    这里直接调用read()进行数据的提取并进行打印,返回值(n)有三种结果:

    • n>0

      表示正确读取;

    • n==0

      表示对端关闭连接;

    • n<0

      表示read()调用失败;

    当正确读取时对数据进行打印并调用write()回响回对端;

    当对端关闭连接与读取失败时调用日志插件打印出对应日志信息并同样对连接进行关闭;

    关闭连接涉及到移除Epoll中关心的文件描述符与close()关闭连接,这里值得注意的是,在进行epoll_ctl()将文件描述符进行移除时需要确保该文件描述符为一个有效文件描述符,否则调用将失败报错;

    因此需要先移除文件描述符再调用close()对文件描述符进行关闭;

    同时这里直接调用read()会存在一个问题,即数据读取可能不完全的问题(此处只提出问题不进行解决,不再赘述);


Epoll Echo Server 测试及完整代码

从测试结果可以看出,其结果与Select方案Poll方案多路转接所实现的EchoServer相同,且其效率要比另两种方案多路转接方案更为优秀;

  • 完整代码(供参考)

    [半介莽夫 - Gitee For half-intermediate-mangfu/IO/AdvancedIO/EpollServer ]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/501796.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue路由模式面试题

vue路由模式 1.路由的模式有哪些?有什么区别? history和hash模式 区别: 1.表现的形态不同: 在地址栏url中:hash模式中带有**#**号,history没有 2.请求错误时表现不同: 在hash模式中,对于404地址请求时,不会进行请求 但是在history模式中,对于404请求时,仍然会进行请求…

Docker安装Prometheus和Grafana

概念简述 安装prometheus 第一步&#xff1a;确保安装有docker 第二步&#xff1a;拉取镜像 第三步&#xff1a;准备相关挂载目录及文件 第四步&#xff1a;启动容器 第五步&#xff1a;访问测试 安装grafana 第一步&#xff1a;确保安装有docker 第二步&#xff1a;拉…

UE5失真材质

渐变材质函数&#xff1a;RadialGradientExponential&#xff08;指数径向渐变&#xff09; 函数使用 UV 通道 0 来产生径向渐变&#xff0c;同时允许用户调整半径和中心点偏移。 用于控制渐变所在的位置及其涵盖 0-1 空间的程度。 基于 0-1 的渐变中心位置偏移。 源自中心的径…

Android授权USB使用权限示例

使用效果&#xff1a; 授权实现过程&#xff1a; 1.在AndroidManifest.xml中增加android.hardware.usb.action.USB_DEVICE_ATTACHED的action及meta-data action: <action android:name"android.hardware.usb.action.USB_DEVICE_ATTACHED"/> meta-data: &l…

matlab时频分析库

time frequency gallery

算法题(25):只出现一次的数字(三)

审题&#xff1a; 该题中有两个元素只出现一次并且其他元素都出现两次&#xff0c;需要返回这两个只出现一次的数&#xff0c;并且不要求返回顺序 思路: 由于对空间复杂度有要求&#xff0c;我们这里不考虑哈希表。我们采用位运算的方法解题 方法&#xff1a;位运算 首先&#…

python +tkinter绘制彩虹和云朵

python tkinter绘制彩虹和云朵 彩虹&#xff0c;简称虹&#xff0c;是气象中的一种光学现象&#xff0c;当太阳光照射到半空中的水滴&#xff0c;光线被折射及反射&#xff0c;在天空上形成拱形的七彩光谱&#xff0c;由外圈至内圈呈红、橙、黄、绿、蓝、靛、紫七种颜色。事实…

HTML——28.音频的引入

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>音频引入</title></head><body><!--audio:在网页中引入音频当属性名和属性值一样&#xff0c;可以只写属性名src属性:指定音频文件路径&#xff0c;必…

基于Spring Boot + Vue3实现的在线汽车保养维修预约管理系统源码+文档

前言 基于Spring Boot Vue3实现的在线汽车保养维修预约管理系统是一种前后端分离架构的应用&#xff0c;它结合了Java后端开发框架Spring Boot和现代JavaScript前端框架Vue.js 3.0的优势。这样的系统可以为汽车服务站提供一个高效的平台来管理客户的预约请求 技术选型 系统…

【Python学习(六)——While、for、循环控制、指数爆炸】

Python学习&#xff08;六&#xff09;——While、for、循环控制、指数爆炸 本文介绍了While、for、循环控制、指数爆炸&#xff0c;仅作为本人学习时记录&#xff0c;感兴趣的初学者可以一起看看&#xff0c;欢迎评论区讨论&#xff0c;一起加油鸭~~~ 心中默念&#xff1a;Py…

计算机网络——期末复习(5)期末考试样例1(含答案)

考试题型&#xff1b; 概念辨析&#xff15;个、计算与分析&#xff13;个、综合题&#xff13;&#xff0d;&#xff14;个 必考知识点&#xff1a; 概述&#xff1a;协议 体系结构 物理层&#xff1b;本次考核较少 链路层&#xff1a;CSMA/CD 退避二进制算法 &#xff0…

豆包ai 生成动态tree 增、删、改以及上移下移 html+jquery

[豆包ai 生成动态tree 增、删、改以及上移下移 htmljquery) 人工Ai 编程 推荐一Kimi https://kimi.moonshot.cn/ 推荐二 豆包https://www.doubao.com/ 实现效果图 html 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF…

5大常见高并发限流算法选型浅析

高并发场景下&#xff0c;如何确保系统稳定运行&#xff0c;成为了每一个开发工程师必须面对的挑战。**你是否曾因系统崩溃、请求超时或资源耗尽而头疼不已&#xff1f;**高并发限流算法或许能帮你解决这些难题。 在处理高并发请求时&#xff0c;应该如何选择合适的限流算法呢…

【重庆】《政务数字化应用费用测算规范》(T/CDCIDA 001—2023)-省市费用标准解读系列36

《政务数字化应用费用测算规范&#xff08;报批稿&#xff09;》于2023年11月18日实施&#xff0c;本文件按照GB/T 1.1-2020给出的规则起草&#xff0c;主要适用于重庆政务数字化应用项目的费用测算。我司基于专业第三方信息化项目造价机构角度&#xff0c;从标准创新点、定制软…

力扣【SQL连续问题】

180. 连续出现的数字 SELECT DISTINCT if(a.num b.num AND b.num c.num,a.num,null) AS ConsecutiveNums FROM Logs a LEFT OUTER JOIN Logs b ON a.id1 b.id LEFT OUTER JOIN Logs c ON a.id2 c.id WHERE if(a.num b.num AND b.num c.num,a.num,null) IS NOT NULL603. 连…

qml MouseArea详解

1. 概述 MouseArea 是 QML 中用于处理鼠标事件的一个非常重要的项&#xff08;Item&#xff09;。它允许开发者响应鼠标的点击、拖拽、悬停等操作。MouseArea 可以与任何 QML 项目&#xff08;如 Rectangle, Image, Text 等&#xff09;结合使用&#xff0c;用于实现用户交互。…

Git快速入门(三)·远程仓库GitHub以及Gitee的使用

目录 1. 远程仓库GitHub 1.1 登录 1.2 创建库 1.3 创建文件 1.4 修改文件 1.5 创建分支 1.6 删除库 1.7 将远程仓库下载到本地 1.7.1 关联登录 1.7.2 克隆 1.7.3 通过GitHub Desktop更改远程库 2. 远程仓库Gitee 2.1 登录 2.2 创建文件 2.3 关联…

Uncaught ReferenceError: __VUE_HMR_RUNTIME__ is not defined

Syntax Error: Error: vitejs/plugin-vue requires vue (>3.2.13) or vue/compiler-sfc to be present in the dependency tree. 第一步 npm install vue/compiler-sfc npm run dev 运行成功&#xff0c;本地打开页面是空白&#xff0c;控制台报错 重新下载了vue-loa…

Rockect基于Dledger的Broker主从同步原理

1.前言 此文章是在儒猿课程中的学习笔记&#xff0c;感兴趣的想看原来的课程可以去咨询儒猿课堂 这篇文章紧挨着上一篇博客来进行编写&#xff0c;有些不清楚的可以看下上一篇博客&#xff1a; RocketMQ原理简述&#xff08;二&#xff09;-CSDN博客 2.Broker的高可用 如果…

企业为何需要小型语言模型:AI 应用的新趋势与策略

在人工智能蓬勃发展的当下&#xff0c;语言模型作为其中的关键技术&#xff08;LLM的擅长与不擅长&#xff1a;深入剖析大语言模型的能力边界&#xff09;&#xff0c;深刻影响着各个行业的发展和企业的运营模式。长期以来&#xff0c;“越大越好” 的理念在人工智能领域根深蒂…