Reactor 模型

网络服务器端,用了处理高并发网络 IO请求的一种编程模型

处理 3 类事件:

  • 连接事件:客户端→服务器的连接请求,对应服务端的连接事件
  • 写事件:客户端→服务器的读请求,服务端处理后要写回客户端,对应服务端的写事件
  • 读事件:服务端要从客户端读取请求内容,对应服务端的读事件

3 个关键角色:

  • acceptor:处理连接事件,接收连接、创建 handler
  • handler:处理读写事件
  • reactor:专门监听和分配事件,连接请求 → acceptor、读写请求 → handler

事件驱动框架

事件驱动框架就是 Reactor 的具体实现。包括:

  • 事件初始化:创建要监听的事件类型,及该类事件对应的 handler
  • 事件捕获、分发和处理主循环
    • 捕获发生的事件
    • 判断事件类型
    • 根据事件类型,调用对应 handler 处理事件

Redis 如何实现 Reactor 模型

实现代码:

  • 头文件:ae.h
  • 实现:ae.c

事件的数据结构:aeFileEvent

Redis 的事件驱动框架定义了 2 类事件:

  • IO 事件
  • 时间事件

下面介绍 IO 事件 aeFileEvent 的数据结构:

/* File event structure */
typedef struct aeFileEvent {
// 事件类型的掩码,AE_(READABLE|WRITABLE|BARRIER)
int mask;
// AE_READABLE 事件的处理函数
aeFileProc *rfileProc;
// AE_WRITABLE 事件的处理函数
aeFileProc *wfileProc;
// 指向客户端私有数据
void *clientData;
} aeFileEvent;

主循环:aeMain 函数

是在 Redis 初始化时调用的,详见 Redis 源码简洁剖析 07 - main 函数启动

void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
// 循环调用
while (!eventLoop->stop) {
// 核心函数,处理事件的逻辑
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}

代码非常简单,就是循环调用 aeProcessEvents 函数。aeMain 是在 main 函数中被调用的:

// 事件驱动框架,循环处理各种触发的事件
aeMain(server.el);
// 循环结束,删除 eventLoop
aeDeleteEventLoop(server.el);

事件捕获与分发:aeProcessEvents 函数

主体有 3 个 if 分支:

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;
}

核心是第 2 个 if 语句:

// 有 IO 事件发生 || 紧急时间事件发生
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
……
// 调用 aeApiPoll 捕获事件
numevents = aeApiPoll(eventLoop, tvp);
……
}

aeApiPoll 函数如何捕获事件?依赖于操作系统底层提供的 IO 多路复用机制,实现事件捕获,检查是否有新的连接、读写事件的发生。为了适配不同的操作系统,Redis 对不同操作系统实现网络 IO 多路复用函数,进行统一封装,封装后的代码在 4 个文件中实现:

  • ae_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

ae_epoll.c 中 aeApiPoll 函数的实现,核心是调用了 epoll_wait 函数,并将 epoll 返回的事件信息保存起来。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0; // 调用 epoll_wait 获取监听到的事件
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
if (retval > 0) {
int j; // 获取监听到的事件数量
numevents = retval;
// 处理每个事件
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events + j; if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE | AE_READABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE | AE_READABLE; // 保存事件信息
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}

在 Mac 上查看源码,aeApiPoll 方法会进入 ae_kqueue.c 中:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0; if (tvp != NULL) {
struct timespec timeout;
timeout.tv_sec = tvp->tv_sec;
timeout.tv_nsec = tvp->tv_usec * 1000;
retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
&timeout);
} else {
retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
NULL);
} if (retval > 0) {
int j; /* Normally we execute the read event first and then the write event.
* When the barrier is set, we will do it reverse.
*
* However, under kqueue, read and write events would be separate
* events, which would make it impossible to control the order of
* reads and writes. So we store the event's mask we've got and merge
* the same fd events later. */
for (j = 0; j < retval; j++) {
struct kevent *e = state->events+j;
int fd = e->ident;
int mask = 0; if (e->filter == EVFILT_READ) mask = AE_READABLE;
else if (e->filter == EVFILT_WRITE) mask = AE_WRITABLE;
addEventMask(state->eventsMask, fd, mask);
} /* Re-traversal to merge read and write events, and set the fd's mask to
* 0 so that events are not added again when the fd is encountered again. */
numevents = 0;
for (j = 0; j < retval; j++) {
struct kevent *e = state->events+j;
int fd = e->ident;
int mask = getEventMask(state->eventsMask, fd); if (mask) {
eventLoop->fired[numevents].fd = fd;
eventLoop->fired[numevents].mask = mask;
resetEventMask(state->eventsMask, fd);
numevents++;
}
}
}
return numevents;
}

事件注册:aeCreateFileEvent 函数

main 函数中调用了 createSocketAcceptHandler

if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
serverPanic("Unrecoverable error creating TCP socket accept handler.");
}

createSocketAcceptHandler 创建接收连接的 handler:

int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {
int j; for (j = 0; j < sfd->count; j++) {
if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {
/* Rollback */
for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);
return C_ERR;
}
}
return C_OK;
}

其主要是调用了 aeCreateFileEvent,aeCreateFileEvent 就是实现事件和处理函数注册的核心函数。

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
// 错误处理
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd]; // 核心
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}

Linux 提供了 epoll_ctl API,用于增加新的观察事件。而 Redis 在此基础上,封装了 aeApiAddEvent 函数,对 epoll_ctl 进行调用,注册希望监听的事件和相应的处理函数。

ae_epoll.c 中 aeApiAddEvent 实现如下:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD; ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
// 增加新的观察事件
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}

注册的函数 acceptTcpHandler 在 network.c 中:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata); // 每次处理 1000 个
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
anetCloexec(cfd);
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
}

