redis服務器是一個事件驅動程序,服務器主要處理以下兩類事件:
- 文件事件:redis服務器通過套接字與客戶端進行連接,通信時會產生相應的文件事件,而服務器則通過監聽並處理這些事件來完成一系列網絡通信操作。簡言之文件事件就是服務器對套接字操作的抽象;
- 時間事件:redis服務器中的一些操作需要在給定的時間點執行,而時間事件就是服務器對這類定時操作的抽象。
本文只借文件事件來研究redis中事件,時間事件以後再介紹。這裏還是從initServer()方法中的部分代碼看起:
...
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
...
// Create an event handler for accepting new connections in TCP and Unix domain sockets.
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) {
serverPanic("Unrecoverable error creating server.ipfd file event.");
}
}
...
if (server.sofd > 0 && aeCreateFileEvent(server.el, server.sofd, AE_READABLE, acceptUnixHandler,NULL) == AE_ERR) {
serverPanic("Unrecoverable error creating server.sofd file event.");
}
...
先是調用了aeCreateEventLoop()方法創建以及初始化aeEventLoop結構體並返回複製給server.el。方法及結構體如下:
/* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */
aeFiredEvent *fired; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
int flags;
} aeEventLoop;
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop;
int i;
if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
eventLoop->setsize = setsize;
eventLoop->lastTime = time(NULL);
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
eventLoop->stop = 0;
eventLoop->maxfd = -1;
eventLoop->beforesleep = NULL;
eventLoop->aftersleep = NULL;
eventLoop->flags = 0;
if (aeApiCreate(eventLoop) == -1) goto err;
/* Events with mask == AE_NONE are not set. So let's initialize the
* vector with it. */
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
...
}
結構體中定義了文件事件、時間事件、已註冊事件的最大描述符等信息,初始化時maxfd=-1表明當前還沒有註冊任何事件。回到上文的initServer()方法中,下面兩部分分別是將tcp套接字和unix域套接字通過aeCreateFileEvent()方法創建文件事件,並針對不同的事件類型,事件處理程序也不同。如這裏tcp套接字的事件處理程序是acceptTcpHandler()方法;而unix域套接字的事件處理程序是acceptUnixHandler()方法。當服務端接收到應答不同套接字的信息,就會調用對應的事件處理程序進行處理。
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)
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;
}
如上可以看到,在aeCreateFileEvent()方法中實際是調用aeApiAddEvent()方法將套接字註冊到之前定義的server.el中,這裏如果選擇的是epoll系統調用,那麼aeApiAddEvent()方法體如下:
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;
}
可以看到實際上調用的還是epoll_ctl()方法。再回到aeCreateFileEvent()方法中,同時也將事件處理程序也賦值給server.el結構體中對應套接字的文件事件結構體中。
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
這裏在寫法上它並不是直接將事件處理程序賦值,而是聲明瞭一個指向該套接字對應的文件事件結構體的指針*fe,我猜測是這樣後面賦值語句寫起來會簡潔些。
當有客戶端連接到服務器中監聽的套接字時,文件事件分發器就會調用對應的事件處理程序進行處理。假設這裏是tcp連接,調用的也就是acceptTcpHandler()方法。
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
...
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
...
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
}
int anetTcpAccept(char *err, int s, char *ip, size_t ip_len, int *port) {
...
if ((fd = anetGenericAccept(err,s,(struct sockaddr*)&sa,&salen)) == -1)
return ANET_ERR;
...
return fd;
}
static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len) {
int fd;
while(1) {
fd = accept(s,sa,len);
...
}
return fd;
}
上述三個方法從上往下依次調用,包括創建socket、通過該socket與客戶端建立連接然後進行處理等等。
關於文件事件上文只介紹了其中少數幾個方法,如aeCreateFileEvent()方法是接受一個套接字描述符、一個事件類型以及一個事件處理程序作爲參數,將給定的套接字事件加入到I/O多路複用程序監聽的範圍之內,並將事件和事件處理程序關聯起來。實際上還有其他幾個api:
aeEventLoop *aeCreateEventLoop(int setsize);
void aeDeleteEventLoop(aeEventLoop *eventLoop);
void aeStop(aeEventLoop *eventLoop);
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData);
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);
int aeGetFileEvents(aeEventLoop *eventLoop, int fd);
...
根據方法名基本就可大概猜出方法作用,此處就不多做介紹啦!