Redis 通讯协议(RESP)
Redis 基于 RESP (Redis Serialization Protocal)协议来完成客户端和服务端通讯。RESP 本质是一种文本协议,实现简单、易于解析。 特征和描述如下表所示:
类型 | 协议描述 | |
网络层 | 客户端和服务端通过 tcp/ 流式套接字来进行通讯,为了 防止粘包 因此命令或数据均以 \r\n (CRLF) 结尾 | |
请求 | *<参数数量>CR LF <参数1的字节数量>CRLF<br/><参数1的数据>CRLF<br/>...<br><参数 1 的字节数量 >CR LF<br/><参数 1 的数据> CR LF<br/>... <br><参数1的字节数量>CRLF<br/><参数1的数据>CRLF<br/>...<br><参数 N 的字节数量 >CR LF <参数 N 的数据> CR LF |
*2\r\n3\nget\r\n$13\r\nusername:1234\r\n。见 callSendCommond -> redis AppendConnadnArgv -> redisFromatCommandArgv |
简单字符串回复 | 第一个字节+ | +ok\r\n |
错误回复 | 第一个字节- | -ERR unknown command 'sa' \r\n |
整数回复 | 第一个字节: | :0\r\n |
批量回复 | 第一个字节$ | 6\r\nfoobar\r\n,空回复 -1 |
多条批量回复 | 第一个字节* | *5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n, 空回复 *0\r\n |
特别说明:如果客户端和服务端在一台机器上。那么会对通讯协议进行优化,直接走本地回环
我们以通过 tcpdump
工具来帮助我们进行网络抓包。
# linux tcpdump -i lo part 6379 -Ann # mac tcpdump -i lo0 port 6379 -Ann
测试一下 ( 我本机是 mac 环境):
# 客户端 A 127.0.0.1:6379> set msg100 1 OK
服务端抓包结果,如下:
➜ ~ sudo tcpdump -i lo0 port 6379 -Ann Password: tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes 21:52:53.447885 IP 127.0.0.1.51645 > 127.0.0.1.6379: Flags [P.], seq 1564111974:1564112006, ack 169183468, win 6272, options [nop,nop,TS val 774447713 ecr 772455554], length 32: RESP "set" "msg100" "1" E..T..@.@...............]:tf ........H..... .)"a. ..*3 $3 set $6 msg100 $1 1 21:52:53.447912 IP 127.0.0.1.6379 > 127.0.0.1.51645: Flags [.], ack 32, win 6376, options [nop,nop,TS val 774447713 ecr 774447713], length 0 E..4..@.@............... ...]:t......(..... .)"a.)"a 21:52:53.528935 IP 127.0.0.1.6379 > 127.0.0.1.51645: Flags [P.], seq 1:6, ack 32, win 6376, options [nop,nop,TS val 774447793 ecr 774447713], length 5: RESP "OK" E..9..@.@............... ...]:t......-..... .)"..)"a+OK 21:52:53.528966 IP 127.0.0.1.51645 > 127.0.0.1.6379: Flags [.], ack 6, win 6272, options [nop,nop,TS val 774447793 ecr 774447793], length 0 E..4..@.@...............]:t. ........(..... .)"..)".
客户端效果
客户端是对显示结果做了转化,在 redis-cli.c/cliFormatReplyTTY
中
static sds cliFormatReplyTTY(redisReply *r, char *prefix) { sds out = sdsempty(); switch (r->type) { case REDIS_REPLY_ERROR: out = sdscatprintf(out,"(error) %s\n", r->str); break; case REDIS_REPLY_STATUS: out = sdscat(out,r->str); out = sdscat(out,"\n"); break; case REDIS_REPLY_INTEGER: out = sdscatprintf(out,"(integer) %lld\n",r->integer); break; case REDIS_REPLY_DOUBLE: out = sdscatprintf(out,"(double) %s\n",r->str); break; case REDIS_REPLY_STRING: case REDIS_REPLY_VERB: /* If you are producing output for the standard output we want * a more interesting output with quoted characters and so forth, * unless it's a verbatim string type. */ if (r->type == REDIS_REPLY_STRING) { out = sdscatrepr(out,r->str,r->len); out = sdscat(out,"\n"); } else { out = sdscatlen(out,r->str,r->len); out = sdscat(out,"\n"); } break; case REDIS_REPLY_NIL: out = sdscat(out,"(nil)\n"); break; case REDIS_REPLY_BOOL: out = sdscat(out,r->integer ? "(true)\n" : "(false)\n"); break; case REDIS_REPLY_ARRAY: case REDIS_REPLY_MAP: case REDIS_REPLY_SET: case REDIS_REPLY_PUSH: if (r->elements == 0) { if (r->type == REDIS_REPLY_ARRAY) out = sdscat(out,"(empty array)\n"); else if (r->type == REDIS_REPLY_MAP) out = sdscat(out,"(empty hash)\n"); else if (r->type == REDIS_REPLY_SET) out = sdscat(out,"(empty set)\n"); else if (r->type == REDIS_REPLY_PUSH) out = sdscat(out,"(empty push)\n"); else out = sdscat(out,"(empty aggregate type)\n"); } else { unsigned int i, idxlen = 0; char _prefixlen[16]; char _prefixfmt[16]; sds _prefix; sds tmp; /* Calculate chars needed to represent the largest index */ i = r->elements; if (r->type == REDIS_REPLY_MAP) i /= 2; do { idxlen++; i /= 10; } while(i); /* Prefix for nested multi bulks should grow with idxlen+2 spaces */ memset(_prefixlen,' ',idxlen+2); _prefixlen[idxlen+2] = '\0'; _prefix = sdscat(sdsnew(prefix),_prefixlen); /* Setup prefix format for every entry */ char numsep; if (r->type == REDIS_REPLY_SET) numsep = '~'; else if (r->type == REDIS_REPLY_MAP) numsep = '#'; else numsep = ')'; snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%ud%c ",idxlen,numsep); for (i = 0; i < r->elements; i++) { unsigned int human_idx = (r->type == REDIS_REPLY_MAP) ? i/2 : i; human_idx++; /* Make it 1-based. */ /* Don't use the prefix for the first element, as the parent * caller already prepended the index number. */ out = sdscatprintf(out,_prefixfmt,i == 0 ? "" : prefix,human_idx); /* Format the multi bulk entry */ tmp = cliFormatReplyTTY(r->element[i],_prefix); out = sdscatlen(out,tmp,sdslen(tmp)); sdsfree(tmp); /* For maps, format the value as well. */ if (r->type == REDIS_REPLY_MAP) { i++; sdsrange(out,0,-2); out = sdscat(out," => "); tmp = cliFormatReplyTTY(r->element[i],_prefix); out = sdscatlen(out,tmp,sdslen(tmp)); sdsfree(tmp); } } sdsfree(_prefix); } break; default: fprintf(stderr,"Unknown reply type: %d\n", r->type); exit(1); } return out; }
我们也可以使用 nc 命令来替代 redis-cli 命令行:
➜ ~ sudo nc 127.0.0.1 6379 set a a +OK get a $1 a
其他说明
- Redis 常见的错误 (
src/redis-cli.c
)
#define REDIS_ERR -1 #define REDIS_OK 0 /* When an error occurs, the err flag in a context is set to hold the type of * error that occurred. REDIS_ERR_IO means there was an I/O error and you * should use the "errno" variable to find out what is wrong. * For other values, the "errstr" field will hold a description. */ #define REDIS_ERR_IO 1 /* Error in read or write */ #define REDIS_ERR_EOF 3 /* End of file */ #define REDIS_ERR_PROTOCOL 4 /* Protocol error */ #define REDIS_ERR_OOM 5 /* Out of memory */ #define REDIS_ERR_TIMEOUT 6 /* Timed out */ #define REDIS_ERR_OTHER 2 /* Everything else... */ #define REDIS_REPLY_STRING 1 #define REDIS_REPLY_ARRAY 2 #define REDIS_REPLY_INTEGER 3 #define REDIS_REPLY_NIL 4 #define REDIS_REPLY_STATUS 5 #define REDIS_REPLY_ERROR 6 #define REDIS_REPLY_DOUBLE 7 #define REDIS_REPLY_BOOL 8 #define REDIS_REPLY_MAP 9 #define REDIS_REPLY_SET 10 #define REDIS_REPLY_ATTR 11 #define REDIS_REPLY_PUSH 12 #define REDIS_REPLY_BIGNUM 13 #define REDIS_REPLY_VERB 14 /* Default max unused reader buffer. */ #define REDIS_READER_MAX_BUF (1024*16) /* Default multi-bulk element limit */ #define REDIS_READER_MAX_ARRAY_ELEMENTS ((1LL<<32) - 1)
Redis 命令对象
redis 命令是使用的是 redisCommand 数据结构来管理的。
数据结构
typedef void redisCommandProc(client *c); // 函数指针类型,指向命令实现函数 typedef int redisGetKeysProc(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); struct redisCommand { char *name; redisCommandProc *proc; // 限制命令的个数。-N 表示至少 N 个参数,包含命令本身 int arity; // 字符串方式设置命令的属性之间运用 | 运算,程序内部自动解析,函数 populateCommandTable char *sflags; /* Flags as string representation, one char per flag. */ // 将 flags 字符串类型转换成整数,多个属性 uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */ /* Use a function to determine keys arguments in a command line. * Used for Redis Cluster redirect. */ redisGetKeysProc *getkeys_proc; /* What keys should be loaded in background when calling this command? */ int firstkey; /* The first argument that's a key (0 = no keys) */ int lastkey; /* The last argument that's a key */ int keystep; /* The step between first and last key */ long long microseconds, calls, rejected_calls, failed_calls; int id; /* Command ID. This is a progressive ID starting from 0 that is assigned at runtime, and is used in order to check ACLs. A connection is able to execute a given command if the user associated to the connection has this command bit set in the bitmap of allowed commands. */ };
针对 sflag 标示,这里可以看看 《redis 设计与实现》
flag 记录的是 flag 值与 sflag 进行运算的结果,见 populateCommandTable 函数 (src/server.c/populateCommandTable
)
for (int j = 0; j < argc; j++) { char *flag = argv[j]; if (!strcasecmp(flag,"write")) { c->flags |= CMD_WRITE|CMD_CATEGORY_WRITE; } else if (!strcasecmp(flag,"read-only")) { c->flags |= CMD_READONLY|CMD_CATEGORY_READ; } else if (!strcasecmp(flag,"use-memory")) { c->flags |= CMD_DENYOOM; } else if (!strcasecmp(flag,"admin")) { c->flags |= CMD_ADMIN|CMD_CATEGORY_ADMIN|CMD_CATEGORY_DANGEROUS; } else if (!strcasecmp(flag,"pub-sub")) { c->flags |= CMD_PUBSUB|CMD_CATEGORY_PUBSUB; } else if (!strcasecmp(flag,"no-script")) { c->flags |= CMD_NOSCRIPT; } else if (!strcasecmp(flag,"random")) { c->flags |= CMD_RANDOM; } else if (!strcasecmp(flag,"to-sort")) { c->flags |= CMD_SORT_FOR_SCRIPT; } else if (!strcasecmp(flag,"ok-loading")) { c->flags |= CMD_LOADING; } else if (!strcasecmp(flag,"ok-stale")) { c->flags |= CMD_STALE; } else if (!strcasecmp(flag,"no-monitor")) { c->flags |= CMD_SKIP_MONITOR; } else if (!strcasecmp(flag,"no-slowlog")) { c->flags |= CMD_SKIP_SLOWLOG; } else if (!strcasecmp(flag,"cluster-asking")) { c->flags |= CMD_ASKING; } else if (!strcasecmp(flag,"fast")) { c->flags |= CMD_FAST | CMD_CATEGORY_FAST; } else if (!strcasecmp(flag,"no-auth")) { c->flags |= CMD_NO_AUTH; } else if (!strcasecmp(flag,"may-replicate")) { c->flags |= CMD_MAY_REPLICATE; } else { /* Parse ACL categories here if the flag name starts with @. */ uint64_t catflag; if (flag[0] == '@' && (catflag = ACLGetCommandCategoryFlagByName(flag+1)) != 0) { c->flags |= catflag; } else { sdsfreesplitres(argv,argc); return C_ERR; } } }
具体命令比较多
struct redisCommand redisCommandTable[] = { {"module",moduleCommand,-2, "admin no-script", 0,NULL,0,0,0,0,0,0}, {"get",getCommand,2, "read-only fast @string", 0,NULL,1,1,1,0,0,0}, // ..... }
以 set 为例子 {"set",setCommand,-3, "write use-memory @string", 0,NULL,1,1,1,0,0,0}
参考资料
- 《Redis 设计与实现》 黄健宏