webserver服务器从零搭建到上线(八)|EpollPoller事件分发器类

文章目录

终于要开始我们的重点:事件分发起和事件循环了,在这里我们将揭开事件驱动的IO多路复用模型的神秘面纱!

EpollPoller事件分发器类

成员变量和成员函数解释

这些都是在头文件中声明的,我们可以先对类中封装的各个方法进行合理的研究和猜测。

私有的成员函数和成员变量

在这里我们简单介绍一下私有成员函数和成员变量
私有成员函数如下

cpp 复制代码
void fillActiveChannels(int numEvents, ChannelList *activeChannels) const;
void update(int operation, Channel *channel);
  • fillActiveChannels()这里主要就是将 epoll_wait 返回的活跃事件填充到 activeChannels中。
  • update()这里的根据操作类型(添加、修改、删除),调用epoll_ctl来更新epoll实例中的Channel对象。
    在Channel类中,我们也写了一个update,它的具体实现是loop_->updateChannel(this);,调用了EventLoop中的updateChannel,所以我们有理由怀疑,其中的updateChannel()就是在调用这里的update方法

私有成员变量

cpp 复制代码
static const int kInitEventListSize = 16;
using EventList = std::vector<epoll_event>;

int epollfd_;
EventList events_;
  • kInitEventListSize :初始事件列表大小。
  • EventList :用于存储epoll事件的向量类型。
  • int epollfd_ :epoll实例的文件描述符。
  • EventList events_ :存储从epoll_wait返回的事件列表。

成员函数

cpp 复制代码
    EPollPoller(EventLoop *Loop);
    ~EPollPoller() override;

    //重写基类Poller的抽象方法
    Timestamp poll(int timeoutMs, ChannelList *activeChannels) override;
    void updateChannel(Channel *channel) override;
    void removeChannel(Channel *channel) override;
  • Timestamp poll(int timeoutMs, ChannelList *activeChannels)
    • 调用epoll_wait等待事件发生,将活跃的事件填充到activeChannels中。
  • void updateChannel(Channel *channel)
    • 更新或添加一个Channel对象到epoll实例中,调用epoll_ctl。
  • void removeChannel(Channel *channel)
    • 从epoll实例中移除一个Channel对象,调用epoll_ctl。

具体实现

cpp 复制代码
#include "EpollPoller.h"
#include "Logger.h"
#include "Channel.h"

#include <errno.h>
#include <unistd.h>
#include <strings.h>

const int kNew = -1;
const int kAdded = 1;
const int kDeleted = -1;

EPollPoller::EPollPoller(EventLoop *loop) 
    : Poller(loop)
    , epollfd_(::epoll_create1(EPOLL_CLOEXEC))
    , events_(kInitEventListSize) { //创建了vector<epoll_events>
        if (epollfd_ < 0) LOG_FATAL("epoll_create error:%d\n", errno);

}

EPollPoller::~EPollPoller() {
    ::close(epollfd_);
}

Timestamp EPollPoller::poll(int timeoutMs, ChannelList *activeChannels)
{
    LOG_INFO("func=%s => fd total count:%lu \n"
        , __FUNCTION__
        , channels_.size());
    
    int numEvents = ::epoll_wait(epollfd_
                                 , &*events_.begin()
                                 , static_cast<int>(events_.size())
                                 , timeoutMs);
    int saveErrno = errno;
    Timestamp now(Timestamp::now());

    if (numEvents > 0) {
        LOG_INFO("%d events happened \n", numEvents);
        fillActiveChannels(numEvents, activeChannels);
        if (numEvents == events_.size()) {
            events_.resize(events_.size() * 2);
        }
    } else if (numEvents == 0) {
        LOG_DEBUG("%s timeout! \n", __FUNCTION__);
    } else {
        if (saveErrno != EINTR) {
            errno = saveErrno;
            LOG_ERROR("EPollPoller::poll() err!");
        }
    }

    return Timestamp();
}

void EPollPoller::updateChannel(Channel *channel) {
    const int index = channel->index();
    // LOG_INFO("func=%s =>fd=%d events=%d index=%d\n"
    //     , __FUNCTION__
    //     , channel->fd
    //     , channel->events()
    //     , index)
    if (index == kNew || index == kDeleted) {
        if (index == kNew) {
            int fd = channel->fd();
            channels_[fd] = channel;
        }

        channel->set_index(kAdded);
        update(EPOLL_CTL_ADD, channel);
    } else { //说明channel已经在Poller注册过了
        int fd = channel->fd();
        if (channel->isNoneEvent()) {
            update(EPOLL_CTL_DEL, channel);
            channel->set_index(kDeleted);
        } else {
            update(EPOLL_CTL_MOD, channel);
        }

    }
}

