每个cs程序尤其是高并发的网络服务端程序都有自己的网络异步事件处理库,redis不例外。
事件库仅仅包括ae.c、ae.h,还有3个不同的多路复用(本文仅描述epoll)的wrapper文件,事件库封装了框架调用的主循环函数,暴露了时间、文件事件注册和销毁函数,典型的依赖反转模式。
网络操作都在networking.c里,封装了常见的socket操作。
我们从redis启动的main函数开始,从用户发出连接键入命令开始遍历网络事件库所涉及的函数,unix套接口相关函数不表。
首先对几个最常用对象进行解释。
//redis启动的时候(init_server())会创建的一个全局使用的事件循环结构 typedef struct aeEventLoop { int maxfd; //仅仅是select 使用 long long timeEventNextId; aeFileEvent events[AE_SETSIZE]; //用于保存epoll需要关注的文件事件的fd、触发条件、注册函数。 aeFiredEvent fired[AE_SETSIZE]; //epoll_wait之后获得可读或者可写的fd数组,通过aeFiredEvent->fd再定位到events。 aeTimeEvent *timeEventHead; //以链表形式保存多个时间事件,每隔一段时间机会触发注册的函数。 int stop; void *apidata; //每种多路复用方法的使用的私有变量,例如epoll就是epfd和一个事件数组;而select是保存rset、wset。 aeBeforeSleepProc *beforesleep; // sleep之前调用的函数,有些事情是每次循环必须做的,并非文件、时间事件。 } aeEventLoop; //文件可读写事件 typedef struct aeFileEvent { int mask; //触发条件:读、写 aeFileProc *rfileProc; //当fd可读时执行的事件(accept,read) aeFileProc *wfileProc; //当fd可写时执行的事件(write) void *clientData; //caller 传入的数据指针 } aeFileEvent; //超时时间事件 typedef struct aeTimeEvent { long long id; /* time event identifier. */ long when_sec; /* seconds */ long when_ms; /* milliseconds */ aeTimeProc *timeProc; //当出现超时的时候所执行的事件 aeEventFinalizerProc *finalizerProc; void *clientData; //caller传入的数据指针 struct aeTimeEvent *next; //单链表指向下一个时间事件 } aeTimeEvent; //从epoll_wait或者select返回的,已经触发的文件事件 typedef struct aeFiredEvent { int fd; int mask; } aeFiredEvent;
我们来模拟redis server 启动和用户键入命令的过程,先上图。
好吧,从main函数开始吧。
// src/redis.c 853 server.el = aeCreateEventLoop();
创建一个aeEventLoop结构体,成员apidata指向一个aeApiState对象,如果使用epoll,fd是由epoll_create创建的全局epoll fd ,events[] 用于保存epoll_wait返回的事件组。
// src/redis.c 866 server.ipfd = anetTcpServer(server.neterr,server.port,server.bindaddr);
创建监听。
// src/redis.c 903 aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
注册一个时间事件serverCron,作用以后再讲。
// src/redis.c 906 aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, acceptTcpHandler,NULL)
为监听fd的注册一个文件事件,首先把listenfd和触发条件epoll_ctl加入到全局的epoll fd进行监控。再以fd作为文件事件event数组index定位,mask填入只读,rfileProc填入acceptTcpHandler函数。
// src/redis.c 1571 aeSetBeforeSleepProc(server.el,beforeSleep);
aeEventLoop注册一个beforeSleep函数,这个函数在主循环里每次会被调用,作用以后再讲。
// src/ redis.c 1572 aeMain()
网络事件库的核心循环函数。
// src/ae.c 379 aeCreateLoop->beforesleep()
就是上面注册的beforeSleep函数。
// src/ae.c 275 aeProcessEvents(eventLoop, AE_ALL_EVENTS);
先调度aeApiPoll,用epoll_wait处理文件事件,返回的fd和触发条件先存储在eventLoop->fired[]里,然后根据fired[]每个事件的的fd,定位到events,根据触发条件调用已经注册的事件。
文件事件处理完毕后,接下来就是处理超时时间事件,这里不表。
假如有个用户连接上redis,则从redis servere就从上面的aeApiPoll的poll_wait返回,产生的只读事件会调度上面注册了acceptTcpHandler函数。
// src/networking.c 390 acceptTcpHandler(eventLoop, fd, NULL, AE_READABLE)
accept返回产生了一个新连接的fd(这里省略了很多步骤),需要从新的连接读取数据,于是为这个fd注册可读事件。
// src/networking.c 20 aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c)
于是调用了aeCreateFileEvent 为只读事件注册, 这里的步骤和上面的listen fd 一样。用epoll_ctl将fd加入到epoll fd里等待下次epoll_wait,再注册触发条件和readQueyFromClient函数,接着aeMain()继续执行等待用户的数据。
假如用户在这个连接键入redis命令例如:set foo bar,redis server端将会从aeApiPoll的epoll_wait返回,和accept一样的处理方式。返回的fd填入到fired[]数组,通过fired[fd]->fd找到是哪个文件事件,找到events[fd]->rfielProc这个函数,就是上面注册的readQueyFromClient 函数。
// src/networking.c 816 readQueyFromClient(server.el, fd, NULL, AE_READABLE)
这个函数首先会执行read从tcp buffer读取用户键入的命令(非阻塞io),然后处理buffer,找到对应的command(lookupCommand),接下来执行这个command(call(c,cmd)),命令执行完毕需要返回结果的时候,会再次注册一个文件事件
// src/networking.c 71 aeCreateFileEvent(server.el, c->fd, AE_WRITABLE, sendReplyToClient, c)
这样下次循环epoll_wait的时候就发现这个fd可写,于是就会执行sendReplyToClient,讲结果发送给client。
redis的这个网络事件库是比较标准的网络框架的模式,实现的功能不算多但够用。