redis协议的解析

redis客户端向服务器发送命令时, redis服务器都需要对命令进行解析,然后调用对应的命令处理函数进行处理.需要说明的是redis的任何协议命令均以\r\n结束.

在之前客户端的连接流程中介绍中, redis服务器会为新连接的客户端创建一个文件事件对象,并监听其可读状态,该文件事件对象的触发回调函数为readQueryFromClient,即假设客户端已经通过如下telnet命令连接到了redis服务器:

telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.

接着在控制终端继续输入redis set命令(这里以set命令为例来触发客户端的连接):

set key 3

redis服务器的select系统调用将返回,并导致函数readQueryFromClient被调用,该函数是读取客户端命令输入的通用函数,其实现为(redis.c):

832 static void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
833     redisClient *c = (redisClient*) privdata;
834     char buf[REDIS_QUERYBUF_LEN];
835     int nread;
836     REDIS_NOTUSED(el);
837     REDIS_NOTUSED(mask);
838 
839     nread = read(fd, buf, REDIS_QUERYBUF_LEN);
840     if (nread == -1) {
841         if (errno == EAGAIN) {
842             nread = 0;
843         } else {
844             redisLog(REDIS_DEBUG, "Reading from client: %s",strerror(errno));
845             freeClient(c);
846             return;
847         }
848     } else if (nread == 0) {
849         redisLog(REDIS_DEBUG, "Client closed connection");
850         freeClient(c);
851         return;
852     }
853     if (nread) {
854         c->querybuf = sdscatlen(c->querybuf, buf, nread);
855         c->lastinteraction = time(NULL);
856     } else {
857         return;
858     }
859 
860 again:
861     if (c->bulklen == -1) {
862         /* Read the first line of the query */
863         char *p = strchr(c->querybuf,'\n');
864         size_t querylen;
865         if (p) {
866             sds query, *argv;
867             int argc, j;
868 
869             query = c->querybuf;
870             c->querybuf = sdsempty();
871             querylen = 1+(p-(query));
872             if (sdslen(query) > querylen) {
873                 /* leave data after the first line of the query in the buffer */
874                 c->querybuf = sdscatlen(c->querybuf,query+querylen,sdslen(query)-querylen);
875             }
876             *p = '\0'; /* remove "\n" */
877             if (*(p-1) == '\r') *(p-1) = '\0'; /* and "\r" if any */
878             sdsupdatelen(query);
879 
880             /* Now we can split the query in arguments */
881             if (sdslen(query) == 0) {
882                 /* Ignore empty query */
883                 sdsfree(query);
884                 return;
885             }
886             argv = sdssplitlen(query,sdslen(query)," ",1,&argc);
887             sdsfree(query);
888             if (argv == NULL) oom("Splitting query in token");
889             for (j = 0; j < argc && j < REDIS_MAX_ARGS; j++) {
890                 if (sdslen(argv[j])) {
891                     c->argv[c->argc] = argv[j];
892                     c->argc++;
893                 } else {
894                     sdsfree(argv[j]);
895                 }
896             }
897             free(argv);
898             /* Execute the command. If the client is still valid
899              * after processCommand() return and there is something
900              * on the query buffer try to process the next command. */
901             if (processCommand(c) && sdslen(c->querybuf)) goto again;
902             return;
903         } else if (sdslen(c->querybuf) >= 1024) {
904             redisLog(REDIS_DEBUG, "Client protocol error");
905             freeClient(c);
906             return;
907         }
908     } else {
909         /* Bulk read handling. Note that if we are at this point
910            the client already sent a command terminated with a newline,
911            we are reading the bulk data that is actually the last
912            argument of the command. */
913         int qbl = sdslen(c->querybuf);
914 
915         if (c->bulklen <= qbl) {
916             /* Copy everything but the final CRLF as final argument */
917             c->argv[c->argc] = sdsnewlen(c->querybuf,c->bulklen-2);
918             c->argc++;
919             c->querybuf = sdsrange(c->querybuf,c->bulklen,-1);
920             processCommand(c);
921             return;
922         }
923     }
924 }

Line839通过read系统调用读取客户端输入,读取操作将是非阻塞的,如果没有错误发生,Line854将读取到的数据保存在客户端对象的字段querybuf中, 共后续分析命令及其参数使用.函数sdscatlen是操作sds系列函数簇之一,其逻辑是把字节追加到指定的sds之中,如果指定的sds空间不够,则自动调整其容量,最后该函数返回存储了追加数据的sds对象.函数sdscatlen 的实现为(sds.c):

