Go最全Redis事件驱动框架(3),阿里大牛整理

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

sock_fd = socket();
// 绑定套接字
bind(sock_fd);
// 监听套接字
listen(sock_fd);

// poll函数可以监听的文件描述符数量,可以大于1024
#define MAX_OPEN = 2048

// pollfd结构体数组,对应文件描述符
struct pollfd client[MAX_OPEN];

// 将创建的监听套接字加入pollfd数组,并监听其可读事件
client[0].fd = sock_fd;
client[0].events = POLLRDNORM;
maxfd = 0;

//初始化client数组其他元素为-1
for(i = 1;i < MAX_OPEN; i++){
client[i].fd = -1;
}

while(1){
// 调用poll函数,检测client数组里的文件描述符是否有就绪的,返回就绪的文件描述符个数
n = poll(client, maxfd+1, &timeout);
// 如果监听套接字的文件描述符有可读事件,则进行处理
if(client[0].revents & POLLRDNORM){
conn_fd = accept();

    // 保存已建立连接套接字
    for(i = 1;i< MAX_OEPN;i++){
        if(client[i].fd < 0){
            client[i].fd = conn_fd;
            client[i].events = POLLRDNORM;
            break;
        }
    }
    maxfd = i;
}
    
// 依次检查已连接套接字的文件描述符
for(i=0;i<MAX_OPEN;i++){
    if(client[i].revents & (POLLRDNORM | POLLERR)){
        //有数据可读,则进行读数据处理
        ... ...
    }
}

}


与select函数相比,poll函数的改进之处主要就在它允许一次监听超过1024个文件描述符。但是调用poll函数后,仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。


#### epoll模型


epoll机制使用epoll\_event结构体来记录待监听的文件描述符和监听事件类型。常见的事件类型有下面几种:


* EPOLLIN:读事件,表示文件描述符对应套接字有数据可读;
* EPOLLOUT:写事件,表示文件描述符对应套接字有数据要写;
* EPOLLERR:错误事件,表示文件描述符对应套接字出错。


epoll\_data联合体以及epoll\_event结构体如下,



typedef union epoll_data
{

int fd;

} epoll_data_t;

struct epoll_event
{
// epoll监听的事件类型
uint32_t events;
// 应用程序数据
epoll_data_t data;
}


select和poll函数在创建好文件描述符集合或pollfd数组后,就可以往数组中添加需要监听的文件描述符了。而对于epoll首先需要调用epoll\_create函数,创建一个epoll实例。这个实例里维护了待监听的文件描述符和已就绪的文件描述符。这样就不需要在后面进行遍历查询哪些是已经就绪的文件描述符了。epoll具体的进行网络通信的流程如下,


![](https://img-blog.csdnimg.cn/447fdb205639426f82cea92e69f62565.png)


epoll简易代码流程如下,



// 监听套接字和已连接套接字变量
int sock_fd,conn_fd;
// 创建套接字
sock_fd = socket();
// 绑定套接字
bind(sock_fd);
// 监听套接字
listen(sock_fd);

// 创建epoll实例对象
epfd = epoll_create(EPOLL_SIZE);
// 创建epoll_event结构体数组,保存套接字对应文件
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE);

// 创建epoll_event变量
struct epoll_event ee
// 监听读事件
ee.events = EPOLLIN;
// 监听的文件描述符是刚创建的监听套接字
ee.data.fd = sock_fd;
// 将监听套接字加入到监听列表中
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ee);

while(1){
n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
for(i=0; i<n; i++) {
// 如果是监听套接字描述符就绪,表明有一个新客户端连接到来
if(ep_events[i].data.fd == sock_fd){
conn_fd = accept(sock_fd);
ee.events = EPOLLIN;
ee.data.fd = conn_fd;
// 添加对新创建的已连接套接字描述符的监听
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &&ee);
} else {
// 读取数据并处理

}
}
}


这样Redis在实现网络通信框架时,基于epoll机制中的epoll\_create、epoll\_ctl和epoll\_wait等函数和读写事件,实现了网络通信的事件驱动框架,从而使得Redis虽然是单线程运行,但仍然能高效应对高并发的客户端访问。


#### 小结


Linux三种IO多路复用机制的差异点可以用下面这张图来表示,


