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:
- 调用populateCommandTableParseFlags,解析c.sflags字符串,填写c.flags
- c->id = ACLGetCommandID(c->name); 用户权限相关,忽略
- dictAdd(server.commands, sdsnew(c->name), c) 把命令加入commands命令表
- dictAdd(server.orig_commands, sdsnew(c->name), c) 把命令加入原始命令表,这个是防止rename-command把命令的name改了,记一下原本的name
命令表以name为key,redisCommand数据结构为value,结构如下
至此,命令都加入了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的大体结构如下图:
创建过程
el通过aeCreateEventLoop函数创建,主要流程如下:
- 设置一些基础属性,主要注意这两句,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
- 设置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用来接收连接请求的文件描述符
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,这一块如图所示:
这样,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()总共就分为两部
- 接收就绪事件,调用aeApiPoll()等待就绪事件,把就绪的fd加入fired[]
- 处理就绪事件,根据就绪事件的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()做了两件事:
- 调用epoll_wail()等待就绪事件,函数返回时,就绪事件都被放进了el.apidata.events[]数组中
- 遍历所有就绪事件,把就绪事件的fd放进el.fired
数据流向大概如图所示:
现在,就绪的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);
}
}
- 通过anetTcpAccept()函数,其实就是调用了accept()等到了连接请求,获得一个新的连接描述符cfd,顺带填写了一下cip
- connCreateAcceptedSocket()创建了一个connection对象,把fd挂在connection下,state设为CONN_STATE_ACCEPTING
- 调用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。
现在系统结构如上所示,client是为用户连接创建的,它的fd被加入epfd多路复用监听,并与el.events[fd]绑定,,当有命令发送到fd,会触发其读事件,也就是readQueryFromClient()
至此,后续用户的命令都会触发fd,fd调用readQueryFromClient(),用户命令发送过来的处理流程都在这个函数里面
到这里,我们就讲完了事件的初始化以及处理连接请求的过程!鼓掌!
下一章开始讲处理用户请求的过程