//从poller中删除channel
void EPollPoller::removeChannel(Channel *channel) {
    int fd = channel->fd();
    channels_.erase(fd);

    LOG_INFO("func=%s => fd=%d\n", __FUNCTION__, fd);

    int index = channel->index();
    if (index == kAdded) {
        update(EPOLL_CTL_DEL, channel);
    }
    channel->set_index(kNew);
}

//填写活跃的连接
void EPollPoller::fillActiveChannels(int numEvents, ChannelList *activeChannels) const {
    for (int i = 0; i < numEvents; ++i) {
        Channel *channel = static_cast<Channel*>(events_[i].data.ptr);
        channel->set_revents(events_[i].events);
        activeChannels->push_back(channel); //EventLoop就拿到了它的poller给它返回的所有发生事件的channel列表了
    }
}

//更新channel通道 epoll_ctl add/mod/del
void EPollPoller::update(int operation, Channel *channel) {
    epoll_event event;
    bzero(&event, sizeof event);

    int fd = channel->fd();

    event.events = channel->events();
    event.data.fd = fd;
    event.data.ptr = channel;

    if (::epoll_ctl(epollfd_, operation, fd, &event) < 0) {
        if (operation == EPOLL_CTL_DEL) {
            LOG_ERROR("epoll_ctl del error:%d\n", errno);
        } else {
            LOG_FATAL("epoll_ctl add/mod error:%d\n", errno);
        }
    }
}

常量的作用

cpp 复制代码
// channel未添加到poller中
const int kNew = -1;  // channel的成员index_ = -1
// channel已添加到poller中
const int kAdded = 1;
// channel从poller中删除
const int kDeleted = 2;

他们主要用于表示 channel的状态,在后续的方法具体实现中会体现到。

构造函数和析构函数

cpp 复制代码
EPollPoller::EPollPoller(EventLoop *loop)
    : Poller(loop)
    , epollfd_(::epoll_create1(EPOLL_CLOEXEC))
    , events_(kInitEventListSize)  // vector<epoll_event>
{
    if (epollfd_ < 0)
    {
        LOG_FATAL("epoll_create error:%d \n", errno);
    }
}

EPollPoller::~EPollPoller() 
{
    ::close(epollfd_);
}
  • 构造函数:创建一个epoll实例epollfd_,随后我们需要初始化我们所关注的事件列表大小events_
  • 析构函数:我们知道,我们将所有监控的事件都委托给了内核的epoll实例来进行管理,该实例底层是一颗红黑树。我们最后析构的时候,可以直接关闭close,就可以关闭所有网络IO的文件描述符了。

⭐️poll函数

cpp 复制代码
Timestamp EPollPoller::poll(int timeoutMs, ChannelList *activeChannels)
{
    LOG_INFO("func=%s => fd total count:%lu \n"
        , __FUNCTION__
        , channels_.size());
    
    int numEvents = ::epoll_wait(epollfd_
                                 , &*events_.begin()
                                 , static_cast<int>(events_.size())
                                 , timeoutMs);
    int saveErrno = errno;
    Timestamp now(Timestamp::now());

    if (numEvents > 0) {
        LOG_INFO("%d events happened \n", numEvents);
        fillActiveChannels(numEvents, activeChannels);
        if (numEvents == events_.size()) {
            events_.resize(events_.size() * 2);
        }
    } else if (numEvents == 0) {
        LOG_DEBUG("%s timeout! \n", __FUNCTION__);
    } else {
        if (saveErrno != EINTR) {
            errno = saveErrno;
            LOG_ERROR("EPollPoller::poll() err!");
        }
    }

    return Timestamp();
}

他就是实现我们多路分发的函数:

  • poll 函数使用 epoll_wait 等待事件发生,并将活跃的事件填充到 activeChannels 中。
  • 如果发生事件,将这些事件填充到 activeChannels,并在必要时扩展事件列表。
  • 返回当前的时间戳,主要是为了后续方便打日志和进行管理。

updateChannel函数

cpp 复制代码
void EPollPoller::updateChannel(Channel *channel)
{
    const int index = channel->index();
    LOG_INFO("func=%s => fd=%d events=%d index=%d \n", __FUNCTION__, channel->fd(), channel->events(), index);

    if (index == kNew || index == kDeleted)
    {
        int fd = channel->fd();
        if (index == kNew)
        {
            channels_[fd] = channel;
        }
        channel->set_index(kAdded);
        update(EPOLL_CTL_ADD, channel);
    }
    else
    {
        int fd = channel->fd();
        if (channel->isNoneEvent())
        {
            update(EPOLL_CTL_DEL, channel);
            channel->set_index(kDeleted);
        }
        else
        {
            update(EPOLL_CTL_MOD, channel);
        }
    }
}
  • updateChannel 函数根据 Channel 的当前状态(新添加或已删除)来决定是否添加或更新 epoll 实例中的事件,该函数肯定会被EventLoop封装,然后再由Channel自己来进行调用。
  • 如果是新添加的 Channel,则在 epoll 中注册该文件描述符。
  • 如果 Channel 没有感兴趣的事件,则将其从 epoll 中删除。