114 sds sdscatlen(sds s, void *t, size_t len) {
115     struct sdshdr *sh;
116     size_t curlen = sdslen(s);
117 
118     s = sdsMakeRoomFor(s,len);
119     if (s == NULL) return NULL;
120     sh = (void*) (s-(sizeof(struct sdshdr)));
121     memcpy(s+curlen, t, len);
122     sh->len = curlen+len;
123     sh->free = sh->free-len;
124     s[curlen+len] = '\0';
125     return s;
126 }

Line118为sds对象分配新的空间来存储追加的数据, 如果sds对象内的空闲空间不能存储要追加的数据,该函数将分配新空间,如果sds对象内的空闲空间可以存储要追加的数据,则不需要分配新空间.函数sdsMakeRoomFor的实现为(sds.c):

 94 static sds sdsMakeRoomFor(sds s, size_t addlen) {
 95     struct sdshdr *sh, *newsh;
 96     size_t free = sdsavail(s);
 97     size_t len, newlen;
 98 
 99     if (free >= addlen) return s;
100     len = sdslen(s);
101     sh = (void*) (s-(sizeof(struct sdshdr)));
102     newlen = (len+addlen)*2;
103     newsh = realloc(sh, sizeof(struct sdshdr)+newlen+1);
104 #ifdef SDS_ABORT_ON_OOM
105     if (newsh == NULL) sdsOomAbort();
106 #else
107     if (newsh == NULL) return NULL;
108 #endif
109 
110     newsh->free = newlen - len;
111     return newsh->buf;
112 }

Line96获得sds对象的空闲空间大小,Line99比较其空闲空间和要追加的数据量的大小,如果空闲空间能存储要追加的数据,则直接返回.Line100:103动态分配新空间,使新空间将存储之前sds对象已存储的数据,又存储要追加的数据,除此之前新空间还预留了空间.Line103库函数realloc将把sds对象已有数据拷贝到新分配空间,并释放之前的sds对象,注意这里的拷贝将包括sds的元数据,即包括sdshdr对象信息.Line110调整空闲空间的大小,即新分配的大小减去之前sds对象中已有数据的长度.Line111返回sds对象,sds对象其实就是指向类型sdshdr对象的buf字段.

回到函数sdscatlen,Line121:123将要追加的数据存储在新的sds对象的空闲空间,并调整sds对象的存储的数据长度和空闲空间的大小.回到函数readQueryFromClient,Line854字段querybuf将指向存储了客户端命令及其参数数据的sds对象.Line861判断,对于非bulk类型的redis命令或者bulk型的命令的首次命令读取, 客户端对象的bulklen字段的值为-1.Line862:907解析读取到字段querybuf中的redis命令及其参数.Line863定位到新行字节’\n’,如果找到了新行字节说明收入数据中肯定包含了redis命令字符串,该命令的参数可能输入完整,也可能没有输入完整.具体而言,对于bulk类型的redis命令,存在参数没有输入完整的可能性,对于非bulk类型的redis命令,通常已经是完整的输入.如果输入中没有定位到新行字节,说明redis命令字符窜都没有输入完整,需要返回主事件循环, 继续调用select监听. Line869:870类型sds的对象query指向客户端对象的字段querybuf,同时将该字段指向空的sds对象.Line871计算sds对象保存的数据的起始位置到新行字节的数据长度. Line872:875的条件分支得到满足,则说明新行字节后面还有数据,Line874的含义是将客户端对象的字段querybuf指向新行字节后面的数据.Line876:877类型为char *的局部变量p指向新行字节开始的数据, 将新行字节替换为0, 如果新行字节前面是回车字节,也替换为0.因为局部sds对象query指向的数据的’\r’和’\n’被替换为0, Line878调用函数sdsupdatelen调整其长度,其实现为(sds.c):

 87 void sdsupdatelen(sds s) {
 88     struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
 89     int reallen = strlen(s);
 90     sh->free += (sh->len-reallen);
 91     sh->len = reallen;
 92 }