总结

Redis 处理连接、客户端请求是单线程的,但是这单个线程能够处理上千个客户端,就是因为 Redis 是基于 Reactor 模型的。通过事件驱动框架,Redis 可以使用一个循环不断捕获、分发、处理客户端产生的网络连接、数据读写事件。当然这里有一个前提,就是 Redis 几乎所有数据读取和处理都是在内存中操作的,服务端对单个客户端的读写请求处理时间极短。

参考链接

Redis 源码简洁剖析系列

最简洁的 Redis 源码剖析系列文章

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~

原创不易,希望大家转载时请先联系我,并标注原文链接。

Redis 源码简洁剖析 09 - Reactor 模型的更多相关文章

  1. Redis 源码简洁剖析 02 - SDS 字符串

    C 语言的字符串函数 C 语言 string 函数,在 C 语言中可以使用 char* 字符数组实现字符串,C 语言标准库 string.h 中也定义了多种字符串操作函数. 字符串使用广泛,需要满足: ...

  2. Redis 源码简洁剖析 03 - Dict Hash 基础

    Redis Hash 源码 Redis Hash 数据结构 Redis rehash 原理 为什么要 rehash? Redis dict 数据结构 Redis rehash 过程 什么时候触发 re ...

  3. Redis 源码简洁剖析 04 - Sorted Set 有序集合

    Sorted Set 是什么 Sorted Set 命令及实现方法 Sorted Set 数据结构 跳表(skiplist) 跳表节点的结构定义 跳表的定义 跳表节点查询 层数设置 跳表插入节点 zs ...

  4. Redis 源码简洁剖析 05 - ziplist 压缩列表

    ziplist 是什么 Redis 哪些数据结构使用了 ziplist? ziplist 特点 优点 缺点 ziplist 数据结构 ziplist 节点 pre_entry_length encod ...

  5. Redis 源码简洁剖析 06 - quicklist 和 listpack

    quicklist 为什么要设计 quicklist 特点 数据结构 quicklistCreate quicklistDelIndex quicklistDelEntry quicklistInse ...

  6. Redis 源码简洁剖析 07 - main 函数启动

    前言 问题 阶段 1:基本初始化 阶段 2:检查哨兵模式,执行 RDB 或 AOF 检测 阶段 3:运行参数解析 阶段 4:初始化 server 资源管理 初始化数据库 创建事件驱动框架 阶段 5:执 ...

  7. Redis 源码简洁剖析 10 - aeEventLoop 及事件

    aeEventLoop IO 事件处理 IO 事件创建 读事件处理 写事件处理 时间事件处理 时间事件定义 时间事件创建 时间事件回调函数 时间事件的触发处理 参考链接 Redis 源码简洁剖析系列 ...

  8. Redis 源码简洁剖析 11 - 主 IO 线程及 Redis 6.0 多 IO 线程

    Redis 到底是不是单线程的程序? 多 IO 线程的初始化 IO 线程运行函数 IOThreadMain 如何推迟客户端「读」操作? 如何推迟客户端「写」操作? 如何把待「读」客户端分配给 IO 线 ...

  9. Redis 源码简洁剖析 12 - 一条命令的处理过程

    命令的处理过程 Redis server 和一个客户端建立连接后,会在事件驱动框架中注册可读事件--客户端的命令请求.命令处理对应 4 个阶段: 命令读取:对应 readQueryFromClient ...

随机推荐

  1. Google Chrome调整控制台的位置

    众所周知,控制台是开发必备的工具,学会流畅的使用控制台会给我们的开发带来不一样的体验,但是控制台的位置有时却是困扰我们的一件事,控制台默认是在浏览器内,有时十分妨碍我们,那么有没有什么办法修改控制台的 ...

  2. Spring Boot实战二:集成Mybatis

    Spring Boot集成Mybatis非常简单,在之前搭建好的项目中加入Mybatis依赖的jar,在配置文件中加入数据库配置即可,如下图所示: 创建对应的Controller.Service.Da ...

  3. Adversarial Training with Rectified Rejection

    目录 概 主要内容 rejection 实际使用 代码 Pang T., Zhang H., He D., Dong Y., Su H., Chen W., Zhu J., Liu T. Advers ...

  4. Universal adversarial perturbations

    目录 概 主要内容 算法 实验部分 实验1 实验2 实验3 代码 Moosavidezfooli S, Fawzi A, Fawzi O, et al. Universal Adversarial P ...

  5. Java——HashMap集合详解

    第一章 HashMap集合简介 1.1 介绍 HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对.HashMap 的实现不是同步的,这意味着它不是线程安 ...

  6. DES对称加密算法实现:Java,C#,Golang,Python

    数据加密标准(DES,Data Encryption Standard,简写DES)是一种采用块密码加密的对称密钥算法,加密分组长度为64位,其中8.16.24.32.40.48.56.64 等8位是 ...

  7. centos6.5-rsync+inotify

    一.目的 通过监控192.168.3.10的目录,实现实时同步. 实验环境 centos1       192.168.3.10 centos2       192.168.3.11 二.配置 cen ...

  8. UI自动化,通过Xpath结合父类、同级元素,查找到唯一的元素定位

    UI自动化,通过Xpath定位的总结 当页面展示的内容的html布局格式一样,只能第几个控件的绝对定位来区分时,如果后面有变更控件的顺序,就会导致找不到这个元素,维护成本较高,可以尝试用其他的方式,比 ...

  9. 初识python: 继承

    继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展. 通过继承创建的新类称为"子类"或"派生类". 被继承的类称为"基 ...

  10. 图片上传,直接在网页中显示(支持IE,谷歌,火狐浏览器)

    <!doctype html><html lang="en"> <head> <meta charset="UTF-8" ...