redis启动流程、事件机制、以及建立连接过程

649 阅读15分钟

redis server的启动流程很复杂,很多块都可以单独拎出来写一篇文章,本篇文章简单的梳理一下流程,重点关注其中的事件机制和tcp连接的建立!顺便给自己挖几个坑,以后填上。

通过阅读这一段代码重点学习:

  • 多路复用的封装(重点关注epoll)
  • redis加载配置的过程
  • 复习网络编程中建立连接的过程
  • redis的事件机制是怎么实现的 本文章属于保姆级别,讲的非常细,最好对照着源码一起看,不然容易蒙蔽

启动的入口在server.c的main()函数,主要流程如下,关心的步骤重点标出:

struct redisServer server; /* 后面的操作都是对这个全局的server进行操作 */

//设置进程名, 详情见https://blog.csdn.net/TuxedoLinux/article/details/100135213
SPT_init()   

// 初始化配置,填写了一些server的基础配置
// *******命令加载&配置初始化******
initServerConfig() 

// 用户权限,在 Redis 6.0 中引入了 ACL(Access Control List) 的支持,在此前的版本中 Redis 
// 中是没有用户的概念的,其实没有办法很好的控制权限,
// redis 6.0 开始支持用户,可以给每个用户分配不同的权限来控制权限
// 单独开一篇讲
ACLInit();	              

// 判断是否启动哨兵模式
// 涉及到redis的几种模式
// 共有三种:主从模式 VS 哨兵sentinel模式 VS Redis cluster 单独开一个讲
// 后面跟哨兵相关的函数就跳过了
initSentinelConfig()
initSentinel();

// 快照文件检测工具, 关于RDB和AOF,redis持久化,又能单独拎出来讲
if (strstr(argv[0],"redis-check-rdb") != NULL)
        redis_check_rdb_main(argc,argv,NULL);
    else if (strstr(argv[0],"redis-check-aof") != NULL) //日志文件检测工
        redis_check_aof_main(argc,argv);

// ******载入配置文件*******
// 初始化服务端相关配置,比如在这一步会配置 redis 的监听端口
loadServerConfig(server.configfile, config_from_stdin, options);

// *********创建事件循环实例*********
// 设置服务端处理 socket 事件的处理函数及处理定时事件的处理函数
initServer();

// 创建IO多线程,下一篇文章详细讲
InitServerLast();

loadDataFromDisk();
// ********启动事件循环处理线程*******
// 开始接受客户端连接并处理客户端命令
aeMain(server.el);

本章关注redis的事务机制,就是一个命令来,从连接到接受命令到处理命令返回的整个过程

命令加载

命令的加载在initServerConfig() #populateCommandTable()里面

redisCommandTable是一个写死的redisCommand数组

redisCommand结构如下:

struct redisCommand {
    char *name;   
    redisCommandProc *proc;  
    int arity;   
    char *sflags;  
    uint64_t flags; 
    redisGetKeysProc *getkeys_proc;
    int firstkey;
    int lastkey;  
    int keystep; 
    long long microseconds, calls, rejected_calls, failed_calls;
    int id;    
};

对应的含义如下:

属性名作用
name命令的名字,比如 "set"
proc函数指针,指向命令的实现函数,比如 setCommand 。
arity命令参数的个数,用于检查命令请求的格式是否正确。 如果这个值为负数 -N ,那么表示参数的数量大于等于 N 。 注意命令的名字本身也是一个参数, 比如说 SET msg "hello world"命令的参数是 "SET" 、 "msg" 、 "hello world" , 而不仅仅是 "msg" 和 "hello world" 。
sflags字符串形式的标识值, 这个值记录了命令的属性, 比如这个命令是写命令还是读命令, 这个命令是否允许在载入数据时使用, 这个命令是否允许在 Lua 脚本中使用, 等等。
flags对 sflags 标识进行分析得出的二进制标识, 由程序自动生成。 服务器对命令标识进行检查时使用的都是 flags 属性而不是 sflags 属性, 因为对二进制标识的检查可以方便地通过 & 、 ^ 、 ~ 等操作来完成。
calls服务器总共执行了多少次这个命令。
milliseconds服务器执行这个命令所耗费的总时长。

