Reactor 模型
網絡服務器端
,用了處理高併發網絡 IO
請求的一種編程模型
。
處理 3 類事件:
-
連接事件
:客戶端→服務器的連接請求,對應服務端的連接事件 -
寫事件
:客戶端→服務器的讀請求,服務端處理後要寫回客戶端,對應服務端的寫事件 -
讀事件
:服務端要從客戶端讀取請求內容,對應服務端的讀事件
3 個關鍵角色:
-
acceptor
:處理連接事件,接收連接、創建 handler -
handler
:處理讀寫事件 -
reactor
:專門監聽和分配事件,連接請求 → acceptor、讀寫請求 → handler
事件驅動框架
事件驅動框架就是 Reactor 的具體實現。包括:
-
事件初始化
:創建要監聽的事件類型,及該類事件對應的 handler -
事件捕獲、分發和處理主循環
:- 捕獲發生的事件
- 判斷事件類型
- 根據事件類型,調用對應 handler 處理事件
Redis 如何實現 Reactor 模型
實現代碼:
- 頭文件:
ae.h
- 實現:
ae.c
事件的數據結構:aeFileEvent
Redis 的事件驅動框架定義了 2 類事件:
IO 事件
時間事件
下面介紹 IO 事件 aeFileEvent 的數據結構:
/* File event structure */
typedef struct aeFileEvent {
// 事件類型的掩碼,AE_(READABLE|WRITABLE|BARRIER)
int mask;
// AE_READABLE 事件的處理函數
aeFileProc *rfileProc;
// AE_WRITABLE 事件的處理函數
aeFileProc *wfileProc;
// 指向客戶端私有數據
void *clientData;
} aeFileEvent;
主循環:aeMain 函數
是在 Redis 初始化時調用的,詳見 Redis 源碼簡潔剖析 07 - main 函數啓動。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
// 循環調用
while (!eventLoop->stop) {
// 核心函數,處理事件的邏輯
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
代碼非常簡單,就是循環調用 aeProcessEvents 函數。aeMain 是在 main 函數中被調用的:
// 事件驅動框架,循環處理各種觸發的事件
aeMain(server.el);
// 循環結束,刪除 eventLoop
aeDeleteEventLoop(server.el);
事件捕獲與分發:aeProcessEvents 函數
主體有 3 個 if 分支:
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* 若沒有事件處理,則立刻返回*/
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/*如果有 IO 事件發生,或者緊急的時間事件發生,則開始處理*/
if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
…
}
/* 檢查是否有時間事件,若有,則調用 processTimeEvents 函數處理 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
/* 返回已經處理的文件或時間*/
return processed;
}
核心是第 2 個 if 語句:
// 有 IO 事件發生 || 緊急時間事件發生
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
……
// 調用 aeApiPoll 捕獲事件
numevents = aeApiPoll(eventLoop, tvp);
……
}
aeApiPoll 函數如何捕獲事件?依賴於操作系統底層提供的 IO 多路複用
機制,實現事件捕獲,檢查是否有新的連接、讀寫事件的發生。爲了適配不同的操作系統,Redis 對不同操作系統實現網絡 IO 多路複用函數,進行統一封裝,封裝後的代碼在 4 個文件中實現:
- ae_epoll.c,對應 Linux 上的 IO 複用函數 epoll
- ae_evport.c,對應 Solaris 上的 IO 複用函數 evport
- ae_kqueue.c,對應 macOS 或 FreeBSD 上的 IO 複用函數 kqueue
- ae_select.c,對應 Linux(或 Windows)的 IO 複用函數 select
ae_epoll.c
中 aeApiPoll 函數的實現,核心是調用了 epoll_wait
函數,並將 epoll 返回的事件信息保存起來。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
// 調用 epoll_wait 獲取監聽到的事件
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/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 | AE_READABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE | AE_READABLE;
// 保存事件信息
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}
在 Mac 上查看源碼,aeApiPoll 方法會進入 ae_kqueue.c
中:
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
if (tvp != NULL) {
struct timespec timeout;
timeout.tv_sec = tvp->tv_sec;
timeout.tv_nsec = tvp->tv_usec * 1000;
retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
&timeout);
} else {
retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
NULL);
}
if (retval > 0) {
int j;
/* Normally we execute the read event first and then the write event.
* When the barrier is set, we will do it reverse.
*
* However, under kqueue, read and write events would be separate
* events, which would make it impossible to control the order of
* reads and writes. So we store the event's mask we've got and merge
* the same fd events later. */
for (j = 0; j < retval; j++) {
struct kevent *e = state->events+j;
int fd = e->ident;
int mask = 0;
if (e->filter == EVFILT_READ) mask = AE_READABLE;
else if (e->filter == EVFILT_WRITE) mask = AE_WRITABLE;
addEventMask(state->eventsMask, fd, mask);
}
/* Re-traversal to merge read and write events, and set the fd's mask to
* 0 so that events are not added again when the fd is encountered again. */
numevents = 0;
for (j = 0; j < retval; j++) {
struct kevent *e = state->events+j;
int fd = e->ident;
int mask = getEventMask(state->eventsMask, fd);
if (mask) {
eventLoop->fired[numevents].fd = fd;
eventLoop->fired[numevents].mask = mask;
resetEventMask(state->eventsMask, fd);
numevents++;
}
}
}
return numevents;
}
事件註冊:aeCreateFileEvent 函數
main 函數中調用了 createSocketAcceptHandler
:
if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
serverPanic("Unrecoverable error creating TCP socket accept handler.");
}
而 createSocketAcceptHandler
創建接收連接的 handler:
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;
}
其主要是調用了 aeCreateFileEvent
,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];
// 核心
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;
}
Linux 提供了 epoll_ctl API,用於增加新的觀察事件。而 Redis 在此基礎上,封裝了 aeApiAddEvent 函數,對 epoll_ctl 進行調用,註冊希望監聽的事件和相應的處理函數。
ae_epoll.c 中 aeApiAddEvent 實現如下:
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
/* 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;
}
註冊的函數 acceptTcpHandler 在 network.c
中:
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata);
// 每次處理 1000 個
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
anetCloexec(cfd);
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
}
總結
Redis 處理連接、客戶端請求是單線程的,但是這單個線程能夠處理上千個客戶端,就是因爲 Redis 是基於 Reactor 模型的。通過事件驅動框架,Redis 可以使用一個循環不斷捕獲、分發、處理客戶端產生的網絡連接、數據讀寫事件。
當然這裏有一個前提,就是 Redis 幾乎所有數據讀取和處理都是在內存中操作的,服務端對單個客戶端的讀寫請求處理時間極短。
參考鏈接
Redis 源碼簡潔剖析系列
Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~
原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。