Redis 通信协议-了解 Redis 客户端实现原理-新闻详情

Redis 通信协议-了解 Redis 客户端实现原理


发布时间:2017-06-27责任编辑:朱明 浏览:1088


几乎所有的主流编程语言都有Redis的客户端(http://redis.io/clients),不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:

1.   客户端与服务端之间的通信协议是在 TCP 协议之上构建的。

客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379

客户端和服务器发送的命令或数据一律以 CRLF)结尾。

2.      Redis制定了 RESPREdis Serialization ProtocolRedis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

发送命令

RESP Redis 1.2 版本中引入, 并最终在 Redis 2.0 版本成为 Redis 服务器通信的标准方式。

在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。

RESP 的规定一条命令的格式如下:

*<参数数量> CR LF

$<参数 1 的字节数量> CR LF

<参数 1 的数据> CR LF

...

$<参数 N 的字节数量> CR LF

<参数 N 的数据> CR LF

命令本身也作为协议的其中一个参数来发送。

例如我们经常执行的 SET 命令,在命令行中我们输入如下:

SET key value

使用 RESP 协议规定的格式:

*3

$3

SET

$3 # 这里 key 一共三个字节

key

$5 # 这里 value 一共五个字节

value

这个命令的实际协议值如下:

"*3 $3 SET $3 key $5 value "

回复

Redis 命令会返回多种不同类型的回复。

通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:

       状态回复(status reply)的第一个字节是 "+"

       错误回复(error reply)的第一个字节是 "-"

       整数回复(integer reply)的第一个字节是 ":"

       批量回复(bulk reply)的第一个字节是 "$"

       多条批量回复(multi bulk reply)的第一个字节是 "*"

我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就按照RESP进行结果解析的,所以看不到中间结果,redis-cli.c 源码对命令结果的解析结构如下:

static sds cliFormatReplyTTY(redisReply *r, char *prefix) {

    sds out = sdsempty();

switch (r->type) {

                 // 处理错误回复

case REDIS_REPLY_ERROR:

out = sdscatprintf(out,"(error) %s ", r->str);

break;

// 处理状态回复

case REDIS_REPLY_STATUS:

out = sdscat(out,r->str);

out = sdscat(out," ");

break;

// 处理整数回复

case REDIS_REPLY_INTEGER:

out = sdscatprintf(out,"(integer) %lld ",r->integer);

break;

// 处理字符串回复

case REDIS_REPLY_STRING:

/* If you are producing output for the standard output we want

* a more interesting output with quoted characters and so forth */

out = sdscatrepr(out,r->str,r->len);

out = sdscat(out," ");

break;

// 处理 nil

case REDIS_REPLY_NIL:

out = sdscat(out,"(nil) ");

break;

// 处理多回复

case REDIS_REPLY_ARRAY:

if (r->elements == 0) {

out = sdscat(out,"(empty list or set) ");

} 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;

do {

idxlen++;

i /= 10;

} while(i);

 

/* Prefix for nested multi bulks should grow with idxlen+2 spaces */

memset(_prefixlen,' ',idxlen+2);

_prefixlen[idxlen+2] = '';

_prefix = sdscat(sdsnew(prefix),_prefixlen);

 

/* Setup prefix format for every entry */

    snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%ud) ",idxlen);

 

for (i = 0; i < r->elements; i++) {

/* 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,i+1);

 

/* Format the multi bulk entry */

                         tmp = cliFormatReplyTTY(r->element[i],_prefix);

out = sdscatlen(out,tmp,sdslen(tmp));

sdsfree(tmp);

}

sdsfree(_prefix);

}

break;

default:

fprintf(stderr,"Unknown reply type: %d ", r->type);

exit(1);

}

return out;

}

发送命令 一节中使用的格式除了用作命令请求协议之外, 也用在命令的回复协议中: 这种只有一个参数的回复格式被称为批量回复(Bulk Reply)。

统一协议请求原本是用在回复协议中, 用于将列表的多个项返回给客户端的, 这种回复格式被称为多条批量回复(Multi Bulk Reply)。

一个多条批量回复以 *<argc> 为前缀, 后跟多条不同的批量回复, 其中 argc 为这些批量回复的数量。

状态回复

一个状态回复(或者单行回复,single line reply)是一段以 "+" 开始、 " " 结尾的单行字符串。

以下是一个状态回复的例子:

  +OK

客户端库应该返回 "+" 号之后的所有内容。 比如在在上面的这个例子中, 客户端就应该返回字符串 "OK"

状态回复通常由那些不需要返回数据的命令返回,这种回复不是二进制安全的,它也不能包含新行。