populateCommandTable函数遍历redisCommandTable,对每个redisCommand c:

  1. 调用populateCommandTableParseFlags,解析c.sflags字符串,填写c.flags
  2. c->id = ACLGetCommandID(c->name); 用户权限相关,忽略
  3. dictAdd(server.commands, sdsnew(c->name), c) 把命令加入commands命令表
  4. dictAdd(server.orig_commands, sdsnew(c->name), c) 把命令加入原始命令表,这个是防止rename-command把命令的name改了,记一下原本的name

命令表以name为key,redisCommand数据结构为value,结构如下

image.png

至此,命令都加入了server.commands命令表

配置初始化

initServerConfig()除了加载命令,还调用了initConfigValues()进行配置初始化,其过程是遍历configs[]数组的每一个config,调用下面这句:

config->interface.init(config->data); // 细节在介绍configs[]数组的时候说

server下的各种配置(比如端口什么的)就初始化成了configs[]里写死的默认配置了,下一步是要载入配置文件,将配置为自定义的。

载入配置文件

载入配置文件入口是这一句

// ******载入配置文件*******
// 初始化服务端相关配置,比如在这一步会配置 redis 的监听端口
loadServerConfig(server.configfile, config_from_stdin, options);

步骤如下:

1.打开文件

fp = fopen(filename,"r")

2.逐行加到变量config后面,最后调用loadServerConfigFromString(config);

while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
    config = sdscat(config,buf);

3.loadServerConfigFromString (config)

逐行遍历config,对每一行lines[i]:
    // 1,按空格分割到argv
    argv = sdssplitargs(lines[i],&argc);
    // 2. 将配置命令转为小写
    sdstolower(argv[0]);
    // 3.遍历configs[]数组,找到名字对应的那一条配置,更新server对应的配置
    // 更新方法在下面configs[]数组介绍里说
    // 名字是根据config的name和alias(别名)判断
    if ((!strcasecmp(argv[0],config->name) ||
       (config->alias && !strcasecmp(argv[0],config->alias))))
            config->interface.set(config->data, argv[1], 0, &err)
    // 4.最后是一堆if else特化语句。
    //   根据配置的名字设置server的没有在configs[]数组里的相应属性

configs[]数组

config数组是一个声明在config.c里的全局变量

standardConfig configs[] = {
    ...
    createBoolConfig("rdbchecksum", NULL, IMMUTABLE_CONFIG, server.rdb_checksum, 1, NULL, NULL),
    ...
    createEnumConfig("supervised", NULL, IMMUTABLE_CONFIG, supervised_mode_enum, server.supervised_mode, SUPERVISED_NONE, NULL, NULL),
    ...
    createIntConfig("port", NULL, MODIFIABLE_CONFIG, 0, 65535, server.port, 6379, INTEGER_CONFIG, NULL, updatePort), /* TCP port. */
    ...
    {NULL}

翻译过来大概是:

standardConfig configs[] = {
    // *****一个standardConfig成员开始
    // 下面这些是成员属性
    {
        .name = ("port"), \
        .alias = (NULL), \
        .modifiable = (MODIFIABLE_CONFIG),
        { // interface字段
            .init = (numericConfigInit), \
            .set = (numericConfigSet), \
            .get = (numericConfigGet), \
            .rewrite = (numericConfigRewrite) \
        }
        .data.numeric = { \
            .lower_bound = (0), \
            .upper_bound = (65535), \
            .default_value = (6379), \
            .is_valid_fn = (NULL), \
            .update_fn = (updatePort), \
            .is_memory = (INTEGER_CONFIG),
            .numeric_type = NUMERIC_TYPE_INT, \
            .config.i = &(server.port) \
        } \
    }, /***一个成员结束
    {...},
    {...},
    {NULL}
};

每个配置config的interface.set都是numericConfigSet()函数,这个函数调用SET_NUMERIC_TYPE宏,修改config.numeric.i,这个就是server对应配置的地址,比如上面一段的port配置对应的config.i就是server.port的地址

loadServerConfigFromString()函数遍历configs[]数组的时候对每个config都调用了

config->interface.set(config->data, argv[1], 0, &err)

这句就修改了server配置。

与这个同理,在初始化配置的时候,调用

config->interface.init(config->data);

其实就是调用numericConfigInit()函数将server下的配置设为默认值

static void numericConfigInit(typeData data) {
    SET_NUMERIC_TYPE(data.numeric.default_value)
}

创建事件循环实例 initServer()

这里主要看initServer()函数了,这个函数比较繁琐,本次主要关注事件循环相关,其他的标记一下,来日方长,以后细说

createSharedObjects();
adjustOpenFilesLimit();
/************************************************************/
/* server.el是server下面跟事件相关的重要数据结构,在这里创建事件循环 */
/************************************************************/
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);

// 初始化DB
server.db = zmalloc(sizeof(redisDb)*server.dbnum);

/**********************************************/
/****************重点讲一下********************/
/*********************************************/
listenToPort(server.port,&server.ipfd)
listenToPort(server.tls_port,&server.tlsfd)

// 这一坨没明白
server.sofd = anetUnixServer(server.neterr,server.unixsocket,
    server.unixsocketperm, server.tcp_backlog);
anetNonBlock(NULL,server.sofd);
anetCloexec(server.sofd);

evictionPoolAlloc(); /* Initialize the LRU keys pool. */

aofRewriteBufferReset();

resetServerStats();

//创建定时事件,看懂后面的事件这个就懂了,以后总结
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) 