从实现可以看出,该函数只能正确处理以’\0’结束的C语言的字符窜.
回到函数readQueryFromClient, Line880:885如果经过长度调整以后,类型sds的局部对象query数据内容为空,返回主事件循环,继续调用select监听.Line886调用函数sdssplitlen使用空格分割字符分割输入中的redis命令及其参数,该函数的实现为(sds.c):

266 sds *sdssplitlen(char *s, int len, char *sep, int seplen, int *count) {
267     int elements = 0, slots = 5, start = 0, j;
268 
269     sds *tokens = malloc(sizeof(sds)*slots);
270 #ifdef SDS_ABORT_ON_OOM
271     if (tokens == NULL) sdsOomAbort();
272 #endif
273     if (seplen < 1 || len < 0 || tokens == NULL) return NULL;
274     for (j = 0; j < (len-(seplen-1)); j++) {
275         /* make sure there is room for the next element and the final one */
276         if (slots < elements+2) {
277             slots *= 2;
278             sds *newtokens = realloc(tokens,sizeof(sds)*slots);
279             if (newtokens == NULL) {
280 #ifdef SDS_ABORT_ON_OOM
281                 sdsOomAbort();
282 #else
283                 goto cleanup;
284 #endif
285             }
286             tokens = newtokens;
287         }
288         /* search the separator */
289         if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {
290             tokens[elements] = sdsnewlen(s+start,j-start);
291             if (tokens[elements] == NULL) {
292 #ifdef SDS_ABORT_ON_OOM
293                 sdsOomAbort();
294 #else
295                 goto cleanup;
296 #endif
297             }
298             elements++;
299             start = j+seplen;
300             j = j+seplen-1; /* skip the separator */
301         }
302     }
303     /* Add the final element. We are sure there is room in the tokens array. */
304     tokens[elements] = sdsnewlen(s+start,len-start);
305     if (tokens[elements] == NULL) {
306 #ifdef SDS_ABORT_ON_OOM
307                 sdsOomAbort();
308 #else
309                 goto cleanup;
310 #endif
311     }
312     elements++;
313     *count = elements;
314     return tokens;
315 
316 #ifndef SDS_ABORT_ON_OOM
317 cleanup:
318     {
319         int i;
320         for (i = 0; i < elements; i++) sdsfree(tokens[i]);
321         free(tokens);
322         return NULL;
323     }
324 #endif
325 }

该函数的基本思想是,Line269先预先分配一个类型sds的数组,用来指向redis命令及其参数,预分配的数组大小为5,因为大部分redis命令及其参数都小于这个数目.如果个别redis命令的参数比较多,Line278动态的调整这个数组大小.Line304数组中的sds对象指向调用函数sdsnewlen新创建的sds对象,这些新创建的sds对象存储redis命令及其参数.

回到函数readQueryFromClient, 执行完Lline886以后, 局部变量argc保存分割的参数的数量(包括redis命令),局部对象argv指向分割的sds对象,这些对象是动态分配的,因此,Line887调用函数sdsfree可以释放原始的sds对象了.Line889:897将局部对象argv中指向的sds对象记录在客户端对象的字段argv中,参数个数记录到argc字段中,完成之后释放局部管理数组argv.至此,本次调用read系统调用读取的数据中,新行字节以前的数据解析已经结束,其中肯定是包含了redis命令, 其参数可能是完整的,也可能不是完整的.Line901调用函数processCommand准备处理具体的redis命令,如果该函数返回的时候,客户端对象的字段querybuf里面仍然有数据,则跳转到Line860处继续刚才的解析处理.Lline909:922客户端对象的bulklen的值不是-1, 只可能是bulk类型的redis命令, 并且之前的读取到了redis命令及其部分参数值,Line913判断客户端对象接收缓存中的数据长度,(可能是本次读取的数据长度,也可能是之前读取的数据和本次读取的数据),Lline915:922如果客户端缓存对象中累计读取的长度满足bulk类型字节数据要求,则继续更新客户端对象的argv和argc,并调用函数processCommand处理,此时传给该函数的客户端对象已经准备完毕redis命令及其参数.

函数processCommand的实现为(redis.c):

