Redis源码剖析(一)服务器与客户端交互流程
Redis中的C/S模型
Redis底層還是基于網絡請求的,對于單機數據庫而言,網絡請求僅僅是在一臺機器上交互,即服務器客戶端都在一臺計算機上
當在終端輸入redis-serve時,便啟動了一個Redis服務器,隨后開始初始化內部數據,對于Redis而言包括
- 讀取配置文件初始化內部參數
- 創(chuàng)建默認數據庫(默認為16個)
- 創(chuàng)建監(jiān)聽套接字并綁定回調函數(接收客戶端連接請求)
- 執(zhí)行事件驅動循環(huán),開始響應客戶端請求
- …
當在終端輸入redis-cli時,遍啟動了一個客戶端
到這里可以簡單的猜測一下,Redis的命令交互流程大致為
- 啟動一個客戶端,請求連接到服務器
- 服務器接收客戶端請求,建立連接成功,服務器開始監(jiān)聽客戶端文件描述符并綁定回調函數
- 客戶端輸入命令,導致服務器端監(jiān)聽的文件描述符變?yōu)榭勺x,服務器開始讀取命令
- 服務器解析命令,并調用對應的命令處理函數
- 服務器將處理結果反饋給客戶端
- 客戶端文件描述符變?yōu)榭勺x,讀取反饋信息,輸出在終端
上述是一個常見的C/S模型,Redis采用Reactor模式處理連接,Reactor模式就是常說的使用io多路復用函數監(jiān)聽客戶端的方法。不過Redis是單線程下的Reactor,在常見的高并發(fā)服務器設計模型中可以使用Reactor+線程池的方法提高并發(fā)性(也叫one loop per thread,muduo網絡庫采用的設計模型)
下面就從源代碼的角度體會服務器和客戶端交互的流程(只截取關鍵部分)
服務器與客戶端的交互流程
服務器監(jiān)聽客戶端連接
當服務器啟動時,首先執(zhí)行的是server.c/main函數,如上所述,main函數進行了大量初始化工作,其中有一項就是創(chuàng)建監(jiān)聽套接字
//server.c int main(int argc, char **argv) {.../* 初始化服務器,創(chuàng)建監(jiān)聽套接字 */initServer();... }initServer函數中同樣進行了大量初始化工作,其中的一部分是創(chuàng)建監(jiān)聽套接字
//server.c /* 服務器啟動時調用,初始化服務器 */ void initServer(void) {.../* 創(chuàng)建監(jiān)聽套接字 *//* ipfd_count是監(jiān)聽套接字的數量 */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.");}}... }創(chuàng)建監(jiān)聽套接字由函數aeCreateFileEvent函數實現(xiàn)
//ae.c /* * 創(chuàng)建文件事件(事件驅動) * eventLoop : 服務器的事件驅動循環(huán)數組* fd : 文件描述符* mask : 需要監(jiān)聽的事件* proc : 回調函數* clientData: 傳給回調函數的參數*/ int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData) {/* 如果要創(chuàng)建的文件描述符大于服務器規(guī)定的大小,則報錯 */if (fd >= eventLoop->setsize) {errno = ERANGE;return AE_ERR;}/* 返回服務器事件驅動循環(huán)中的第fd個事件 */aeFileEvent *fe = &eventLoop->events[fd];/* 將文件描述符和其需要監(jiān)聽的事件添加到io多路復用函數中 */if (aeApiAddEvent(eventLoop, fd, mask) == -1)return AE_ERR;/* 將監(jiān)聽事件保存在事件驅動中 */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; }函數中的aeEventLoop是事件驅動循環(huán),保存所有正在監(jiān)聽的事件,當io復用返回時,會將所有以激活的事件也保存在aeEventLoop中以便于處理,和libevent中的事件驅動循環(huán)作用相同
aeFileEvent是對事件的封裝,內部保存有監(jiān)聽的文件描述符,監(jiān)聽的事件以及回調函數,和libevent中的事件(event)作用相同
接收客戶端連接請求
另外對于傳給aeCreateFileEvent函數的回調函數,可以猜測它的作用主要就是調用accept函數接收客戶端連接請求,建立與客戶端關聯(lián)的文件描述符,注冊到io復用中。它的定義如下
//networking.c /* 服務器監(jiān)聽套接字的回調函數,用于接收客戶端的連接請求 */ 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--) {/* 調用accept接收客戶端連接請求,返回與客戶端關聯(lián)的文件描述符 */cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);.../* 根據文件描述符創(chuàng)建客戶端實例(client對象),用于與客戶端交互 */acceptCommonHandler(cfd,0,cip);} }每當接收到一個客戶端請求時,服務器都會根據客戶端文件描述符創(chuàng)建一個客戶端實例(client類型),client是服務器與客戶端交互的橋梁,客戶端輸入的所有命令都是讀取到client中的緩沖區(qū)再進行處理的
創(chuàng)建客戶端實例
創(chuàng)建客戶端實例的函數定義如下
//networking.c /* * 根據客戶端文件描述符創(chuàng)建客戶端實例* fd : 接收客戶端連接請求時返回的文件描述符* ip : 客戶端地址<ip, port>*/ static void acceptCommonHandler(int fd, int flags, char *ip) {client *c;/* 以客戶端文件描述符創(chuàng)建客戶端實例 */if ((c = createClient(fd)) == NULL) {serverLog(LL_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;}... }該函數調用createClient,執(zhí)行真正創(chuàng)建客戶端實例的操作
//networking.c /* 根據文件描述符創(chuàng)建與客戶端的連接 */ client *createClient(int fd) {/* 申請client大小的內存空間 */client *c = zmalloc(sizeof(client));if (fd != -1) {/* 設置成非阻塞io */anetNonBlock(NULL,fd);anetEnableTcpNoDelay(NULL,fd);/* keep-alive選項 */if (server.tcpkeepalive)anetKeepAlive(NULL,fd,server.tcpkeepalive);/* 為這次連接創(chuàng)建事件,監(jiān)聽可讀事件, 回調函數為readQueryFromClient */if (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR){close(fd);zfree(c);return NULL;}}... }創(chuàng)建與文件描述符關聯(lián)的事件和上面相同,只是這里的回調函數變?yōu)閞eadQueryFromClient,因為這個連接不是為了接收客戶端連接請求,而是用于接收客戶端的輸入命令。
處理客戶端輸入的命令
當客戶端輸入命令后,會執(zhí)行相應的回調函數readQueryFromClient,該函數主要調用read函數從客戶端文件描述符中讀取輸入的命令
//networking.c /* 當客戶端可讀時的回調函數 */ void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {client *c = (client*) privdata;int nread, readlen;readlen = PROTO_IOBUF_LEN;/* 為緩沖區(qū)申請空間,用于保存客戶端命令 */c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);/* 從客戶端讀取命令,保存在c->querybuf中 */nread = read(fd, c->querybuf+qblen, readlen);.../* 處理客戶端命令 */processInputBuffer(c);processInputBuffer函數根據客戶端選項執(zhí)行不同的操作,最終調用processCommand函數處理命令,這個函數會先解析客戶端的命令關鍵字,判斷這個關鍵字是否合法。如果合法,再判斷參數個數是否合法
//server.c /* 解析客戶端命令,先判斷命令關鍵字是否合法,再判斷參數個數是否合法 */ int processCommand(client *c) {.../* 從命令字典中查找該命令名字,判斷是否存在該命令 *//* 將命令保存在cmd中,其中包括命令對應的處理函數 */c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);...if(...){...}else{//調用命令處理函數 call(c,CMD_CALL_FULL);...} }可以看到這個函數主要是解析命令關鍵字,從底層字典中查找是否有這個關鍵字,如果有,會連同命令對應的處理函數一起返回賦值給cmd變量中,cmd變量是struct redisCommand *類型,稍后可以看到用處
call函數的定義如下
//server.c /* 調用命令處理函數 */ void call(client *c, int flags) {.../* 調用命令回調函數 */c->cmd->proc(c);... }命令結構
Redis內部已經將每個命令以及其對應的處理函數包裝好,這個結構就是struct redisCommand,其中兩個比較重要的成員變量為
//server.h struct redisCommand {char *name; //命令關鍵字redisCommandProc *proc;//處理函數... };到這里可以猜到,在processCommand函數中調用lookupCommand函數時,會查找是否有相應命令關鍵字的結構,如果有,則返回到cmd變量中。現(xiàn)在可以看一下Redis內部是如果包裝每一個命令的了
struct redisCommand redisCommandTable[] = {{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},{"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},{"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},... };原來Redis已經為每個命令設計好了struct redisCommand結構,但是查找是否有命令關鍵字卻不是直接從這個“超大”的數組中一個個找,那樣太慢了。Redis內部是將每個命令關鍵字和它對應的struct redisCommand結構記錄在一個字典中,由于字典(底層由哈希表實現(xiàn))的查找效率是O(1),所以不會造成性能瓶頸
獲取到相應的命令結構后,同樣也獲取了命令處理函數,對于get命令而言是getCommand,對于set命令而言是setCommand,大概就是命令關鍵字后加Command是對應的命令處理函數
所以上述c->cmd->proc(c)函數調用時便直接調用了相應的處理函數,處理完成后,將反饋發(fā)送給客戶端,完成一次交互
小結
服務器與客戶端的交互實際上還是基于網絡請求的,服務器監(jiān)聽客戶端請求,客戶端請求連接,當連接建立成功后服務器會開始監(jiān)聽客戶端的文件描述符(套接字),一旦客戶端輸入命令,服務器便讀取文件描述符獲得客戶端的輸入,然后解析,執(zhí)行處理函數,將結構反饋給客戶端,客戶端將結構顯示在終端,完成一次交互
總結
以上是生活随笔為你收集整理的Redis源码剖析(一)服务器与客户端交互流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每天一道LeetCode-----以字符
- 下一篇: 每天一道LeetCode-----将二叉