muduo 是陈硕基于 Reactor 模式开发的一个 c++ 网络库,而 Reactor 模式是 Douglas C. Schmidt 在 1995 年提出来的,论文《Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events》中介绍道:
The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer.
本文旨在通过 muduo 源码中提供的一个 example 来一探 Reactor 模式究竟。代码如下:
int main(int argc, char* argv[])
{
EventLoop loop;
InetAddress listenAddr(2021);
TcpServer server(&loop, listenAddr, "FileServer");
server.setConnectionCallback(onConnection);
server.start();
loop.loop();
}
程序中先是定义了个 EventLoop 类对象 loop,看下对应构造函数都干了啥:
EventLoop::EventLoop()
: looping_(false),
quit_(false),
eventHandling_(false),
callingPendingFunctors_(false),
iteration_(0),
threadId_(CurrentThread::tid()),
poller_(Poller::newDefaultPoller(this)),
timerQueue_(new TimerQueue(this)),
wakeupFd_(createEventfd()),
wakeupChannel_(new Channel(this, wakeupFd_)),
currentActiveChannel_(NULL)
{
LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread) {
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
} else {
t_loopInThisThread = this;
}
wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead, this));
// we are always reading the wakeupfd
wakeupChannel_->enableReading();
}
数据成员有点多,但本例主要可关注 threadId_、poller_、wakeupFd_、wakeupChannel_ 几个。其中 threadId_ 记录当前对象所属的 IO 线程 tid,另外几个后续提到再介绍。方法体中首先是个 if-else 判断,是为了保证 one loop per thread,即每个线程只能有一个 EventLoop 类对象。
判断没问题后再注册事件回调,先看 handleRead() 方法:
void EventLoop::handleRead()
{
uint64_t one = 1;
ssize_t n = sockets::read(wakeupFd_, &one, sizeof one);
if (n != sizeof one) {
LOG_ERROR << "handleRead() reads " << n << " bytes instead of 8";
}
}
这里调用了 read() 读取文件描述符 wakeupFd_ 指向信息,wakeupFd_ 就是前面提到的几个数据成员之一,它是调用了 createEventfd() 方法对其初始化:
int createEventfd()
{
int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (evtfd < 0) {
LOG_SYSERR << "Failed in eventfd";
abort();
}
return evtfd;
}
系统调用 eventfd() 方法创建了事件通知文件描述符 evtfd,参数 EFD_NONBLOCK 表示 evtfd 是非阻塞的,这样当计数器为 0 无数据可读时,立马返回 EAGAIN 错误;另一个参数 EFD_CLOEXEC 表示 fork 子进程时不继承父进程的文件描述符。
再看数据成员 wakeupChannel_,从初始化可看出其实是个 Channel 类对象,初始化时传入 loop 对象和刚创建的事件通知文件描述符 wakeupFd_。实际上 Channel 类正负责文件描述符 wakeupFd_ 的 IO 事件的注册与分发,部分成员如下:
class Channel : noncopyable
{
public:
...
void handleEvent(Timestamp receiveTime);
void setReadCallback(ReadEventCallback cb) { readCallback_ = std::move(cb); }
void setWriteCallback(EventCallback cb) { writeCallback_ = std::move(cb); }
void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb); }
void setErrorCallback(EventCallback cb) { errorCallback_ = std::move(cb); }
...
priveate:
...
EventLoop* loop_;
const int fd_;
int events_;
int revents_; // it's the received event types of epoll or poll
int index_; // used by Poller.
...
};
创建好类对象 wakeupChannel_ 后,先通过 std::bind() 方法将读 wakeupFd_ 事件与 loop 绑定,然后再调用成员方法 setReadCallback() 注册回调,最后调用 enableReading() 更新:
// Channel.cc
// POLLIN:表示对应的文件描述符可读,POLLPRI:表示对应的文件描述符有紧急的数据可读
const int Channel::kReadEvent = POLLIN | POLLPRI;
// Channel.h
void enableReading() { events_ |= kReadEvent; update(); }
// Channel.cc
void Channel::update()
{
addedToLoop_ = true;
loop_->updateChannel(this);
}
// EventLoop.cc
void EventLoop::updateChannel(Channel* channel)
{
assert(channel->ownerLoop() == this);
assertInLoopThread();
poller_->updateChannel(channel);
}
根据调用链看到最后调用的是 Poller 类指针 poller_ 指向对象的 updateChannel() 方法。这里的 poller_ 也是 EventLoop 类的数据成员之一,在调用构造函数时是通过 Poller::newDefaultPoller() 方法初始化的:
Poller* Poller::newDefaultPoller(EventLoop* loop)
{
if (::getenv("MUDUO_USE_POLL")) {
return new PollPoller(loop);
} else {
return new EPollPoller(loop);
}
}
根据类名可以看到分别是 IO 多路复用的 poll 和 epoll 机制,这里以 epoll 机制为例:
EPollPoller::EPollPoller(EventLoop* loop)
: Poller(loop),
epollfd_(::epoll_create1(EPOLL_CLOEXEC)),
events_(kInitEventListSize)
{
if (epollfd_ < 0) {
LOG_SYSFATAL << "EPollPoller::EPollPoller";
}
}
EpollPoller 类公有继承自 Poller 类,Poller 类是对 IO 多路复用的封装,定义如下:
class Poller : noncopyable
{
public:
...
// Changes the interested I/O events. Must be called in the loop thread.
virtual void updateChannel(Channel* channel) = 0;
// Remove the channel, when it destructs. Must be called in the loop thread.
virtual void removeChannel(Channel* channel) = 0;
...
protected:
typedef std::map<int, Channel*> ChannelMap;
ChannelMap channels_;
...
};
看定义主要是对 channel 对象的操作,且成员方法都定为纯虚函数。这里显然是利用了多态,根据实际对象调用对应的函数实现:
void EPollPoller::updateChannel(Channel* channel)
{
Poller::assertInLoopThread();
const int index = channel->index();
...
if (index == kNew || index == kDeleted) {
...
update(EPOLL_CTL_ADD, channel);
} else {
...
if (channel->isNoneEvent()) {
update(EPOLL_CTL_DEL, channel);
channel->set_index(kDeleted);
} else {
update(EPOLL_CTL_MOD, channel);
}
}
}
三处都调用的 update() 方法,只是参数不同。
void EPollPoller::update(int operation, Channel* channel)
{
struct epoll_event event;
memZero(&event, sizeof event);
event.events = channel->events();
event.data.ptr = channel;
int fd = channel->fd();
string op = operationToString(operation);
LOG_TRACE << "epoll_ctl op = " << op << " fd = " << fd
<< " event = { " << channel->eventsToString() << " }";
if (::epoll_ctl(epollfd_, operation, fd, &event) < 0) {
if (operation == EPOLL_CTL_DEL) {
LOG_SYSERR << "epoll_ctl op =" << op << " fd =" << fd;
} else {
LOG_SYSFATAL << "epoll_ctl op =" << op << " fd =" << fd;
}
}
}
与源码稍微有些不同,但功能完全一致。方法其实就是对 epoll_ctl() 方法的封装,参数准备和调用。第一个参数 epollfd_ 是在调用 EventLoop 类构造函数时初始化数据成员 poller_ 时初始化的,第二个参数 operation 表示事件的添加、修改或删除,第三个参数 fd 即 Channel 类数据成员 fd_:
// Channel.h
int fd() const { return fd_; }
// Channel.cc
Channel::Channel(EventLoop* loop, int fd__)
: loop_(loop),
fd_(fd__),
events_(0),
revents_(0),
index_(-1),
logHup_(true),
tied_(false),
eventHandling_(false),
addedToLoop_(false)
{
}
也就是初始化 EventLoop 类对象时生成的事件通知文件描述符 wakeupFd_,第四个参数 event 是个 struct epoll_event 数据结构,定义如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中 events 指向的就是 Channel 类数据成员 events_,在 EventLoop 类构造函数中调用的 enableReading() 方法对其赋的值。至此, loop 对象的初始化工作就已完成,主要是做了以下几件事:
1. 记录线程 tid,if-else 判断保证 one loop per thread;
2. 执行 eventfd() 系统调用,生成事件通知文件描述符 wakeupFd_;
3. 初始化 poller_ 对象,确认 IO 多路复用机制,如 epoll,并调用 epoll_create() 方法创建内核事件表;
4. 初始化 Channel 类对象 wakeupChannel_,定义各类型事件,如 POLLIN | POLLPRI,并调用 epoll_ctl() 方法向内核事件表注册 wakeupFd_ 和相关事件。
loop 对象的初始化完成其实也是准备工作的完成,回到例程中,第 2-5 行主要是做 tcp 连接的处理,包括端口的设置、套接字的创建与监听、数据的读取等,这块并不是本文讨论的重点,暂且不展开来讲,直接跳最后一步,也就是执行 EventLoop 类的成员方法 loop() :
const int kPollTimeMs = 10000;
void EventLoop::loop()
{
...
while (true) {
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
...
for (Channel* channel : activeChannels_) {
currentActiveChannel_ = channel;
currentActiveChannel_->handleEvent(pollReturnTime_);
}
...
}
...
}
为了方便理解本文主题,特意忽略了部分代码。方法体中有个 while 无限循环,循环体可分为两步:第一步是调用 poller_ 指针指向对象的 poll() 方法获取当前活动事件的 channel 列表。
Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels)
{
LOG_TRACE << "fd total count " << channels_.size();
int numEvents = ::epoll_wait(epollfd_,
&*events_.begin(),
static_cast<int>(events_.size()),
timeoutMs);
int savedErrno = errno;
Timestamp now(Timestamp::now());
if (numEvents > 0) {
LOG_TRACE << numEvents << " events happened";
fillActiveChannels(numEvents, activeChannels);
if (implicit_cast<size_t>(numEvents) == events_.size()) {
events_.resize(events_.size()*2);
}
} else if (numEvents == 0) {
LOG_TRACE << "nothing happened";
} else {
...
}
return now;
}
由于定义 EventLoop 类对象时选择的是 epoll 机制,这里调用的自然也是 EpollPoller 类中 poll() 方法,超时时间定为 10 秒,若在超时之前有事件响应,即 numEvents > 0,则调用 fillActiveChannels() 方法:
// EpollPoller.cc
void EPollPoller::fillActiveChannels(int numEvents,
ChannelList* activeChannels) const
{
assert(implicit_cast<size_t>(numEvents) <= events_.size());
for (int i = 0; i < numEvents; ++i) {
Channel* channel = static_cast<Channel*>(events_[i].data.ptr);
#ifndef NDEBUG
int fd = channel->fd();
ChannelMap::const_iterator it = channels_.find(fd);
assert(it != channels_.end());
assert(it->second == channel);
#endif
channel->set_revents(events_[i].events);
activeChannels->push_back(channel);
}
}
// Channel.h
void set_revents(int revt) { revents_ = revt; } // used by pollers
revents_ 表示目前活动的事件,获取 channel 列表后执行第二步 handleEvent() 方法:
void Channel::handleEvent(Timestamp receiveTime)
{
...
handleEventWithGuard(receiveTime);
}
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
...
if ((revents_ & POLLHUP) && !(revents_ & POLLIN)) {
if (logHup_) {
LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";
}
if (closeCallback_) closeCallback_();
}
if (revents_ & POLLNVAL) {
LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLNVAL";
}
if (revents_ & (POLLERR | POLLNVAL)) {
if (errorCallback_) errorCallback_();
}
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP)) {
if (readCallback_) readCallback_(receiveTime);
}
if (revents_ & POLLOUT) {
if (writeCallback_) writeCallback_();
}
...
}
最终调用的是 Channel 类中的 handleEventWithGuard() 方法,在该方法中做的 IO 事件分发。
综上,算是对 muduo 网络库中的 Reactor 模式有了个大概的认识,但对有些问题仍不甚了解,此处暂且不表,待日后有了更透彻的理解后再回头来看。如果大家有什么问题也欢迎交流哈~