774 /* If this function gets called we already read a whole
775  * command, argments are in the client argv/argc fields.
776  * processCommand() execute the command or prepare the
777  * server for a bulk read from the client.
778  *
779  * If 1 is returned the client is still alive and valid and
780  * and other operations can be performed by the caller. Otherwise
781  * if 0 is returned the client was destroied (i.e. after QUIT). */
782 static int processCommand(redisClient *c) {
783     struct redisCommand *cmd;
784 
785     sdstolower(c->argv[0]);
786     /* The QUIT command is handled as a special case. Normal command
787      * procs are unable to close the client connection safely */
788     if (!strcmp(c->argv[0],"quit")) {
789         freeClient(c);
790         return 0;
791     }
792     cmd = lookupCommand(c->argv[0]);
793     if (!cmd) {
794         addReplySds(c,sdsnew("-ERR unknown command\r\n"));
795         resetClient(c);
796         return 1;
797     } else if ((cmd->arity > 0 && cmd->arity != c->argc) ||
798                (c->argc < -cmd->arity)) {
799         addReplySds(c,sdsnew("-ERR wrong number of arguments\r\n"));
800         resetClient(c);
801         return 1;
802     } else if (cmd->type == REDIS_CMD_BULK && c->bulklen == -1) {
803         int bulklen = atoi(c->argv[c->argc-1]);
804 
805         sdsfree(c->argv[c->argc-1]);
806         if (bulklen < 0 || bulklen > 1024*1024*1024) {
807             c->argc--;
808             c->argv[c->argc] = NULL;
809             addReplySds(c,sdsnew("-ERR invalid bulk write count\r\n"));
810             resetClient(c);
811             return 1;
812         }
813         c->argv[c->argc-1] = NULL;
814         c->argc--;
815         c->bulklen = bulklen+2; /* add two bytes for CR+LF */
816         /* It is possible that the bulk read is already in the
817          * buffer. Check this condition and handle it accordingly */
818         if ((signed)sdslen(c->querybuf) >= c->bulklen) {
819             c->argv[c->argc] = sdsnewlen(c->querybuf,c->bulklen-2);
820             c->argc++;
821             c->querybuf = sdsrange(c->querybuf,c->bulklen,-1);
822         } else {
823             return 1;
824         }
825     }
826     /* Exec the command */
827     cmd->proc(c);
828     resetClient(c);
829     return 1;
830 }

Line785将redis命令转换为小写格式, 所以客户端输入的redis命令字符窜可以是大写格式也可以是小写格式, 客户端对象的字段argv[0]指向的sds保存redis命令字符串,argv[1],argv[2]等依次是其命令参数.Line788:791如果是客户端发送了quit命令主动结束连接,则调用函数freeClient释放客户端相关的所有资源,并关闭TCP连接,其实现为(redis.c):

701 static void freeClient(redisClient *c) {
702     listNode *ln;
703 
704     aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
705     aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
706     sdsfree(c->querybuf);
707     listRelease(c->reply);
708     freeClientArgv(c);
709     close(c->fd);
710     ln = listSearchKey(server.clients,c);
711     assert(ln != NULL);
712     listDelNode(server.clients,ln);
713     free(c);
714 }

调用函数aeDeleteFileEvent释放该客户端对象相关的文件事件对象.释放字段querybuf指向的输入接收缓存区.调用函数listRelease释放客户端对象的响应列表.调用函数freeClientArgv释放客户端对象的redis命令及其参数.系统调用close关闭TCP连接.Line710:712将该客户端对象从全局链表中删除.因为客户端对象都是在堆上动态分配的,Line713释放其内存.

回到函数processCommand, Line792查找命令字符串对应的处理函数,函数lookupCommand的实现为(redis.c):

759 static struct redisCommand *lookupCommand(char *name) {
760     int j = 0;
761     while(cmdTable[j].name != NULL) {
762         if (!strcmp(name,cmdTable[j].name)) return &cmdTable[j];
763         j++;
764     }
765     return NULL;
766 }

通过比较命令字符串,如果找到匹配的,则返回该命令处理函数.全局对象cmdTable定义为(redis.c):