/************************************************************/
// 处理事件的重点在这里
/************************************************************/
createSocketAcceptHandler(&server.ipfd, acceptTcpHandler)
aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,
        NULL)
aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
        moduleBlockedClientPipeReadable,NULL)

// 设置例行函数,以后讲
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);

多路复用库的选择(evport\select\epoll\kqueue)

可以看到ae.c里面有这一段代码

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

意思就是按照优先级,根据宏判断环境是否有相关库,如果环境里有它就用它(include对应的封装文件),优先级是:

evport > epoll > kqueue > select

本文后续用epoll举例!!!!其他举一反三

redis的事件机制底层使用 evport\select\epoll\kqueue之一,在4者上面鸽子封装了一层,封装的函数是ae_xxx.c(比如epoll封装成ae_epoll.c),封装的文件提供的接口名是一模一样的,4个封装文件都提供如下接口:

  • aeApiCreate() :调用epoll_create
  • aeApiResize()
  • aeApiFree()
  • aeApiAddEvent() :调用epoll_ctl 增加事件
  • aeApiDelEvent() :调用epoll_ctl删除事件
  • aeApiPoll() :调用epoll_wait等待就绪事件,并把就绪事件放进el.fired[]数组
  • aeApiName() :return "epoll";

这里补充一下epoll的用法:

1.epoll_create()创建一个epfd
2.epoll_ctl()将要监视的socket加入epfd
3.epoll_wait()等待IO事件。如果当前没有可用的事件,这个函数会阻塞调用线程

server.el的整体结构和创建过程

el的大体结构如下图:

image.png

创建过程

el通过aeCreateEventLoop函数创建,主要流程如下:

  1. 设置一些基础属性,主要注意这两句,server.el.events和fired在这里被初始化好的:
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);

在el中:

  • fired是就绪事件表
  • events是未就绪事件表

2.创建epfd

aeApiCreate(eventLoop)

调用epoll_create()创建el.apidata.epfd

  1. 设置enents的MASK
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;

至此,创建好了server.el,以及它下面的epfd

listernToPort()

initServer()调用了这两句:

listenToPort(server.port,&server.ipfd);
listenToPort(server.tls_port,&server.tlsfd);

我们看看listenToPort()做了什么

函数主体外面是一个for循环,遍历每一个配置里绑定的ip地址(server.bindaddr)

char **bindaddr = server.bindaddr;
for (j = 0; j < bindaddr_count; j++) {
    char* addr = bindaddr[j];
    addr是一个ip地址,下面开始处理addr
}

