Reactor 模型就是网络服务器端用来处理高并发网络 IO 请求的一种编程模型。包含:
- 三类处理事件,即连接事件、写事件、读事件;
- 三个关键角色,即 reactor、acceptor、handler。
所谓的事件驱动框架,就是在实现 Reactor 模型时,需要实现的代码整体控制逻辑。简单来说,事件驱动框架包括了两部分:一是事件初始化;二是事件捕获、分发和处理主循环。
事件初始化是在服务器程序启动时就执行的,它的作用主要是创建需要监听的事件类型,以及该类事件对应的 handler。而一旦服务器完成初始化后,事件初始化也就相应完成了,服务器程序就需要进入到事件捕获、分发和处理的主循环中。
在开发代码时,我们通常会用一个 while 循环来作为这个主循环。然后在这个主循环中,我们需要捕获发生的事件、判断事件类型,并根据事件类型,调用在初始化时创建好的事件 handler 来实际处理事件。
Redis 对 Reactor 模型的实现
Redis 的事件驱动框架定义了两类事件:IO 事件和时间事件,分别对应了客户端发送的网络请求和 Redis 自身的周期性操作。
IO 事件 aeFileEvent:
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
- mask 是用来表示事件类型的掩码。对于网络通信的事件来说,主要有 AE_READABLE、AE_WRITABLE 和 AE_BARRIER 三种类型事件。框架在分发事件时,依赖的就是结构体中的事件类型;
- rfileProc 和 wfileProc 分别是指向 AE_READABLE 和 AE_WRITABLE 这两类事件的处理函数,也就是 Reactor 模型中的 handler。框架在分发事件后,就需要调用结构体中定义的函数进行事件处理;
- 最后一个成员变量 clientData 是用来指向客户端私有数据的指针。
主循环:aeMain 函数
aeMain 函数的逻辑很简单,就是用一个循环不停地判断事件循环的停止标记。如果事件循环的停止标记被设置为 true,那么针对事件捕获、分发和处理的整个主循环就停止了;否则,主循环会一直执行。
在主循环中,事件又是如何被捕获、分发和处理呢?这就是由 aeProcessEvents 函数来完成的了。
事件捕获与分发:aeProcessEvents 函数
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* 若没有事件处理,则立刻返回*/
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/*如果有IO事件发生,或者紧急的时间事件发生,则开始处理*/
if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
…
}
/* 检查是否有时间事件,若有,则调用processTimeEvents函数处理 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
/* 返回已经处理的文件或时间*/
return processed;
}
这三个分支分别对应了以下三种情况:
- 情况一:既没有时间事件,也没有网络事件;
- 情况二:有 IO 事件或者有需要紧急处理的时间事件;
- 情况三:只有普通的时间事件。
第二种情况:
首先,当该情况发生时,Redis 需要捕获发生的网络事件,并进行相应的处理。在这种情况下,aeApiPoll 函数会被调用,用来捕获事件。
int aeProcessEvents(aeEventLoop *eventLoop, int flags){
...
if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
...
//调用aeApiPoll函数捕获事件
numevents = aeApiPoll(eventLoop, tvp);
...
}
...
」
Redis 对不同操作系统实现的网络 IO 多路复用函数,aeApiPoll 函数都进行了统一的封装,封装后的代码分别通过以下四个文件中实现:
- e_epoll.c,对应 Linux 上的 IO 复用函数 epoll;
- ae_evport.c,对应 Solaris 上的 IO 复用函数 evport;
- ae_kqueue.c,对应 macOS 或 FreeBSD 上的 IO 复用函数 kqueue;
- ae_select.c,对应 Linux(或 Windows)的 IO 复用函数 select。
事件注册:aeCreateFileEvent 函数
在初始化的过程中,aeCreateFileEvent 就会被 initServer 函数调用,用于注册要监听的事件,以及相应的事件处理函数。
aeCreateFileEvent 如何实现事件和处理函数的注册呢?
首先,Linux 提供了 epoll_ctl API,用于增加新的观察事件。而 Redis 在此基础上,封装了 aeApiAddEvent 函数,对 epoll_ctl 进行调用。
所以这样一来,aeCreateFileEvent 就会调用 aeApiAddEvent,然后 aeApiAddEvent 再通过调用 epoll_ctl,来注册希望监听的事件和相应的处理函数。等到 aeProceeEvents 函数捕获到实际事件时,它就会调用注册的函数对事件进行处理了。
此文章为10月Day10学习笔记,内容来源于极客时间《Redis 源码剖析与实战》