『 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.mzph.cn/bicheng/66130.shtml

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

相关文章

PDF预览插件

PDF预览插件 可用于当前页面弹窗形式查看,可增加一些自定义功能 pdf预览插件 代码块: pdfobject.js <div class="pdfwrap"><div class="item"><h3>笑场</h3><div class="tags"><p>李诞</p><i&…

【Java项目】基于SpringBoot的【新生宿舍管理系统】

【Java项目】基于SpringBoot的【新生宿舍管理系统】 技术简介&#xff1a;本系统使用采用B/S架构、Spring Boot框架、MYSQL数据库进行开发设计。 系统简介&#xff1a;管理员登录进入新生宿舍管理系统可以查看首页、个人中心、公告信息管理、院系管理、班级管理、学生管理、宿舍…

Huginn - 构建代理、执行自动化任务

文章目录 一、关于 Huginn什么是Huginn&#xff1f;Huginn 功能加入Huginn展示 二、安装1、Docker2、本地安装3、开发 三、使用Huginn代理gems四、部署1、Heroku2、OpenShiftOpenShift 在线 3、在任何服务器上手动安装4、可选设置4.1 私人开发设置4.2 启用WeatherAgent4.3 禁用…

电子应用设计方案86:智能 AI背景墙系统设计

智能 AI 背景墙系统设计 一、引言 智能 AI 背景墙系统旨在为用户创造一个动态、个性化且具有交互性的空间装饰体验&#xff0c;通过融合先进的技术和创意设计&#xff0c;提升室内环境的美观度和功能性。 二、系统概述 1. 系统目标 - 提供多种主题和风格的背景墙显示效果&…

基于Spring Boot的IT技术交流和分享平台的设计与实现源码

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的IT技术交流和分享平台的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 基于S…

单元测试3.0+ @RunWith(JMockit.class)+mock+injectable+Expectations

Jmockit使用笔记_基本功能使用Tested_Injectable_Mocked_Expectations_jmockit.class-CSDN博客 静态变量直接赋值就好&#xff0c;没必要mock了 测试框架Jmockit集合junit使用 RunWith(JMockit.class) 写在测试案例类上的注解 Tested 在测试案例中,写在我们要测试的类上…

PADS Logic原理图中有很多页原理图,如何(怎样)删除其中一页或者多页

我们在进行PADS Logic进行原理图设计的时候&#xff0c;有时候可能遇到一次性设计了很多页的原理图&#xff0c;比如说十几页的原理图。那么我们在进行PADS Layout的时候&#xff0c;可能将这些原理图绘制两块板或者多块PCB板&#xff0c;那么这时候我们需要将其中的一张原理图…

Elasticsearch 创建索引 Mapping映射属性 索引库操作 增删改查

Mapping Type映射属性 mapping是对索引库中文档的约束&#xff0c;有以下类型。 text&#xff1a;用于分析和全文搜索&#xff0c;通常适用于长文本字段。keyword&#xff1a;用于精确匹配&#xff0c;不会进行分析&#xff0c;适用于标签、ID 等精确匹配场景。integer、long…

《GICv3_Software_Overview_Official_Release_B》学习笔记

1.不同版本的 GIC 架构及其主要功能如下图所示&#xff1a; 2.GICv2m&#xff08;Generic Interrupt Controller Virtualization Model&#xff09;是针对ARM架构的GIC&#xff08;通用中断控制器&#xff09;的一种扩展&#xff0c; GICv2m扩展为虚拟化环境中的中断管理提供了…

【QT】找不到qwt_plot.h

系统环境&#xff1a; linux 20.04 qt 6.7.2 cmake 3.22 原因&#xff1a; Qwt没有正式的FindQwt.cmake&#xff0c;Qwt也没有提供QwtConfig.cmake。而且cmake不支持qmake的配置特性&#xff0c;也不支持读取mkspecs (.prf)文件。也就是说cmake构建的qt项目不可用qwt。 解决步…