![](https://img-blog.csdnimg.cn/3ab2b374fcc34c378377fd052997ace3.png)


### Redis源码进阶


从上面了解了事件驱动框架的基础,那么下面来看看Redis源码中的实现,


找到ae\_select.c文件,它就使用了select机制实现IO多路复用,


前面定义了fd\_set 结构的读事件集合和写事件集合,\_表示已就绪的文件描述符集合。


先看看fd\_set的数据结构,



// long int类型的数组,数组一共32个元素(1024/32=32),每个元素是32位(long int类型的大小),每一位可以用来表示一个文件描述符的状态
typedef struct{

__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];

} fd_set



#include <sys/select.h>
#include <string.h>

typedef struct aeApiState {
fd_set rfds, wfds;
/* We need to have a copy of the fd sets as it’s not safe to reuse
* FD sets after select(). */
fd_set _rfds, _wfds;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));

if (!state) return -1;
FD_ZERO(&state->rfds);
FD_ZERO(&state->wfds);
eventLoop->apidata = state;
return 0;

}

static int aeApiResize(aeEventLoop eventLoop, int setsize) {
/
Just ensure we have enough room in the fd_set type. */
if (setsize >= FD_SETSIZE) return -1;
return 0;
}

static void aeApiFree(aeEventLoop *eventLoop) {
zfree(eventLoop->apidata);
}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;

if (mask & AE_READABLE) FD_SET(fd,&state->rfds);
if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds);
return 0;

}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;

if (mask & AE_READABLE) FD_CLR(fd,&state->rfds);
if (mask & AE_WRITABLE) FD_CLR(fd,&state->wfds);

}

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, j, numevents = 0;

memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));
memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));

retval = select(eventLoop->maxfd+1,
            &state->_rfds,&state->_wfds,NULL,tvp);
if (retval > 0) {
    for (j = 0; j <= eventLoop->maxfd; j++) {
        int mask = 0;
        aeFileEvent *fe = &eventLoop->events[j];

        if (fe->mask == AE_NONE) continue;
        if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))
            mask |= AE_READABLE;
        if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))
            mask |= AE_WRITABLE;
        eventLoop->fired[numevents].fd = j;
        eventLoop->fired[numevents].mask = mask;
        numevents++;
    }
}
return numevents;

}

static char *aeApiName(void) {
return “select”;
}


再看ae\_epoll.c文件,它使用了epoll机制实现IO多路复用,



#include <sys/epoll.h>

typedef struct aeApiState {
int epfd;
struct epoll_event *events;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));

if (!state) return -1;
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
    zfree(state);
    return -1;
}
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
    zfree(state->events);
    zfree(state);
    return -1;
}
eventLoop->apidata = state;
return 0;

}

static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;

state->events = zrealloc(state->events, sizeof(struct epoll_event)*setsize);
return 0;

}

static void aeApiFree(aeEventLoop *eventLoop) {
aeApiState *state = eventLoop->apidata;

close(state->epfd);
zfree(state->events);
zfree(state);

}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState state = eventLoop->apidata;
struct epoll_event ee = {0}; /
avoid valgrind warning /
/
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;

}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState state = eventLoop->apidata;
struct epoll_event ee = {0}; /
avoid valgrind warning */
int mask = eventLoop->events[fd].mask & (~delmask);

ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {
    epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
    /* Note, Kernel < 2.6.9 requires a non null event pointer even for
     * EPOLL_CTL_DEL. */
    epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
}

}

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;

retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
        tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/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;
        if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
        eventLoop->fired[j].fd = e->data.fd;
        eventLoop->fired[j].mask = mask;
    }
}
return numevents;

}

static char *aeApiName(void) {
return “epoll”;
}


## Reactor模型的工作机制


Reactor模型是网络服务器端用来处理高并发网络IO请求的一种编程模型。


它基于三类事件和三个角色来处理高并发请求。


三类事件分别是连接事件、写事件、读事件;


三个角色分别是reactor、acceptor、handler。


### 事件类型与关键角色


如果了解过RBAC(Role-Based Access Control)基于角色的访问控制模型的人应该知道用户、角色、权限三者之间的关系。类似的,我们先来看看这三类事件和Reactor模型的关系。


Reactor模型处理的是客户端和服务器端交互过程中,不同类请求在服务器端引发的待处理事件。


* 连接事件:当一个客户端要和服务端进行交互时,客户端向服务器端发送连接请求,以建立连接,这里对应的服务器端的一个连接事件;
* 写事件:连接建立后,客户端会给服务器端发送读请求,以便读取数据。服务器端在处理读请求时,需要向客户端写回数据,这里对应的服务器端的一个写事件;
* 读事件:无论客户端给服务器端发送读或写请求,服务器端都需要从客户端读取请求内容,这里对应的服务器端的一个读事件。


