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

手写muduo网络库(三):事件分发器(Poller,EPollPoller实现)

一、引言

在网络编程里,高效处理多个 I/O 事件是关键。Muduo 网络库为我们提供了很好的解决方案,其中事件分发器(Poller)是核心组件之一。本文将详细剖析 muduo 网络库中事件分发器的实现,包含抽象基类 Poller 和具体实现类 EPollPoller,同时会解释 DefaultPoller 文件存在的意义。

二、Poller 抽象基类

2.1 头文件 Poller.h

#pragma once#include "NonCopyable.h"
#include "Timestamp.h"#include <vector>
#include <unordered_map>class Channel;
class EventLoop;class Poller : NonCopyable
{
public:using ChannelList = std::vector<Channel *>;Poller(EventLoop *loop);virtual ~Poller() = default;virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0;virtual void updateChannel(Channel *channel) = 0;virtual void removeChannel(Channel *channel) = 0;bool hasChannel(Channel *channel) const;static Poller *newDefaultPoller(EventLoop *loop);protected:using ChannelMap = std::unordered_map<int, Channel *>;ChannelMap channels_;private:EventLoop *ownerLoop_; 
};
  • 作用Poller 作为抽象基类,定义了事件分发器的通用接口,为不同的事件分发策略提供了统一的抽象,方便后续扩展不同的具体实现。
  • 成员解释
    • ChannelList:使用 std::vector<Channel *> 来存储发生事件的 Channel 指针。
    • poll 纯虚函数:用于等待事件发生(epoll_wait的封装),并将活跃的 Channel 填充到 activeChannels 中(由上层提供一个空的activeChannels ,poll方法填充并返回给上层处理),返回事件发生的时间戳。
    • updateChannel 纯虚函数:用于更新 Channel 关注的事件。
    • removeChannel 纯虚函数:用于从事件分发器中移除 Channel
    • hasChannel 方法:用于检查指定的 Channel 是否已经在 Poller 中注册。
    • newDefaultPoller 静态方法:用于创建默认的 Poller 实例。
    • channels_:使用 std::unordered_map<int, Channel *> 来管理所有的 Channel 对象(注册到epoll或曾经注册到epoll但没有被remove掉的),键为文件描述符,值为 Channel 指针,方便快速查找和管理。
    • ownerLoop_:指向所属的 EventLoop,确保 Poller 与 EventLoop 关联。

类成员中ownerLoop_生命周期长于Poller对象,不用再析构函数中管理,所以直接使用default析构;同时要注意的是Poller是基类,析构函数应该定义为虚函数,以确保派生类在析构时可以调用到基类析构函数。

2.2 实现文件 Poller.cpp

#include "Poller.h"
#include "Channel.h"Poller::Poller(EventLoop *loop): ownerLoop_(loop)
{
}bool Poller::hasChannel(Channel *channel) const
{auto it = channels_.find(channel->fd());return it != channels_.end() && it->second == channel;
}
  • 构造函数:初始化 ownerLoop_,将 Poller 与所属的 EventLoop 关联起来。
  • hasChannel 方法:通过查找 channels_ 中是否存在指定 Channel 的文件描述符,并且该描述符对应的 Channel 指针与传入的 Channel 指针相同,来判断 Channel 是否已经注册。

三、DefaultPoller 文件的作用

3.1 DefaultPoller.cpp

#include "Poller.h"
#include "EPollPoller.h"#include <stdlib.h>Poller *Poller::newDefaultPoller(EventLoop *loop)
{if (::getenv("MUDUO_USE_POLL"))//检查环境变量是否注册{return nullptr; // 如果注册过,生成poll的实例,这里为了简单没有实现}else{return new EPollPoller(loop); // 默认生成epoll的实例}
}
  • 作用DefaultPoller 文件的存在主要是为了实现默认 Poller 实例的创建,并且遵循头文件尽可能少暴露的原则。
  • 好处
    • 解耦创建逻辑:将默认 Poller 实例的创建逻辑单独放在一个文件中,避免在头文件中暴露具体的 Poller 实现细节,如 EPollPoller。这样,其他模块在使用 Poller 时,只需要包含 Poller.h 头文件,而不需要关心具体的 Poller 实现。
    • 灵活性:通过环境变量 MUDUO_USE_POLL 来选择默认的 Poller 实现,方便在不同的环境下进行切换。如果设置了该环境变量,则可以返回 nullptr(表示使用 poll),否则返回 EPollPoller 的实例。

四、EPollPoller 具体实现