addr可能是ipv4可能是ipv6地址, 通过strchr()在addr中搜索第一个出现:的位置,如果搜索到了就是ipv6地址

ps:IPv6 地址大小为 128 位。首选 IPv6 地址表示法为 x:x:x:x:x:x:x:x,ipv4是x.x.x.x*

if (strchr(addr,':')) { //ipv6
  sfd->fd[sfd->count] = anetTcp6Server(server.neterr,port,addr,server.tcp_backlog);
} else { //ipv4
  sfd->fd[sfd->count] = anetTcpServer(server.neterr,port,addr,server.tcp_backlog);
}

ipv4和ipv6的两个函数都是调用_anetTcpServer(),这个函数就是创建了一个socket,并且调用了bind()+listen()完成监听,返回fd 我们来看看这个函数

(下面代码省略了错误处理和特殊case)

static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{
    /*1.初始化一些变量*/
    int s = -1, rv;
    char _port[6];  /* strlen("65535") */
    struct addrinfo hints, *servinfo, *p;
    snprintf(_port,6,"%d",port);
    memset(&hints,0,sizeof(hints));
    hints.ai_family = af;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;   
    
    /*2.主机名到地址解析*/
    rv = getaddrinfo(bindaddr,_port,&hints,&servinfo)
    
    /*3.创建socket bind+listen*/
    for (p = servinfo; p != NULL; p = p->ai_next) {
        s = socket(p->ai_family,p->ai_socktype,p->ai_protocol))
        anetSetReuseAddr(err,s)
        anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog)
        // 没出错的话就return
        return s;
    }
    

这里要复习一下getaddrinfo()函数的用法,getaddrinfo()函数是为了获取调用socket()函数创建socket时传入的参数

int getaddrinfo(const char *node,     
                const char *service,  
                const struct addrinfo *hints, //指定一些基本值
                struct addrinfo **res);//得到链表

listernToPort()第一步初始化变量,主要是addrinfo hints,hints就是给函数返回的addrinfo填写的基本值(给函数一个暗示),这里填写的分别是

hints.ai_family = af;              ipv4还是ipv6
hints.ai_socktype = SOCK_STREAM;   返回的socket类型
hints.ai_flags = AI_PASSIVE;       AI_PASSIVE 标志

getaddrinfo()函数的出参是&servinfo,和hint一样,也是一个addrinfo结构体,这个结构体有*ai_next指针,所以是一个链表

struct addrinfo {
    int	ai_flags;	/* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
    int	ai_family;	/* PF_xxx */
    int	ai_socktype;	/* SOCK_xxx */
    int	ai_protocol;	/* 0 or IPPROTO_xxx for IPv4 and IPv6 */
    socklen_t ai_addrlen;	/* length of ai_addr */
    char *ai_canonname;	        /* canonical name for hostname */
    struct sockaddr *ai_addr;	/* binary address */
    struct addrinfo *ai_next;	/* next structure in linked list */
};

后续就遍历这个链表,取出一个可用的addrinfo,作为socket()的参数,然后调用anetListen(), anetListen函数包括了bind()和listen()

static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
    bind(s,sa,len);
    listen(s, backlog);
    return ANET_OK;
}

小结一下listentoport()函数:

int listenToPort(int port, socketFds *sfd);

对所有绑定的端口,先调用getaddrinfo()获取地址信息,然后调用socket()+bind()+listen()三件套监听了一个socket,然后把这个socket对应的fd加入sfd数组,嗯,就这么简单

经过下面两句

listenToPort(server.port,&server.ipfd);
listenToPort(server.tls_port,&server.tlsfd);

server.ipfd和server.tlsfd就都是listen()过的socket的数组了,本文关注ipfd,它是redis用来接收连接请求的文件描述符

image.png

createSocketAcceptHandler()

下面要看initServer()里创建事件这一坨函数

createSocketAcceptHandler(&server.ipfd, acceptTcpHandler)

函数代码如下:

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

可以看到函数对server.ipfd里的所有fd调用了aeCreateFileEvent()函数,另外这里有个回滚机制,可以学习一下