removeChannel 函数

cpp 复制代码
void EPollPoller::removeChannel(Channel *channel)
{
    int fd = channel->fd();
    LOG_INFO("func=%s => fd=%d\n", __FUNCTION__, fd);
    
    int index = channel->index();
    if (index == kAdded)
    {
        update(EPOLL_CTL_DEL, channel);
    }
    channel->set_index(kNew);
}
  • removeChannel 函数将 Channel 从 epoll 实例中删除,并更新其状态。这一看就是我们的EventLoop需要调用的函数。

removeChannelupdateChannel

从这两个函数理我们可以看出,他们其实是为EventLoop提供操作Channel的方法。从代码的具体实现细节来看,我们可以领略到 channel 为什么要设置一个 index_ 标志,主要就是为了实现channel的复用,我们总不能每次有新连接都新建一个channel,连接断开就删除channel吧!

⭐️fillActiveChannels 函数

cpp 复制代码
void EPollPoller::fillActiveChannels(int numEvents, ChannelList *activeChannels) const
{
    for (int i = 0; i < numEvents; ++i)
    {
        Channel *channel = static_cast<Channel*>(events_[i].data.ptr);
        channel->set_revents(events_[i].events);
        activeChannels->push_back(channel);
    }
}
  • fillActiveChannels 函数将 epoll_wait 返回的所有活跃事件填充到 activeChannels 列表中。

  • 然手我们介绍一下 event.data,我们将已经被激活的event直接拿到手,这里就需要用到我们的event.data.ptr:
    data字段是一个联合体,具体结构包含了我们常用的int fdvoid *ptr

    ptr 是一个通用指针,可以用来指向任何类型的数据。它通常用于关联用户自定义的数据结构(这里是我们的Channel*),以便在事件触发时可以快速访问这些数据。例如,你可以将 ptr 设置为你的应用程序中某个特定对象的指针,当对应的文件描述符触发事件时,你的应用程序可以通过 ptr 直接访问到这个对象

  • 然后调用channel的set_revents方法,可以将已经被激活的事件直接初始化到我们的channel中。

  • 随后把 channel 推到我们的 activeChannels

⭐️update 函数

cpp 复制代码
void EPollPoller::update(int operation, Channel *channel)
{
    epoll_event event;
    bzero(&event, sizeof event);
    
    int fd = channel->fd();

    event.events = channel->events();
    event.data.fd = fd; 
    event.data.ptr = channel;
    
    if (::epoll_ctl(epollfd_, operation, fd, &event) < 0)
    {
        if (operation == EPOLL_CTL_DEL)
        {
            LOG_ERROR("epoll_ctl del error:%d\n", errno);
        }
        else
        {
            LOG_FATAL("epoll_ctl add/mod error:%d\n", errno);
        }
    }
}

update 函数根据操作类型(添加、修改或删除)调用 epoll_ctl 来更新 epoll 实例中的 Channel。

其实说白了update就是用来封装epoll_ctl的。

该函数被 EPollPoller::removeChannelEPollPoller::updateChannel调用,用来更新Channel的封装的fd以及其需要监控的相关事件。

总结

EPollPoller 类实现了基于 epoll 的 I/O 多路复用,通过监控多个文件描述符上的事件,并在事件发生时通知相应的 Channel 对象来处理事件。通过实现这些函数,EPollPoller 能够高效地管理和分发事件。

下一节,我们将讲解EventLoop类的具体实现!

相关推荐
流星白龙几秒前
【C++习题】10.反转字符串中的单词 lll
开发语言·c++
Smile丶凉轩23 分钟前
微服务即时通讯系统的实现(服务端)----(1)
c++·git·微服务·github
萝卜兽编程1 小时前
优先级队列
c++·算法
努力的悟空1 小时前
国土变更调查拓扑错误自动化修复工具的研究
运维·自动化
旦沐已成舟2 小时前
DevOps-Jenkins-新手入门级
服务器
珹洺2 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
孙同学要努力2 小时前
C++知识整理day1——前置基础知识整理(命名空间、输入输出、函数重载、引用)
开发语言·c++
沐泽Mu2 小时前
嵌入式学习-C嘎嘎-Day05
开发语言·c++·学习
几窗花鸢2 小时前
力扣面试经典 150(下)
数据结构·c++·算法·leetcode
周末不下雨3 小时前
win11+ubuntu22.04双系统 | 联想 24 y7000p | ubuntu 22.04 | 把ubuntu系统装到1T的移动固态硬盘上!!!
linux·运维·ubuntu