在创建完文件描述符FD之后,Redis就拥有了一个可以监听客户端事件的句柄了。
注意在创建fd之后,实际上并没有开始接受客户端连接。
接受客户端连接请求过程分两个部分,
· accept
· wait
accept做的事情是等待客户端连接。
当我们在服务器上运行了 redis-server,Redis会启动并开始初始化,在初始化完成时,Redis持有了一个文件描述符,并且开始等待连接。
直到有一个客户端启动了 redis-cli,
这个时候服务端才会收到一个连接请求。
下面分accept和wait两个部分说明注册的逻辑。
accept
accept可以细分为两个步骤,
- 打开6379默认端口,创建socket连接监听描述符。
- 创建socket连接的文件描述符。
创建监听描述符
Redis允许通过ipv4和ipv6两种方式来连接服务端。
在 aeCreateEventLoop 完成后,跟着就是打开端口开始监听,
redis.c line 1679
/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
ListenToPort 做了一件事,它打开了默认端口,然后设置socket的协议族等参数,创建一个文件描述符s,
从listenToPort往下跟,会来到 anet.c,按划分这个文件属于网络模块。
最终来到 _anetTcpServer() 函数。
static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{
int s, 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; /* No effect if bindaddr != NULL */
if ((rv = getaddrinfo(bindaddr,_port,&hints,&servinfo)) != 0) {
anetSetError(err, "%s", gai_strerror(rv));
return ANET_ERR;
}
for (p = servinfo; p != NULL; p = p->ai_next) {
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1) // <- 创建socket
continue;
if (af == AF_INET6 && anetV6Only(err,s) == ANET_ERR) goto error;
if (anetSetReuseAddr(err,s) == ANET_ERR) goto error;
if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) goto error;
goto end;
}
if (p == NULL) {
anetSetError(err, "unable to bind socket");
goto error;
}
error:
s = ANET_ERR;
end:
freeaddrinfo(servinfo);
return s;
}
上面代码注释的部分,就是创建一个socket连接了。返回的是一个文件描述符s,这个s会给到哪里呢。这要来看 listToPort的源码,
redis.c line 1573
int listenToPort(int port, int *fds, int *count) {
int j;
/* Force binding of 0.0.0.0 if no bind address is specified, always
* entering the loop if j == 0. */
if (server.bindaddr_count == 0) server.bindaddr[0] = NULL;
for (j = 0; j < server.bindaddr_count || j == 0; j++) {
if (server.bindaddr[j] == NULL) {
/* Bind * for both IPv6 and IPv4, we enter here only if
* server.bindaddr_count == 0. */
fds[*count] = anetTcp6Server(server.neterr,port,NULL,
server.tcp_backlog); // 创建 socket 的调用
if (fds[*count] != ANET_ERR) {
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
fds[*count] = anetTcpServer(server.neterr,port,NULL,
server.tcp_backlog); // 创建 socket 的调用
if (fds[*count] != ANET_ERR) {
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
/* Exit the loop if we were able to bind * on IPv4 or IPv6,
* otherwise fds[*count] will be ANET_ERR and we'll print an
* error and return to the caller with an error. */
if (*count) break;
} else if (strchr(server.bindaddr[j],':')) {
/* Bind IPv6 address. */
fds[*count] = anetTcp6Server(server.neterr,port,server.bindaddr[j],
server.tcp_backlog);
} else {
/* Bind IPv4 address. */
fds[*count] = anetTcpServer(server.neterr,port,server.bindaddr[j],
server.tcp_backlog);
}
if (fds[*count] == ANET_ERR) {
redisLog(REDIS_WARNING,
"Creating Server TCP listening socket %s:%d: %s",
server.bindaddr[j] ? server.bindaddr[j] : "*",
server.port, server.neterr);
return REDIS_ERR;
}
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
return REDIS_OK;
}
上面代码注释的部分就是调用 _anetTcpServer() 的地方。
注意看 fds[],它是从参数来的,redis.c 的调用中传进来 listenToPort 的第二个参数是个指针,
也就是这里的 fds。
当我们创建完socket之后会得到一个文件描述符s,在listenToPort 这里就会把s赋值给 fds。
回到redis.c中,你会发现刚刚调用 listenToPort 那里,第二个参数就是 server.ipfd 了
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
现在记住 server.ipfd 保存的是和 socket 相关的fd。
创建连接描述符
在 redis.c 的1747行。
void initServer(void) {
...
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR) // 创建连接描述符
{
redisPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
这部分代码在创建event loop之后,意味着在accept客户端连接之前,必须要有一个可用的EL(event loop),在这之前的客户端连接是无效的。
主要看这个函数,
aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL)
首先,server.ipfd[] ,眼熟不。
上面说到在端口6379上打开了socket,得到一个文件描述符s,然后赋值给 server.ipfd。
这里redis把他作为参数用来创建一个文件事件。
然后是 acceptTcpHandler,这是个函数指针,它的实现在 networking.c 的 570 行。
这个函数分两部分分析,
· aeCreateFileEvent()
· acceptTcpHandler()
aeCreateFileEvent是真正创建监听描述符的函数。而acceptTcpHandler是处理当监听到客户端连接了,后续要做的操作。
aeCreateFileEvent
这是整个Redis里创建文件事件的主函数,到处都会看到它,甚至会看到有些函数反复调用。
但不要慌!它不过是个函数。
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) //根据 fd 创建一个监听fd的文件描述符
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;
}
函数实现非常简洁。重点关注中间调用 aeApiAddEvent 的地方。
前面说过Redis会根据不同的系统架构使用不同的函数库,aeApiAddEvent 在Redis中也有四个不同的实现。
还是默认以 ae_epoll 中的实现为例来分析。
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata; //取el中的 apidata
struct epoll_event ee;
/* 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.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1; //注册文件描述符
return 0;
}
首先它从Redis中的eventloop 里获取 apidata ,这是个指针,意味着它想直接改变它的值。
在中间经过一些参数初始化后,就调用系统内核 epoll_ctl 函数注册文件描述符。
传进去的 epfd,注意是从 eventloop 里获取的,它是全局唯一的。
也就意味着Redis其实只创建了一个文件描述符 epfd,用于监听所有的其他的文件描述符。
这是个很关键的地方,因为后面的eventloop是个while(true)循环,在循环里不断地从 epfd 里取数据。如果没有弄明白这个 epfd -> fd[] 的一对多映射关系,就会弄不清楚Redis的核心逻辑。
在 epoll_ctl 执行完之后,如果结果不是-1的话就说明成功了。
然后Redis会得到一个全局唯一的 epfd 文件描述符。
回到 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]; //以fd为下标取一个文件事件结构体
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc; // 把 proc 指针函数赋值给这个文件事件
if (mask & AE_WRITABLE) fe->wfileProc = proc; // 把 proc 指针函数赋值给这个文件事件
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
&eventLoop->events[fd] 在redis初始化的时候就已经创建好并分配内存了,这时候只是从数组里按文件描述符取对应的可用event结构体出来。
在注册文件描述符成功之后,就可以把函数指针赋值给这个文件事件了。
这里有个疑问,Redis把文件事件放进去了,
那什么时候取,怎么取?
简单说的话,Redis会启动一个循环,不断地读取 epfd ,看看是不是有新数据进来。
内核在fd发生数据IO的时候会有触发 epfd 的电平变化,如果发现 epfd 有数据。那么只要从 events[] 中按对应的 fd下标取 aeFileEvent 结构体,就可以拿到对应的处理函数 proc了。
这个循环就是事件驱动了,也叫事件循环,在 ae.c 的 aeMain() 中可以看到。
acceptTcpHandler
说完文件描述符注册,接下来是 acceptTcpHandler 是什么逻辑了。
前面说到在 accept之前,Redis只是创建了个链接,但并没有真正和客户端连接上。需要客户端发起请求,服务端accept了才真正开始。
Redis在socket上收到客户端连接请求之后,fd会触发高电平。
linux内核在这个时候会把这个事情通知给Redis注册的 epfd,eventloop在发现epfd有数据进来的时候就会取到这个 aeFileEvent结构体,而acceptTcpHandler就保存在这个结构体中。
acceptTcpHandler的真正实现是在 networking.c 的542行。
static void acceptCommonHandler(int fd, int flags) {
redisClient *c;
if ((c = createClient(fd)) == NULL) { // 对每个客户端创建一个 redisClient
redisLog(REDIS_WARNING,
"Error registering fd event for the new client: %s (fd=%d)",
strerror(errno),fd);
close(fd); /* May be already closed, just ignore errors */
return;
}
/* If maxclient directive is set and this is one client more... close the
* connection. Note that we create the client instead to check before
* for this condition, since now the socket is already set in non-blocking
* mode and we can send an error for free using the Kernel I/O */
if (listLength(server.clients) > server.maxclients) { //超出连接数量,返回错误信息
char *err = "-ERR max number of clients reached\r\n";
/* That's a best effort error message, don't check write errors */
if (write(c->fd,err,strlen(err)) == -1) {
/* Nothing to do, Just to avoid the warning... */
}
server.stat_rejected_conn++;
freeClient(c);
return;
}
server.stat_numconnections++;
c->flags |= flags;
}
到这里就真正的完成了接收连接的工作。
代码逻辑比较简单,这里有一个比较精妙的设计是在超出连接后该怎么办?
作者对这种情况的设计是,
1.首先允许建立连接
2.通过这个连接返回错误信息
3.如果返回错误信息失败也不做过多的尝试,反正已经尽力了
4.关闭这个连接
你会发现其实这个连接是可以建立的,并不是因为性能等各项原因不能建立。而且作者也不会对它做过多的错误尝试,反正也不关心客户端能不能收到错误信息。
这就是避免过度设计的哲学体现。
wait
wait才是Redis的重头戏。
上面说Redis创建了一个全局的文件描述符 epfd,通过eventloop去判断它是否有数据进来。
这部分的代码逻辑在 ae.c 的 352 行
/* Process every pending time event, then every pending file event
* (that may be registered by time event callbacks just processed).
* Without special flags the function sleeps until some file event
* fires, or when the next time event occurs (if any).
*
* If flags is 0, the function does nothing and returns.
* if flags has AE_ALL_EVENTS set, all the kind of events are processed.
* if flags has AE_FILE_EVENTS set, file events are processed.
* if flags has AE_TIME_EVENTS set, time events are processed.
* if flags has AE_DONT_WAIT set the function returns ASAP until all
* the events that's possible to process without to wait are processed.
*
* The function returns the number of events processed. */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
...
numevents = aeApiPoll(eventLoop, tvp); //line 400
上面省略了一些代码,重头戏在 400行这里。
aeApiPoll,照惯例分析 ae_epoll 的 aeApiPoll 函数。
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;
}
在这个函数里面会调用内核接口 epoll_wait,
在指定的时间参数 tvp内,如果没有事件发生则直接返回。
有事件发生,retval则是总的事件数量。
比如有一个客户端连接,客户端发送了SET命令,那么retval就是1,两个客户端这里就是2。
可以在这里打印系统日志跟踪实际的执行情况。
回到 ae.c 中,
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
在 aeApiPoll接下来的代码里Redis做了一件事,
根据发生的事件描述符,取对应的 aeFileEvent 结构体,还记得这些结构体的proc指针吗,
proc指针保存了一开始 aeCreateFileEvent 的时候传进去的函数指针。
这里就是真正地执行proc函数指针的地方。
小结
Redis的事件驱动实际还是比较复杂的,需要花不少时间反复看代码才能清晰地理解里面的操作逻辑。
里面有非常多精妙的设计,看完事件驱动之后能明白为什么Redis是人类编程精华。