下面我们重点看aeCreateFileEvent函数

注意:ipfd是用来接收tcp连接的fd数组,下面将进入等待连接,接收连接的过程

aeCreateFileEvent()

直接上精简后的代码,函数并不长

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    aeFileEvent *fe = &eventLoop->events[fd]; 
    aeApiAddEvent(eventLoop, fd, mask);
    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;
}

可以看到重点就是调用aeApiAddEvent(),还记得吗,这是封装多路复用的函数,对于epoll调用的就是epoll_ctl()将ipfd里的fd加入epfd,并且为其绑定一个aeFileEvent fe,这个fe存在el->events[fd]里,第一次调用,传进来的mask是AE_READABLE,为其fe设置一个读操作,就是acceptTcpHandler

记一下,aeCreateFileEvent(enentloop, fd, mask)的主要作用就是将fd加入多路复用监听,并为其在eventloop中绑定读写操作,这个函数后面还要用到

小结一下,createSocketAcceptHandler(&server.ipfd, acceptTcpHandler)通过调用aeCreateFileEvent(),把ipfd[]里的所有fd通过epoll_ctl()函数加入了多路复用监听,并且给他们在server.el.events[]中绑定了一个aeFileEvent fe,这个fe有一个读操作,对应的函数是acceptTcpHandler,这一块如图所示:

image.png

这样,InitServer()里跟事件相关的就差不多了,我们回到main()函数

aeMain(server.el);

aeMain就进入事件循环了,不断的等待事件,处理事件

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

可以看到就是不断的循环aeProcessEvents(server.el),我们来看这个函数

aeProcessEvents()

// 例行函数before和after,以后说
// aeApiPoll()调用epoll_wait()并把就绪fd加入el.fired[]
eventLoop->beforesleep(eventLoop);
numevents = aeApiPoll(eventLoop, tvp);
eventLoop->aftersleep(eventLoop);

// 遍历fired[]处理就绪事件
for (j = 0; j < numevents; j++) {
    aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
    if (xxx) {
        fe->rfileProc(eventLoop,fd,fe->clientData,mask);
        fired++;
        fe = &eventLoop->events[fd];
    }
    if (xxx) {
        fe->wfileProc(eventLoop,fd,fe->clientData,mask);
        fired++;
    }
}

aeProcessEvents()总共就分为两部

  1. 接收就绪事件,调用aeApiPoll()等待就绪事件,把就绪的fd加入fired[]
  2. 处理就绪事件,根据就绪事件的fd绑定的event,调用读或者写函数

接受就绪事件

先看aeApiPoll()

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    // *****1.调用wpoll_wait, 就绪的fd都在states->events里******
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,...);
    if (retval > 0) {
        int j;
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;
            /*这里省略一段对maask的处理*/
            
            // *****2.就绪的fd从states->events放入fired[]******
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

可以看到,aeApiPoll()做了两件事:

  1. 调用epoll_wail()等待就绪事件,函数返回时,就绪事件都被放进了el.apidata.events[]数组中
  2. 遍历所有就绪事件,把就绪事件的fd放进el.fired

数据流向大概如图所示: image.png

现在,就绪的fd都存在fired[]数组里了,下面要对他们处理

处理就绪事件

再看一下aeProcessEvents()处理就绪事件的代码

for (j = 0; j < numevents; j++) {
    aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
    if (xxx) {
        fe->rfileProc(eventLoop,fd,fe->clientData,mask);
        fired++;
        fe = &eventLoop->events[fd];
    }
    if (xxx) {
        fe->wfileProc(eventLoop,fd,fe->clientData,mask);
        fired++;
    }
}

fileProc()时从哪来的呢?还记得aeCreateFileEvent()里创建fd后给它绑定了一个带有读写函数的event吗,就是它了

我们遍历fired[]数组,通过fired[j].fd为索引i,在el.enents[i]找到其绑定的event,然后根据一些mask判断,调用其绑定的读函数rfileProc()或者写函数wfileProc()

我们第一次通过这句注册的是接收tcp请求事件:

createSocketAcceptHandler(&server.ipfd, acceptTcpHandler)

绑定的是读函数acceptTcpHandler,所以这里会调用acceptTcpHandler,处理tcp连接的主逻辑就在这个函数里了

acceptTcpHandler(server.el,fd,fe->clientData,mask)

acceptTcpHandler()处理tcp连接请求

先看下精简后的代码

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];

    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport); // connect fd
        acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
    }
}
  1. 通过anetTcpAccept()函数,其实就是调用了accept()等到了连接请求,获得一个新的连接描述符cfd,顺带填写了一下cip
  2. connCreateAcceptedSocket()创建了一个connection对象,把fd挂在connection下,state设为CONN_STATE_ACCEPTING
  3. 调用acceptCommonHandler()处理上一步创建的connection对象