4.1 头文件 EPollPoller.h

#pragma once#include "Poller.h"
#include "Timestamp.h"#include <vector>
#include <sys/epoll.h>class Channel;class EPollPoller : public Poller
{
public:EPollPoller(EventLoop *loop);~EPollPoller() override;Timestamp poll(int timeoutMs, ChannelList *activeChannels) override;void updateChannel(Channel *channel) override;void removeChannel(Channel *channel) override;private:static const int kInitEventListSize = 16;void fillActiveChannels(int numEvents, ChannelList *activeChannels) const;void update(int operation, Channel *channel);using EventList = std::vector<epoll_event>;int epollfd_;EventList events_; 
};
  • 作用EPollPoller 是 Poller 的具体实现类,使用 epoll 机制来处理事件。
  • 成员解释
    • kInitEventListSize:初始化 events_ 向量的大小。
    • fillActiveChannels 方法:用于将 epoll_wait 返回的活跃事件对应的 Channel 填充到 activeChannels 中。
    • update 方法:用于调用 epoll_ctl 函数更新 epoll 实例中的事件。
    • epollfd_epoll 实例的文件描述符。
    • events_:用于存储 epoll_wait 返回的事件。

4.2 实现文件 EPollPoller.cpp

4.2.1 构造函数和析构函数
const int kNew = -1;    // 某个channel还没添加至Poller          // channel的成员index_初始化为-1
const int kAdded = 1;   // 某个channel已经添加至Poller
const int kDeleted = 2; // 某个channel已经从Poller删除EPollPoller::EPollPoller(EventLoop *loop): Poller(loop), epollfd_(::epoll_create1(EPOLL_CLOEXEC)) , events_(kInitEventListSize)
{if (epollfd_ < 0){LOG_ERROR << "epoll_create error: " << errno;exit(-1);}
}EPollPoller::~EPollPoller()
{::close(epollfd_);
}
  • 构造函数:调用 epoll_create1(EPOLL_CLOEXEC) 创建一个 epoll 实例,并初始化 events_ 向量。EPOLL_CLOEXEC 标志确保在子进程中关闭该文件描述符,避免资源泄漏。如果创建失败,则输出错误信息并退出程序。
  • 析构函数:关闭 epoll 文件描述符,释放资源,剩余资源再出作用域自动释放。
4.2.2 poll 方法
Timestamp EPollPoller::poll(int timeoutMs, ChannelList *activeChannels)
{LOG_DEBUG << "fd total count: " <<  channels_.size();//&*events_.begin()解释:events_是可变数组容器,通过begin方法得到器迭代器,对迭代器解引用并取地址得到器底层数组的首地址,这个是epoll_wait可以接受的参数。int numEvents = ::epoll_wait(epollfd_, &*events_.begin(), static_cast<int>(events_.size()), timeoutMs);int saveErrno = errno;Timestamp now(Timestamp::now());if (numEvents > 0){LOG_DEBUG << numEvents <<" events happend!!!";fillActiveChannels(numEvents, activeChannels);if (numEvents == events_.size()){events_.resize(events_.size() * 2);}}else if (numEvents == 0){LOG_DEBUG << " timeout!";}else{if (saveErrno != EINTR){errno = saveErrno;LOG_ERROR << "EPollPoller::poll() error!";}}return now;
}
  • 事件轮询:调用 epoll_wait 函数等待事件发生,超时时间为 timeoutMs
  • 事件处理
    • 如果有事件发生,调用 fillActiveChannels 方法将活跃的 Channel 添加到 activeChannels 中。
    • 如果 events_ 向量已满,则将其容量扩大一倍,以应对更多的事件。
  • 错误处理:如果 epoll_wait 返回错误,且不是被信号中断,则输出错误信息。
4.2.3 updateChannel 方法
void EPollPoller::updateChannel(Channel *channel)
{const int index = channel->index();LOG_INFO << "update fd= " << channel->fd() << " events= " << channel->events() << " index= "  << index;if (index == kNew || index == kDeleted){if (index == kNew){int fd = channel->fd();channels_[fd] = channel;}else{}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);}}
}
  • 通道状态判断:根据 Channel 的 index 值判断其状态(kNew代表没有被注册到channelmap和epoll中的channelkAdded代表已经注册到channelmap和epoll中的channel , kDeleted代表注册到channelmap但从epoll中删除中的channel)。
  • 添加通道:如果 Channel 是新的,则将其添加到 channels_ 中,并调用 update 方法使用 EPOLL_CTL_ADD 操作将其添加到 epoll 实例中。
  • 更新或删除通道:如果 Channel 已经注册过,且没有关注的事件,则使用 EPOLL_CTL_DEL 操作从 epoll 实例中删除;否则使用 EPOLL_CTL_MOD 操作更新事件。