180 static struct redisCommand cmdTable[] = {
181     {"get",getCommand,2,REDIS_CMD_INLINE},
182     {"set",setCommand,3,REDIS_CMD_BULK},
183     {"setnx",setnxCommand,3,REDIS_CMD_BULK},
184     {"del",delCommand,2,REDIS_CMD_INLINE},
185     {"exists",existsCommand,2,REDIS_CMD_INLINE},
186     {"incr",incrCommand,2,REDIS_CMD_INLINE},
187     {"decr",decrCommand,2,REDIS_CMD_INLINE},
188     {"rpush",rpushCommand,3,REDIS_CMD_BULK},
189     {"lpush",lpushCommand,3,REDIS_CMD_BULK},
190     {"rpop",rpopCommand,2,REDIS_CMD_INLINE},
191     {"lpop",lpopCommand,2,REDIS_CMD_INLINE},
192     {"llen",llenCommand,2,REDIS_CMD_INLINE},
193     {"lindex",lindexCommand,3,REDIS_CMD_INLINE},
194     {"lset",lsetCommand,4,REDIS_CMD_BULK},
195     {"lrange",lrangeCommand,4,REDIS_CMD_INLINE},
196     {"ltrim",ltrimCommand,4,REDIS_CMD_INLINE},
197     {"sadd",saddCommand,3,REDIS_CMD_BULK},
198     {"srem",sremCommand,3,REDIS_CMD_BULK},
199     {"sismember",sismemberCommand,3,REDIS_CMD_BULK},
200     {"scard",scardCommand,2,REDIS_CMD_INLINE},
201     {"sinter",sinterCommand,-2,REDIS_CMD_INLINE},
202     {"smembers",sinterCommand,2,REDIS_CMD_INLINE},
203     {"randomkey",randomkeyCommand,1,REDIS_CMD_INLINE},
204     {"select",selectCommand,2,REDIS_CMD_INLINE},
205     {"move",moveCommand,3,REDIS_CMD_INLINE},
206     {"rename",renameCommand,3,REDIS_CMD_INLINE},
207     {"renamenx",renamenxCommand,3,REDIS_CMD_INLINE},
208     {"keys",keysCommand,2,REDIS_CMD_INLINE},
209     {"dbsize",dbsizeCommand,1,REDIS_CMD_INLINE},
210     {"ping",pingCommand,1,REDIS_CMD_INLINE},
211     {"echo",echoCommand,2,REDIS_CMD_BULK},
212     {"save",saveCommand,1,REDIS_CMD_INLINE},
213     {"bgsave",bgsaveCommand,1,REDIS_CMD_INLINE},
214     {"shutdown",shutdownCommand,1,REDIS_CMD_INLINE},
215     {"lastsave",lastsaveCommand,1,REDIS_CMD_INLINE},
216     {"type",typeCommand,2,REDIS_CMD_INLINE},
217     {"",NULL,0,0}
218 };

类型redisCommand的定义为(redis.c):

114 typedef void redisCommandProc(redisClient *c);
115 struct redisCommand {
116     char *name;
117     redisCommandProc *proc;
118     int arity;
119     int type;
120 };

字段name是redis命令字符串,字段proc是该命令的处理函数,字段arity是redis命令参数个数,包含命令本身,字段type说明该命令是否是bulk命令.

回到函数processCommand, Line793:802如果redis服务端不支持客户端的命令或者命令参数个数不正确,则调用函数resetClient复位该客户端对象,该函数的实现为(redis.c):

768 /* resetClient prepare the client to process the next command */
769 static void resetClient(redisClient *c) {
770     freeClientArgv(c);
771     c->bulklen = -1;
772 }

复位客户端对象会把解析的redis命令及其参数释放,并赋值字段bulklen为初始值,该函数不会断开与客户端的连接.

回到函数processCommand, Line802如果该命令是bulk类型,并且是首次解析其参数,即字段bulklen为-1,那么解析的最后一个参数必定为value字节数据的长度,Line803将该字符串参数转换为整数,转换完成后,Line805就可以释放掉该sds对象.Line806:812可以看出,value字节数最大支持1GB.Line815设置接下来需要继续读取的value的字节数,包括以\r\n结束的2个字节.Line816:822如果客户端对象接收缓存中已经存在要读取的字节,则创建新的sds对象存储value字节,注意value字节是不包含\r\n结束字节的,同时客户端对象记录该sds对象,至此该redis命令的所有参数都已经准备完毕,Line827调用该命令的处理函数,处理完成后,调用resetClient复位客户端对象,释放动态sds对象.如果Line818的条件判断不满足,说明参数输入不完整,需要读取参数,则返回到主事件循环,继续监听文件事件对象,等待客户端输入.如果Line802的条件判断不满足,说明该命令或者是非bulk类型命令,或者是bulk命令,且其所有参数都已经记录在客户端对象中,这时候直接调用Line827命令处理函数.

你可能感兴趣的:(Redis)