// 监听套接字
 listen(sock_fd);
 // 被监听的描述符集合
 fd_set rset;int max_fd = sock_fd;
 // 初始化rset数组,使用FD_ZERO宏设置每个元素为0
 FD_ZERO(&rset)
 // 使用FD_SET宏设置rset数组中位置为sock_fd的文件描述符为1,表示需要监听该文件描述符
 FD_SET(sock_fd, &rset)
 // 设置超时事件
 struct timeval timeout;
 timeout.tv_sec = 3;
 timeout.tv_usec = 0;while(1){
 // 调用select函数,检查rset数组保存的文件描述符是否已有读事件就绪,返回就绪的文件描述符个数
 n = select(max_fd+1, &rset, NULL, NULL, &timeout);
 // 调用FD_ISSET宏,在rset数组中检测sock_fd对应的文件描述符是否就绪
 if(FD_ISSET(sock_fd, &rset)){
 // 如果sock_fd已经就绪,表明已有客户端连接;调用accept函数建立连接
 conn_fd = accept();
 // 设置rset数组中位置为conn_fd的文件描述符为1,表示需要监听该文件描述符
 FD_SET(conn_fd, &rset);
 }
// 依次检查已连接套接字的文件描述符
for(i=0;i<max_fd;i++){
    if(FD_ISSET(i, &rset)){
        //有数据可读,则进行读数据处理
        ... ...
    }
}

}

从上可以看出使用select函数进行监听文件描述符的缺点在于监听数量有限,默认能监听的文件描述符是1024,即使修改了默认值,在大并发的情况下还是有数量限制。另外select函数返回后,最后还需要遍历所有描述符集合才能知道哪些文件描述符就绪。


#### poll模型


poll函数完成网络通信的主要流程如下:


1. 创建pollfd数组和监听套接字,并进行绑定;
2. 将监听套接字加入pollfd数组,并设置其监听读事件,也就是客户读的连接请求;
3. 循环调用poll函数,检测pollfd数组中是否有就绪的文件描述符。


![]()


 主要的简易代码逻辑如下,
// 监听套接字和已连接套接字变量
 int sock_fd,conn_fd;
 // 创建套接字
 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具体的进行网络通信的流程如下,


![]()


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多路复用机制的差异点可以用下面这张图来表示,


![]()


### 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);
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;
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;
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;
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模型处理的是客户端和服务器端交互过程中,不同类请求在服务器端引发的待处理事件。


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


![]()


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


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


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


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


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


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


![]()


## 事件驱动框架


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


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


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


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


![]()


如上图所示,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 */