状态回复的额外开销非常少,只需要三个字节(开头的 "+" 和结尾的 CRLF)。

错误回复

错误回复和状态回复非常相似, 它们之间的唯一区别是, 错误回复的第一个字节是 "-" 而状态回复的第一个字节是 "+"

错误回复只在某些地方出现问题时发送: 比如说, 当用户对不正确的数据类型执行命令, 或者执行一个不存在的命令, 等等。

一个客户端库应该在收到错误回复时产生一个异常。

以下是两个错误回复的例子:

  -ERR unknown command 'foobar'

                        -WRONGTYPE Operation against a key holding the wrong kind of value

"-" 之后,直到遇到第一个空格或新行为止,这中间的内容表示所返回错误的类型。

ERR 是一个通用错误,而 WRONGTYPE 则是一个更特定的错误。 一个客户端实现可以为不同类型的错误产生不同类型的异常, 或者提供一种通用的方式, 让调用者可以通过提供字符串形式的错误名来捕捉(trap)不同的错误。

不过这些特性用得并不多, 所以并不是特别重要, 一个受限的(limited)客户端可以通过简单地返回一个逻辑假(false)来表示一个通用的错误条件。

整数回复

整数回复就是一个以 ":" 开头, CRLF 结尾的字符串表示的整数。

比如说, ":0 " ":1000 " 都是整数回复。

返回整数回复的其中两个命令是 INCR LASTSAVE 被返回的整数没有什么特殊的含义, INCR 返回键的一个自增后的整数值, LASTSAVE 则返回一个 UNIX 时间戳, 返回值的唯一限制是这些数必须能够用 64 位有符号整数表示。

整数回复也被广泛地用于表示逻辑真和逻辑假: 比如 EXISTS SISMEMBER 都用返回值 1 表示真, 0 表示假。

其他一些命令, 比如 SADD SREM SETNX 只在操作真正被执行了的时候, 才返回 1 否则返回 0

以下命令都返回整数回复: SETNX DEL EXISTS INCR INCRBY DECR DECRBY DBSIZE LASTSAVE RENAMENX MOVE LLEN SADD SREM SISMEMBER SCARD

批量回复

服务器使用批量回复来返回二进制安全的字符串,字符串的最大长度为 512 MB

客户端:GET mykey

服务器:foobar

服务器发送的内容中:

       第一字节为 "$" 符号

      - 接下来跟着的是表示实际回复长度的数字值

               - 之后跟着一个 CRLF

                        - 再后面跟着的是实际回复数据

                        - 最末尾是另一个 CRLF

对于前面的 GET 命令,服务器实际发送的内容为:

"$6foobar "

如果被请求的值不存在, 那么批量回复会将特殊值 -1 用作回复的长度值, 就像这样:

客户端:GET non-existing-key

服务器:$-1

这种回复称为空批量回复(NULL Bulk Reply)。

当请求对象不存在时,客户端应该返回空对象,而不是空字符串: 比如 Ruby 库应该返回 nil C 库应该返回NULL (或者在回复对象中设置一个特殊标志), 诸如此类。

多条批量回复

LRANGE 这样的命令需要返回多个值, 这一目标可以通过多条批量回复来完成。

多条批量回复是由多个回复组成的数组, 数组中的每个元素都可以是任意类型的回复, 包括多条批量回复本身。

多条批量回复的第一个字节为 "*" 后跟一个字符串表示的整数值, 这个值记录了多条批量回复所包含的回复数量, 再后面是一个 CRLF

客户端: LRANGE mylist 0 3

服务器: *4

服务器: $3

服务器: foo

服务器: $3

服务器: bar

服务器: $5

服务器: Hello

服务器: $5

服务器: World

在上面的示例中,服务器发送的所有字符串都由 CRLF 结尾。

正如你所见到的那样, 多条批量回复所使用的格式, 和客户端发送命令时使用的统一请求协议的格式一模一样。 它们之间的唯一区别是:

       统一请求协议只发送批量回复。

       而服务器应答命令时所发送的多条批量回复,则可以包含任意类型的回复。

以下例子展示了一个多条批量回复, 回复中包含四个整数值, 以及一个二进制安全字符串:

*5

:1

:2

:3

:4

$6

foobar

在回复的第一行, 服务器发送 *5 表示这个多条批量回复包含 5 条回复, 再后面跟着的则是 5 条回复的正文。

多条批量回复也可以是空白的(empty), 就像这样:

客户端: LRANGE nokey 0 1

服务器: *0

无内容的多条批量回复(null multi bulk reply)也是存在的, 比如当 BLPOP 命令的阻塞时间超过最大时限时, 它就返回一个无内容的多条批量回复, 这个回复的计数值为 -1

客户<