Redis事件驱动框架(中):Redis实现了Reactor模型吗?

2023年 10月 10日 18.5k 0

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 源码剖析与实战》

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论