深入理解Reactor 网络编程模型
Reactor模式
介绍
网络编程模型通常有如下几种:Reactor, Proactor, Asynchronous, Completion Token, and Acceptor-Connector. 本文主要对最主流的Reactor
模型进行介绍。通常网络编程模型处理的主要流程如下
initiate => receive => demultiplex => dispatch => process events
I/O多路复用可以用作并发事件驱动(event-driven)程序的基础,即整个事件驱动模型是一个状态机,包含了状态(state), 输入事件(input-event), 状态转移(transition), 状态转移即状态到输入事件的一组映射。通过I/O多路复用的技术检测事件的发生,并根据具体的事件(通常为读写),进行不同的操作,即状态转移。
Reactor
模式是一种典型的事件驱动的编程模型,Reactor
逆置了程序处理的流程,其基本的思想即为Hollywood Principle— 'Don't call us, we'll call you'
.
普通的函数处理机制为:调用某函数-> 函数执行, 主程序等待阻塞-> 函数将结果返回给主程序-> 主程序继续执行
Reactor
事件处理机制为:主程序将事件以及对应事件处理的方法在Reactor
上进行注册, 如果相应的事件发生,Reactor
将会主动调用事件注册的接口,即 回调函数. libevent
即为封装了epoll
并注册相应的事件(I/O读写,时间事件,信号事件)以及回调函数,实现的事件驱动的框架。
Reactor
事件处理机制的编程模型,在Redis中也得到了很好的运用,Redis中基于I/O多路复用(mutiplexing) 开发Reactor事件处理机制,监听多个套接字的AE_READABLE读,AE_WRITABLE写事件。读事件绑定读操作和具体执行命令的操作函数,写事件绑定命令回复的操作函数。
架构
The Reactor architectural pattern allows event-driven applications to demultiplex and dispatch service requests that are delivered to an application from one or more clients.
Reactor
架构模式允许事件驱动的应用通过多路分发的机制去处理来自不同客户端的多个请求。
上图即为Reactor核心的事件处理流程,有如下几个关键组件
事件(事件源)
linux
上为文件描述符,handler
即为注册在特定事件上的程序,事件发生通常在linux下为I/O事件,由操作系统触发
Reactor (反应器)
事件管理的接口,内部使用event demultiplexer
注册,注销事件;并运行事件循环,当有事件进入"就绪"状态时,调用注册事件的回调函数处理事件。
class Reactor {
public:
int register_handler(EventHandler *pHandler, int event);
int remove_handler(EventHandler *pHandler, int event);
void handle_events(timeval *ptv);
}
Event demultiplexer(事件多路分发机制)
通常是由操作系统提供的I/O多路复用的机制,例如select
, epoll
. 程序首先将handler
(事件源)以及对应的事件注册到event demultiplexer
上;当有事件到达时,event demultiplexer
就会发出通知,通知Reactor
调用事件处理程序进行处理
Event Handler(事件处理程序)
事件处理程序提供了一组接口,在Reactor
相应的事件发生时调用,执行相应的事件处理,通常会绑定一个有效的handler
class Event_Handler {
public:
// events maybe read/write/timeout/close .etc
virtual void handle_events(int events) = 0;
virtual HANDLE get_handle() = 0;
}
举例
下图描述了一个简单的日志服务器,即一个或者多个客户端通过不同的请求获得不同设备的日志,例如打印机的运行情况,数据库的TPS等等。对于传统的线程池模型来说只能每个对于每个请求使用一个单独的线程去处理,这就导致了当请求增加时过多了线程上下文切换,出现了性能上的瓶颈。
事件驱动的模型如下图,充分的利用linux select epoll模型,并注册相应的回调函数对于不同的事件,根据I/O多路复用的机制,实现了高并发,和高可扩展性。
实现
如下是一种Reactor的简单实现,监听STDIN,并注册不同的事件。处理网络请求也类似,具体可参考 reactor-server源码
#include <sys/epoll.h>
#include <unistd.h>
#include <iostream>
#include <array>
#include <unordered_map>
typedef int EventType;
class Epoll {
// 封装了epoll I/O 多路复用的机制, Event demultiplexer
public:
static const int NO_FLAGS = 0;
static const int BLOCK_INDEFINITELY = -1;
static const int MAX_EVENTS = 5;
Epoll() {
fileDescriptor = epoll_create1(NO_FLAGS);
event.data.fd = STDIN_FILENO;
// 设置epoll event 为EPOLLIN(对应文件描述符可读), EPOLLPRI(对应文件描述符有紧急事件可读)
event.events = EPOLLIN | EPOLLPRI;
}
int wait() {
return epoll_wait(fileDescriptor, events.data(), MAX_EVENTS, BLOCK_INDEFINITELY);
}
int control() {
return epoll_ctl(fileDescriptor, EPOLL_CTL_ADD, STDIN_FILENO, &event);
}
~Epoll() {
close(fileDescriptor);
}
private:
int fileDescriptor;
struct epoll_event
event;
std::array<epoll_event, MAX_EVENTS> events{};
};
class EventHandler {
// Event Handler
public:
int handle_event(EventType et) {
std::cout << "Event Handler: " << et << std::endl;
return 0;
}
};
class Reactor {
// Dispatcher
public:
Reactor() {
epoll.control();
}
//注册对应的回调函数到handlers中
void addHandler(std::string event, EventHandler callback) {
handlers.emplace(std::move(event), std::move(callback));
}
void run() {
while (true) {
int numberOfEvents = wait();
for (int i = 0; i < numberOfEvents; ++i) {
std::string input;
std::getline(std::cin, input);
try {
// 根据的具体的事件去找对应的handler,并执行相应的操作
handlers.at(input).handle_event(EventType(i));
} catch (const std::out_of_range &e) {
std::cout << "no handler for " << input << std::endl;
}
}
}
}
private:
// handlers Table, 存储事件以及其对应的handlers
std::unordered_map<std::string, EventHandler> handlers{};
Epoll epoll;
int wait() {
int numberOfEvents = epoll.wait();
return numberOfEvents;
}
};
int main() {
Reactor reactor;
reactor.addHandler("a", EventHandler{});
reactor.addHandler("b", EventHandler{});
reactor.run();
}