connection的ConnectionType属性

connection有ConnectionType成员,下面挂了很多函数指针,后面会用到,这里列举一下

ConnectionType CT_Socket = {
    .ae_handler = connSocketEventHandler,
    .close = connSocketClose,
    .write = connSocketWrite,
    .read = connSocketRead,
    .accept = connSocketAccept,
    .connect = connSocketConnect,
    .set_write_handler = connSocketSetWriteHandler,
    .set_read_handler = connSocketSetReadHandler,
    .get_last_error = connSocketGetLastError,
    .blocking_connect = connSocketBlockingConnect,
    .sync_write = connSocketSyncWrite,
    .sync_read = connSocketSyncRead,
    .sync_readline = connSocketSyncReadLine,
    .get_type = connSocketGetType
};

我们继续追踪一下acceptCommonHandler()函数

acceptCommonHandler()

这个函数关键代码就两句:

c = createClient(conn)
connAccept(conn, clientAcceptHandler) //这个没搞明白,先忽略

第一句是创建client

当连接请求来,redis为每个连接请求创建了一个client对象,看看创建的同时做了什么

创建client

client *createClient(connection *conn) {
    client *c = zmalloc(sizeof(client));
    /* 重点 */
    connSetReadHandler(conn, readQueryFromClient);
    connSetPrivateData(conn, c);
    selectDb(c,0);
    uint64_t client_id;
    atomicGetIncr(server.next_client_id, client_id, 1);
    c->id = client_id;
    // 省略一堆属性设置
    if (conn) linkClient(c);
    initClientMultiState(c);
    return c;
}

重点关注**connSetReadHandler(conn, readQueryFromClient);**这一句

static inline int connSetReadHandler(connection *conn, ConnectionCallbackFunc func) {
    return conn->type->set_read_handler(conn, func);
}

之前介绍过,connection有ConnectionType成员,下面写死了很多函数指针,set_read_handler指向的是connSocketSetReadHandler()函数, 上面这句调用的其实是下面这句

connSocketSetReadHandler(conn, readQueryFromClient);

connSocketSetReadHandler()函数中只有一句我们比较关注,下面这句:

aeCreateFileEvent(server.el,conn->fd, 
    AE_READABLE,conn->type->ae_handler,conn)

一开始初始化server的时候我们也调用过aeCreateFileEvent函数来增加监听的fd以及对应事件吗(不记得往上翻翻),当时我们监听的是用户的连接请求,并为其绑定一个操作,而用户的连接请求触发了操作,操作运行到这里,又调用了aeCreateFileEvent()通过多路复用监听一个新的fd,这个fd就是后续用来接收和处理用户发送的命令用的,为fd绑定的读操作是readQueryFromClient。这里监听的fd是通过前面accept()等到了连接请求,分配的一个新的连接描述符cfd。

image.png 现在系统结构如上所示,client是为用户连接创建的,它的fd被加入epfd多路复用监听,并与el.events[fd]绑定,,当有命令发送到fd,会触发其读事件,也就是readQueryFromClient()

至此,后续用户的命令都会触发fd,fd调用readQueryFromClient(),用户命令发送过来的处理流程都在这个函数里面

到这里,我们就讲完了事件的初始化以及处理连接请求的过程!鼓掌!

下一章开始讲处理用户请求的过程