杰发科技——使用ATCLinkTool解除读保护

0. 原因 在jlink供电电压不稳定的情况下&#xff0c;概率性出现读保护问题&#xff0c;量产时候可以通过离线烧录工具避免。代码中开了读保护&#xff0c;但是没有通过can/uart/lin/gpio控制等方式进行关闭&#xff0c;导致无法关闭读保护。杰发所有芯片都可以用本方式解除读保…

Sublime Text4 4189 安装激活【 2025年1月3日 亲测可用】

-----------------测试时间2025年1月3日------------------- 下载地址 官方网址&#xff1a;https://www.sublimetext.com 更新日志&#xff1a;https://www.sublimetext.com/download V4189 64位&#xff1a;https://www.sublimetext.com/download_thanks?targetwin-x64 ....…

前后端规约

文章目录 引言I 【强制】前后端交互的 API请求内容响应体响应码II 【推荐】MVC响应体III【参考】IV 其他引言 服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否则会因线上采用 HTTPS 协议而导致浏览器提示“不安全”,并且还会带来 URL 维护…

linux安装redis及Python操作redis

目录 一、Redis安装 1、下载安装包 2、解压文件 3、迁移文件夹 4、编译 5、管理redis文件 6、修改配置文件 7、启动Redis 8、将redis服务交给systemd管理 二、Redis介绍 1、数据结构 ①字符串String ②列表List ③哈希Hash ④集合Set ⑤有序集合Sorted Set 2、…

在线RSA pem 密钥pkcs1转pkcs8格式--支持公钥和私钥

具体前往&#xff1a;在线RSA密钥pkcs1转pkcs8--在线将RSA私钥/公钥的pkcs1格式转换到pkcs8格式,支持pem格式

修复OpenLinkSaas客户端在使用AtomGit账号时页面崩溃

问题描述&#xff1a;当一个新的AtomGit用户登录OpenLinkSaas客户端后出现了页面崩溃。 从浏览器控制台来看&#xff0c;是gitNoticeList出现了null。 查看代码后发现是请求atomGit api是返回的一个null的列表 接下来我们加下保护性的代码&#xff0c;来兼容null或undefine的情…

rocketmq-pull模式-消费重平衡和拉取PullTaskImpl线程

1、观察consumer的线程模型 使用arthas分析 MQClientFactoryScheduledThread 定时任务线程 &#xff08;和push模式一致&#xff09; 定时任务线程&#xff0c;包含如下任务&#xff1a; 每2分钟更新nameServer列表 每30秒更新topic的路由信息 每30秒检查broker的存活&#x…

Ungoogled Chromium127 编译指南 MacOS 篇(二)- 项目要求

1. 引言 在开始编译 Ungoogled Chromium 之前&#xff0c;我们需要确保系统满足所有必要的硬件和软件要求。由于浏览器编译是一个资源密集型的任务&#xff0c;合适的硬件配置和完整的软件环境至关重要。本文将详细介绍编译 Ungoogled Chromium 所需的各项要求。 2. 硬件要求…

51单片机——共阴数码管实验

数码管中有8位数字&#xff0c;从右往左分别为LED1、LED2、...、LED8&#xff0c;如下图所示 如何实现点亮单个数字&#xff0c;用下图中的ABC来实现 P2.2管脚控制A&#xff0c;P2.3管脚控制B&#xff0c;P2.4管脚控制C //定义数码管位选管脚 sbit LSAP2^2; sbit LSBP2^3; s…

调试:用电脑开发移动端网页,然后用手机真机调试

一、背景 电脑开发移动端&#xff0c;然后想真机调试... 二、实现 2.1、电脑和手机链接相同局域网 2.2、pnpm run dev 启动项目 2.3、浏览器访问 localhost:3001/login 2.4、Windowsr 输入cmd&#xff0c;在cmd输入 ipconfig 2.5、浏览器访问 ip地址加/login 2.6、手机端…