手写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中的channel
、kAdded代表已经注册到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 方法:根据
operation
(EPOLL_CTL_ADD
、EPOLL_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 网络库中的事件分发器,我们深入了解了网络编程中事件处理的核心机制,同时学习到了优秀的代码设计经验。希望本文对你有所帮助。