WebServer(3) Poller类

本文最后更新于:8 个月前

WebServer(3) Poller类

写在前面

源码链接:https://github.com/yxiao-yang/Tiny-Webserver.git

正文

1 基类Poller的设计

我们编写网络编程代码的时候少不了使用IO复用系列函数,而 muduo 也为我们提供了对此的封装。 muduo 有 Poller 和 EPollPoller 类分别对应着epollpoll。而我们使用的接口是Poller, muduo 以 Poller 为虚基类,派生出 Poller 和 EPollPoller 两个子类,用不同的形式实现 IO 复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;

// 判断 channel是否注册到 poller当中
bool HasChannel(Channel *channel) const;

// EventLoop可以通过该接口获取默认的IO复用实现方式(默认epoll)
/**
* 它的实现并不在 Poller.cc 文件中
* 如果要实现则可以预料会其会包含EPollPoller PollPoller
* 那么外面就会在基类引用派生类的头文件,这个抽象的设计就不好
* 所以外面会单独创建一个 DefaultPoller.cc 的文件去实现
*/
static Poller* NewDefaultPoller(EventLoop *Loop);

protected:
using ChannelMap = std::unordered_map<int, Channel*>;
// 储存 channel 的映射,(sockfd -> channel*)
ChannelMap channels_;

private:
EventLoop *ownerLoop_; // 定义Poller所属的事件循环EventLoop
};
  • ChannelMap channels_ 需要存储从 fd -> channel 的映射
  • ownerLoop_ 定义 Poller 所属的事件循环 EventLoop

重写方法靠派生类实现,这里我们可以专注一下 NewDefaultPoller 方法。

在 muduo 中可以使用此方法获取不同的实例,并且这个方法是在单独的一个 DefaultPoller.cc 文件内实现的。正常情况下,我们可能会在 Poller.cc 文件中完成该成员函数的实现。但是这并不是一个好的设计,因为 Poller 是一个基类。如果在 Poller.cc 文件内实现则势必会在 Poller.cc包含 EPollPoller.h 等头文件。在一个基类中包含其派生类的头文件,这个设计可以说是很诡异的,这并不是一个好的抽象。

因此,我们专门设置了另一个DefaultPoller.cc 文件,在其中包含了 Poller.hEPollPoller.h 的头文件。这样就让 Poller.h 文件显得正常了。

1
2
3
4
5
6
7
Poller* Poller::NewDefaultPoller(EventLoop *loop) {
if (::getenv("MUDUO_USE_POLL")) {
return nullptr; // 生成poll实例
} else {
return new EPollPoller(loop); // 生成epoll实例
}
}

2 EPollPoller类设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class EPollPoller : public Poller {
using EventList = std::vector<epoll_event>;
public:
EPollPoller(EventLoop *Loop);
~EPollPoller() override;

// 重写基类Poller的抽象方法
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;

// 更新channel通道,本质是调用了epoll_ctl
void Update(int operation, Channel *channel);

int epollfd_; // epoll_create在内核创建空间返回的fd
EventList events_; // 用于存放epoll_wait返回的所有发生的事件的文件描述符
};
  • kInitEventListSize 默认监听事件的数量
  • epollfd_ 我们使用 epoll_create 创建的指向 epoll 对象的文件描述符(句柄)
  • EventList events_ 返回事件的数组

可以看到,EPollPoller类继承了Poller类,并打算重写这些核心方法。

3 成员函数

3.1 返回发生事件的 poll 方法

该方法内部调用 epoll_wait 获取发生的事件,并找到这些事件对应的 Channel 并将这些活跃的 Channel 填充入 activeChannels 中,最后返回一个时间戳。
通过 numEvents 的值判断事件情况

  • numEvents > 0 事件发生,需要调用 FillActiveChannels 填充活跃的 Channel。
  • numEvents == 0 事件超时了,打印日志。(可以设置定时器操作)
  • 其他情况则是出错,打印 LOG_ERROR日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Timestamp EPollPoller::Poll(int timeoutMs, ChannelList *activeChannels) {
// 高并发情况经常被调用,影响效率,使用debug模式可以手动关闭
size_t numEvents = ::epoll_wait(epollfd_, &(*events_.begin()),
static_cast<int>(events_.size()), timeoutMs);
int saveErrno = errno;
Timestamp now(Timestamp::now());

// 有事件产生
if (numEvents > 0) {
FillActiveChannels(numEvents, activeChannels); // 填充活跃的channels
// 对events_进行扩容操作
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() failed";
}
}
return now;
}