![](https://img-blog.csdnimg.cn/43f0d41e35ca414081452443e22e6704.png)


 这三类事件和三个角色的关系如下,


首先,连接事件由acceptor来处理,负责接收连接;


acceptor在接收连接后,会创建handler,用于网络连接上对后续读写事件的处理;


其次,读写事件由handler处理;


最后,在高并发场景中,连接事件、读写事件会同事发生,所以,我们需要有一个角色专门监听和分配事件,这个就是reactor角色。当有连接请求时,reactor将产生的连接事件交由acceptor处理;当有读写请求时,reactor将读写事件交由handler处理。


它们和事件的关系如下图,


![](https://img-blog.csdnimg.cn/8923073538ea45ec973eec7098d3b2c1.png)


## 事件驱动框架


事件驱动框架包括两部分:


1. 事件初始化;
2. 事件捕获、分发和处理主循环。


事件初始化是在服务器程序启动时就执行的,它的作用主要是创建需要监听的事件类型,以及该类事件对应的handler。而一旦服务器完成初始化后,事件初始化也就相应完成了,服务器程序就需要进入到事件捕获、分发和处理的主循环中。 


Reactor模式也叫反应堆模式,它是一种事件驱动机制,逆转了事件处理的流程,不再是主动地等事件就绪,而是提前注册好回调函数,当有对应事件发生就调用回调函数。在while循环中,我们需要捕获发生的事件、判断事件类型,并根据事件类型调用初始化创建好的事件的回调函数来处理事件。主要的处理流程如下,


![](https://img-blog.csdnimg.cn/5a6e14b75bc54bdc845c6f72da847ae5.png)


如上图所示,Reactor模型的基本工作机制就是,客户端的不同类型的请求会在服务器端出发连接、读、写三类事件,这三类事件的监听、分发和处理又是由reactor、acceptor、handler三类角色来完成的,然后这三类角色会通过事件驱动框架来实现交互和事件处理。


### Redis对Reactor模型的实现


Redis的网络框架实现了Reactor模型,并且自行开发实现了一个事件驱动框架(对应的文件为ae.c)。在ae.h头文件中Redis为了实现事件驱动框架,相应地定义了**事件的数据结构**、**框架主循环函数**、**事件捕获分发函数、事件和handler注册函数**。下面依次来看一下,


#### ae.h中事件的数据结构定义


事件的数据结构是关联事件类型和事件处理函数的关键要素。而Redis的事件驱动框架定义了两类事件——IO事件和时间事件,分别对应了客户端发送的网络请求和Redis自身的周期性操作。


如下,IO事件aeFileEvent中,


**mask**:用来表示事件类型的掩码。对于网络通信的事件来说,主要有AE\_READABLE、AE\_WRITEABLE和AE\_BARRIER三类事件。框架在分发事件时,依赖的就是结构体中的事件类型。


**refileProc和wfileProc**:分别指向AE\_REABLE和AE\_WRITEABLE这两类事件的处理函数,也就是Reactor模型中的handler。框架在分发事件后,就需要调用结构体中定义的函数进行事件处理。


**clientData**:用来指向客户端私有数据的指针。



/* File event structure /
typedef struct aeFileEvent {
int mask; /
one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;


事件事件aeTimeEvent如下,



/* Time event structure /
typedef struct aeTimeEvent {
long long id; /
time event identifier. /
long when_sec; /
seconds /
long when_ms; /
milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
} aeTimeEvent;


除了事件的数据结构以外,在头文件中还定义了支撑框架运行的主要函数,有负责事件和handler注册的aeCreateFileEvent函数,有负责事件捕获和分发的aeProcessEvents函数,有框架主循环的aeMain函数,头文件中的原型定义如下,



/* Prototypes */
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
void aeMain(aeEventLoop *eventLoop);


#### **ae.c中框架主循环函数**


找到对应的循环主函数实现如下,循环判断停止标记,如果循环标记设置为true,那么针对事件捕获、分发和处理的整个主循环就停止了。



void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}


 那么主循环函数是在哪呗调用的呢?找到server.c文件,在Redis服务器初始化时调用了aeMain函数开始执行事件驱动框架,如下,



int main(int argc, char **argv) {

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

leep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}


 那么主循环函数是在哪呗调用的呢?找到server.c文件,在Redis服务器初始化时调用了aeMain函数开始执行事件驱动框架,如下,



int main(int argc, char **argv) {

[外链图片转存中…(img-VWoE3kJA-1715818505843)]
[外链图片转存中…(img-wmeMBr2G-1715818505843)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值