4.2.4 removeChannel 方法
void EPollPoller::removeChannel(Channel *channel)
{int fd = channel->fd();channels_.erase(fd);LOG_INFO << "remove fd= " << fd;int index = channel->index();if (index == kAdded){update(EPOLL_CTL_DEL, channel);}channel->set_index(kNew);
}
  • 移除通道:从 channels_ 中移除指定的 Channel,并调用 update 方法使用 EPOLL_CTL_DEL 操作从 epoll 实例中删除。最后将 Channel 的状态设置为 kNew
4.2.5 辅助方法
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);}
}void EPollPoller::update(int operation, Channel *channel)
{epoll_event event;::memset(&event, 0, 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: " << errno;}else{LOG_ERROR << "epoll_ctl add/mod error: " << errno;exit(-1);}}
}
  • fillActiveChannels 方法:将 epoll_wait 返回的活跃事件对应的 Channel 添加到 activeChannels 中,并设置其 revents
  • update 方法:根据 operationEPOLL_CTL_ADDEPOLL_CTL_MOD 或 EPOLL_CTL_DEL)调用 epoll_ctl 函数更新 epoll 实例中的事件。如果操作失败,输出相应的错误信息。

五、代码设计经验总结

  • 抽象基类和多态:使用抽象基类 Poller 和纯虚函数实现多态,提高了代码的可扩展性和可维护性。不同的事件分发策略可以通过继承 Poller 类并实现具体的方法来实现。
  • 静态工厂方法:通过静态工厂方法 newDefaultPoller 选择不同的实现,增加了代码的灵活性。可以根据环境变量来选择默认的 Poller 实现。
  • 资源管理:在构造函数中分配资源(如创建 epoll 实例),在析构函数中释放资源(如关闭 epoll 文件描述符),遵循 RAII (资源创建即初始化)原则,避免了资源泄漏。
  • 错误处理:在关键操作中进行错误处理,输出详细的错误信息,方便调试和维护。例如,在创建 epoll 实例、调用 epoll_wait 和 epoll_ctl 时,都进行了错误检查和处理。
  • 头文件暴露控制:将默认 Poller 实例的创建逻辑单独放在 DefaultPoller.cpp 文件中,避免在 Poller.h 头文件中暴露具体的 Poller 实现细节,减少了头文件的暴露,提高了代码的封装性。

通过手写 muduo 网络库中的事件分发器,我们深入了解了网络编程中事件处理的核心机制,同时学习到了优秀的代码设计经验。希望本文对你有所帮助。

 

相关文章:

  • 邮科OEM摄像头图像处理技术:从硬件协同到智能进化
  • 微软PowerBI考试 PL300-在 Power BI 中设计语义模型 【附练习数据】
  • 高考倒计时(vb.net,持续更新版本)
  • 7.3.折半查找(二分查找)
  • Leetcode 3578. Count Partitions With Max-Min Difference at Most K
  • Oracle SQL*Plus 配置上下翻页功能
  • 行为设计模式之Memento(备忘录)
  • Linux 删除登录痕迹
  • 多面体编译的循环分块
  • 字符串方法_indexOf() +_trim()+_split()
  • 定制化平板电脑在各行业中有哪些用途与作用?
  • CppCon 2015 学习:Give me fifteen minutes and I’ll change your view of GDB
  • 【Java多线程从青铜到王者】懒汉模式的优化(九)
  • 【GESP真题解析】第 2 集 GESP 四级样题卷编程题 1:绝对素数
  • IK分词器
  • 模型训练-关于token【低概率token, 高熵token】
  • Q: dify的QA分段方式,question、answer和keywords哪些内容进入向量库呢?
  • Python主动抛出异常详解:掌握raise关键字的艺术
  • 力扣HOT100之堆:347. 前 K 个高频元素
  • TDengine 快速体验(云服务方式)
  • 国外建站主机/在哪个平台做推广比较好
  • 网站做404好处/广州网站营销推广
  • 网站建设评语/深圳将进一步优化防控措施
  • 甘肃住房和城乡建设局网站/网址导航下载到桌面
  • 视频网站的防盗链是怎么做的/长沙网站提升排名
  • 开封建站公司/河南网站seo费用