moduo网络库的reactor模式基本构成为“non-blocking I/O + I/O multiplexing”,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调(event callback)的方式实现业务逻辑。在moduo网络库的reactor模式(上)理清了事件循环(event loop)的基本框架,在此基础上此文加上事件驱动(event-driven)和事件回调(event callback)结构,即reactor最核心的I/O多路复用(I/O mutiplexing)和事件分发(dispatching)机制:进行I/O多路复用并将拿到的I/O事件分发给各个文件描述符(fd)的事件处理函数。
1、事件驱动与分发:I/O多路复用与事件分发
(1)Channel类:封装文件描述符(fd)实现事件分发
每个Channel对象都只属于某一个EventLoop,因此只属于某一个I/O线程。每个Channel对象只负责一个文件描述符的I/O事件分发,Channel会把不同的I/O事件(读、写、错误等)分发为不同的回调。Channel类就是对文件描述符的封装,构造函数Channel(EventLoop* loop, int fd)即将此Channel对象与唯一所属的EventLoop以及文件描述符(fd)绑定了起来。Channel类数据成员events_表示fd事件,用于更新I/O多路复用poll(2)。数据成员revents_表示现正要执行的fd事件,用于事件回调。在目前程序中
1)Channel::enableReading()、Channel::enableWriting()等为设置文件描述符(fd)事件的接口函数:
首先设置fd事件events_,然后执行update()将该Channel的新事件更新到I/O多路复用器poll(2)(update()是通过数据成员EventLoop* loop_,即自己所属的EventLoop对象指针调用EventLoop::updateChannel(),再调用Poller::updateChannel()间接更新poll(2)中的事件。此处疑问:为什么不直接在Channel中添加数据成员Poller指针直接更新事件到poll(2),而是要绕一圈间接更新事件?)
void enableReading() { events_ |= kReadEvent; update(); }
// void enableWriting() { events_ |= kWriteEvent; update(); }
void Channel::update()
{
loop_->updateChannel(this);
}
2)Channel::setReadCallback(const EventCallback& cb)、Channel::setWriteCallback(const EventCallback& cb)、Channel::setErrorCallback(const EventCallback& cb)为设置fd对应事件的用户回调函数的接口函数。
void setReadCallback(const EventCallback& cb) { readCallback_ = cb; }
void setWriteCallback(const EventCallback& cb) { writeCallback_ = cb; }
void setErrorCallback(const EventCallback& cb) { errorCallback_ = cb; }
3)Channel::handleEvent()是Channel的核心,实现事件分发功能,它由EventLoop::loop()调用,它的功能是根据revents_的值分别调用不同的用户调用。而revents_则是在Poller::poll()中得以更新的。
void Channel::handleEvent()
{
if (revents_ & POLLNVAL)
LOG_WARN << "Channel::handle_event() POLLNVAL";
if (revents_ & (POLLERR | POLLNVAL))
if (errorCallback_)
errorCallback_();
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))
if (readCallback_)
readCallback_();
if (revents_ & POLLOUT)
if (writeCallback_)
writeCallback_();
}
//inside of function EventLoop::loop():
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
(*it)->handleEvent();
}
doPendingFunctors();
}
(2)Poller类:封装I/O多路复用poll(2)实现I/O多路复用
首先看系统调用poll(2)的函数原型为
#include <poll.h>
int poll(struct pollfd fd[], nfds_t nfds, int timeout);
struct pollfd的结构为
struct pollfd{
int fd; // 文件描述符
short event;// 请求的事件
short revent;// 返回的事件
}
1)为poll(2)提供数据准备:
Poller封装类首先是通过Poller::updateChannel(Channel* channel)将文件描述符(fd)封装类Channel存储到数据成员std::map<int, Channel*> ChannelMap中(具体实现过程大致为:用户调用Channel::update()----->EventLoop::updateChannel()----->Poller::updateChannel())。接着则可将各个Channel对象的数据成员更新给poll(2),并在每次poll(2)后更新各个Channel的revents_。
2)封装I/O多路复用(I/O multiplexing):
Poller::poll(int timeoutMs, ChannelList* activeChannels)函数中首先进行I/O多路复用poll(2),所需参数从数据成员std::map<int, Channel*> ChannelMap中获得,阻塞直到有fd事件发生或定时时间到时,返回事件发生数量numEvents。然后继续执行内部函数Poller::fillActiveChannels(numEvents, activeChannels)将可发生fd事件对应的Channel反馈给外界,即在数据成员std::map<int, Channel*> ChannelMap中找出事件可发生的fd对应的Channel,存放到外部参数ChannelList* activeChannels中。
注意 2)中并未将I/O多路复用与事件分发合在一起,而是只实现了I/O多路复用,事件分发Channel::handleEvent()则是在EventLoop::loop()中实现。一方面是程序安全方面的考虑,另一方面则是为了方便替换为其他更高效的I/O多路复用机制,如epoll(4)。
至此,一个完整的Reactor模式基本框架就完成了。
2、测试:利用timerfd(2)
有了Reactor基本框架后,我们使用timerfd给EventLoop加上一个定时器功能,对这个框架进行测试。
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value);
传统的Reactor通过控制poll(2)或select(2)的等待时间来实现定时,而现在linux有了timerfd,我们可以用和处理I/O事件相同的方式来处理定时。具体原因参考文章Muduo 网络编程示例之三:定时器
#include "EventLoopThread.hpp"
#include "EventLoop.hpp"
#include "Thread.hpp"
#include <sys/timerfd.h>
#include <unistd.h>
#include <string.h>
#include <memory>
#include <iostream>
using namespace std;
EventLoop* loop;
void timeout()
{
cout<<"tid "<<CurrentThreadtid()<<": Timeout!"<<endl;
loop->quit();
}
int main()
{
EventLoopThread ELThread;
loop = ELThread.startLoop();//thread2
int timerfd=timerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK|TFD_CLOEXEC);
struct itimerspec howlong;
bzero(&howlong, sizeof howlong);
howlong.it_value.tv_sec=3;
timerfd_settime(timerfd,0,&howlong,NULL);
Channel channel(loop,timerfd);
channel.setReadCallback(timeout);
channel.enableReading();
sleep(5);//ensure the main thread do not exit faster than thread2
close(timerfd);
return 0;
}
baddy@ubuntu:~/Documents/Reactor/s1.1$ g++ -std=c++11 -pthread -o test1 MutexLockGuard.hpp Condition.hpp Thread.hpp Thread.cpp Channel.hpp Channel.cpp EventLoop.hpp EventLoop.cpp Poller.hpp Poller.cpp EventLoopThread.hpp testEventLoopThread.cpp
baddy@ubuntu:~/Documents/Reactor/s1.1$ /usr/bin/valgrind ./testTimerDemo
==25681== Memcheck, a memory error detector
==25681== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25681== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==25681== Command: ./testTimerDemo
==25681==
tid 25681: create a new thread
tid 25681: waiting
tid 25682: T