王海鱼 发表于 2025-2-22 07:20:14

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

前情提要

在上一篇博客『 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服务器;
且实在现方式较Select与Poll两种多路转接方案还要简单(尤其是在对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;
      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.events; // 获取事件
            int fd = revs.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;
      int n = read(fd, inbuff, sizeof(inbuff) - 1);
      if (n > 0)
      {
            inbuff = 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 测试及完备代码

https://i-blog.csdnimg.cn/img_convert/ee6af8540ab06f4e768452e486ccf5af.png#pic_center
从测试效果可以看出,其效果与Select方案Poll方案多路转接所实现的EchoServer雷同,且其效率要比另两种方案多路转接方案更为优秀;


[*] 完备代码(供参考)
[半介莽夫 - Gitee For half-intermediate-mangfu/IO/AdvancedIO/EpollServer ]

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 『 Linux 』高级IO (三) - Epoll模型的封装与EpollEchoServer服务器