一.概述
Reactor是一个使用了同步非阻塞的I/O多路复用机制的模式,I/O多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select,poll,epoll函数(现在多使用epoll函数,关于epoll可见上篇博文),传入多个文件描述符,如果有文件描述符就绪,就返回,否则阻塞直到超时。得到就绪状态后启动线程执行
I/O 复用机制需要事件分发器。 事件分发器的作用,将那些读写事件源分发给各读写事件的处理者。Reactor正是这个事件分发器。其是事件驱动的,有一个或多个并发输入源,处理如下图
当acceptor接收到请求后会把它交给reactor,在分发给各个文件描述符的事件处理函数。
所以该模式主要包含两个组件——Reactor和Handler。Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理;新的事件包含连接建立就绪、读就绪、写就绪等。Handler:将自身(handler)与事件绑定,负责事件的处理,完成channel的读入,负责将结果写出channel。
二.来看具体的实现
在编写中将其分为三个函数类
(1)第一个是channel,其负责一个文件描述符的分发,即在其中存储IO事件类型及注册回调函数,带有一个存储fd的变量,可知这个fd变量可被赋予不同的值。Channel可以给不同的IO事件分发不同的回调,其中包含一个主要的函数能根据上层传过来的值进行函数调用。
说到底Channel就是把fd 和fd感兴趣的IO事件,和fd上就绪的IO事件,以及对相应的IO事件进行处理的函数封装到一起,部分代码如下
class Channel
{
private:
int fd_; // Channel管理的fd
__uint32_t events_; // 保存了fd上感兴趣的IO事件
__uint32_t revents_; // 目前fd上就绪的事件
private:
CallBack readHandler_; //事件处理回调函数
CallBack writeHandler_;
CallBack errorHandler_;
CallBack connHandler_;
public:
Channel(EventLoop *loop);
Channel(EventLoop *loop, int fd);
~Channel();
void setHolder(std::shared_ptr<HttpData> holder) //存储相应的事件
{
holder_ = holder;
}
std::shared_ptr<HttpData> getHolder()
{
std::shared_ptr<HttpData> ret(holder_.lock());
return ret;
}
void setReadHandler(CallBack &&readHandler)
{
readHandler_ = readHandler;
}
void setWriteHandler(CallBack &&writeHandler)
{
writeHandler_ = writeHandler;
}
void setErrorHandler(CallBack &&errorHandler)
{
errorHandler_ = errorHandler;
}
void setConnHandler(CallBack &&connHandler)
{
connHandler_ = connHandler;
}
// handleEvent()是Channel的核心,它根据revents_的值分别调用不同的用户回调
void handleEvents()
{
events_ = 0;
if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)) // 挂起
{
events_ = 0;
return;
}
if (revents_ & EPOLLERR) // 错误
{
if (errorHandler_) errorHandler_(); //如果对应的文件描述符发生错误,调用错误处理函数
events_ = 0;
return;
}
if (revents_ & (EPOLLIN | EPOLLPRI | EPOLLRDHUP)) // 可读
{
handleRead();
}
if (revents_ & EPOLLOUT) // 可写
{
handleWrite();
}
handleConn(); // 处理连接
}
void handleRead();
void handleWrite();
void handleError(int fd, int err_num, std::string short_msg);
void handleConn();
void setRevents(__uint32_t ev) // 设置fd上的就绪事件
{
revents_ = ev;
}
void setEvents(__uint32_t ev) // 设置fd上的感兴趣事件
{
events_ = ev;
}
__uint32_t& getEvents() // 返回感兴趣事件
{
return events_;
}
bool EqualAndUpdateLastEvents() // 更新上次注册的事件
{
bool ret = (lastEvents_ == events_);
lastEvents_ = events_;
return ret;
}
__uint32_t getLastEvents()
{
return lastEvents_;
}
};
(2)epoll类,该类负责将一个文件描述符与一个channel类联系起来,其中也封装了epoll相应的文件描述符添加,修改,删除函数,它可将channel感兴趣的事件注册到内核表中,部分代码如下
class Epoll
{
public:
Epoll();
~Epoll();
void epoll_add(SP_Channel request, int timeout); //sp_channel 是channel的智能指针,相应epoll函数的封装
void epoll_mod(SP_Channel request, int timeout);
void epoll_del(SP_Channel request);
std::vector<Channel> poll(); // 核心功能,能够返回就绪的channel数组
private:
std::vector<epoll_event> events_; // epoll_event结构体数组,从内核事件表将所有就绪的事件复制到此
};
(3)eventloop类,该类由一个IO线程创建,并管理一个epoll对象,并管理归属于本EventLoop对象的Channel对象,其主要功能是运行loop函数,其函数是调用epoll::poll函数获得当前活动事件的channel列表,然后依次调用每个channel的事件处理函数。部分代码如下
class EventLoop
{
public:
EventLoop();
~EventLoop();
void loop(); // 循环函数
void quit(); // 退出函数
void assertInLoopThread() // 检查当前线程是否已经创建了其他EventLoop对象,一个线程稚嫩对应一个EventLoop对象
{
assert(isInLoopThread());
}
//管理其中的channel对象
// 从IO复用类对象中poller_中移除Channel对象
void removeFromPoller(shared_ptr<Channel> channel)
{
//shutDownWR(channel->getFd());
poller_->epoll_del(channel);
}
// 更新Channel对象
void updatePoller(shared_ptr<Channel> channel, int timeout = 0)
{
poller_->epoll_mod(channel, timeout);
}
// 添加Channel对象
void addToPoller(shared_ptr<Channel> channel, int timeout = 0)
{
poller_->epoll_add(channel, timeout);
}
private:
shared_ptr<Epoll> poller_; // 每个事件循环中都有一个IO复用
void wakeup();
void handleRead();
void doPendingFunctors();
void handleConn();
};
以上三部分就组成了Reactor的核心部分,概述流程
- 初始化channel,然后将若干个事件handler注册到其中,同时指定感兴趣的事件,channel里应还有一个文件描述符,其可以根据请求调用相应不同的用户回调。
- 当所有具体事件处理器都注册完成后,会调用后eventloop::lopp()来启动循环,其会将每个具体事件处理器与一个文件描述符联系起来,然后同步阻塞等待事件的发生,epoll_wait()
- 当某个文件描述被激活后就会通知channel,channel会回调事件处理器来执行特定的应用功能
为了更高的提升提高Reactor线程的I/O响应,加入线程池,通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程产生的巨大开销。当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
这下再来看下面一张图,这就是加入线程池后的reactor多线程模型
后续整理下线程池相关的知识再来完善。