3.2 填写活跃的连接 FillActiveChannels

通过 epoll_wait 返回的 events 数组内部有指向 channel 的指针,我们可以通过此指针在 EPollPoller 模块获取对 channel 进行操作。
我们需要更新 channel 的返回事件的设置,并且将此 channel 装入 activeChannels。

1
2
3
4
5
6
7
8
void EPollPoller::FillActiveChannels(int numEvents, ChannelList *activeChannels) const {
for (int i = 0; i < numEvents; ++i) {
// void* => Channel*
Channel *channel = static_cast<Channel*>(events_[i].data.ptr);
channel->Set_revents(events_[i].events);
activeChannels->push_back(channel);
}
}

3.3 更新channel在epoll上的状态

我们获取 channel 在 EPollPoller 上的状态,根据状态进行不同操作。最后调用 Update 私有方法。

  • 如果此 channel 还没有被添加到 epoll 上或者是之前已经从 epoll 上注销index == kNew || index == kDeleted,那么此 channel 接下来会进行添加操作。
    • 如果是未添加状态kNew,则需要在 map 上增加此 channel
    • 如果是已删除状态kDeleted,则直接往后执行
    • 设置 channel 状态为 kAdded ,然后调用 Update(EPOLL_CTL_ADD, channel);
  • 如果已经在 poller 上注册的状态,则要进行删除或修改操作,需要判断此 channel 是否还有监视的事情(是否还要事件要等着处理)
    • 如果没有则直接删除,调用 Update(EPOLL_CTL_DEL, channel); 并重新设置状态为 kDeleted
    • 如果还有要监视的事情,则说明要进行修改(MOD)操作,调用 update(EPOLL_CTL_MOD, channel);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void EPollPoller::UpdateChannel(Channel *channel) {
// TODO:__FUNCTION__
// 获取参数channel在epoll的状态
const int index = channel->index();

// 未添加状态和已删除状态都有可能会被再次添加到epoll中
if (index == kNew || index == kDeleted) {
// 添加到键值对
if (index == kNew) {
int fd = channel->fd();
channels_[fd] = channel;
} else { // index == kAdd
}
// 修改channel的状态,此时是已添加状态
channel->set_index(kAdded);
// 向epoll对象加入channel
Update(EPOLL_CTL_ADD, channel);
} else { // channel已经在poller上注册过
// 没有感兴趣事件说明可以从epoll对象中删除该channel了
if (channel->IsNoneEvent()) {
Update(EPOLL_CTL_DEL, channel);
channel->set_index(kDeleted);
} else { // 还有事件说明之前的事件删除,但是被修改了
Update(EPOLL_CTL_MOD, channel);
}
}
}

接着往下看 Update 操作,其本质是调用 epoll_ctl 函数,而里面的操作由之前的 UpdateChannel 所指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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_FATAL << "epoll_ctl add/mod error:" << errno;
}
}
}

3.4 从 epoll 中移除监视的channel

1
2
3
4
5
6
7
8
9
10
11
12
13
void EPollPoller::RemoveChannel(Channel *channel) {
// 从Map中删除
int fd = channel->fd();
channels_.erase(fd);

int index = channel->index();
if (index == kAdded) { // 如果此fd已经被添加到Poller中,则还需从epoll对象中删除
Update(EPOLL_CTL_DEL, channel);
}

// 重新设置channel的状态为未被Poller注册
channel->set_index(kNew);
}
  1. 断言此channel已经没有可关注的事件了。
  2. unordered_map<fd, Channel*> channels中删除此channel,根据fd删除。
  3. 如果之前channel的状态是添加状态,则还需调用Update方法,将此channelepoll对象中删除。
  4. channel设置其状态为未被监视状态。