Redis协议(翻译)

原文:https://redis.io/topics/protocol

      Redis客户端通过使用一种叫 RESP (REdis Serialization Protocol, redis序列化协议) 协议与Redis服务器交互。虽然这个协议是为Redis而设计的,但它也可以用于其他 client-server 架构的软件系统。(译注: 从一些公开的资料来看,陌陌的IM协议设计就参考了Redis协议)

      RESP 权衡了以下几个方面:

  • 实现要简单
  • 解析要快
  • 方便人阅读

     RESP 可以序列化不同的数据类型,像 integers、strings、arrays,对于错误也设计了特殊的类型。客户端以字符串参数数组的请求形式发送给Redis服务器执行,Redis返回命令相关的数据类型。

    RESP是二进制安全(binary-safe)的,并且不需要解析由一个进程发送给另一个进程的bulk 数据,因为它使用长度前缀来传输bulk 数据。

    注意:这里所说的协议只用于client-server的通信。Redis Cluster使用不同的二进制协议在node间进行消息交互。

    

    网络层

    客户端通过建立端口为6379的TCP连接与Redis通信。

    虽然RESP从技术上来说并不是TCP相关的,但对Redis来说该协议只用于TCP(或者其他流式协议如Unix域协议)。 (译注:反观memcached, 既支持tcp, 也支持udp, 但实际上生产环境基本只用tcp。我认为这是一种过度设计了,搞不好还可能被骇客利用来做 memcached udp 反射攻击。。。)

    请求响应模型

    Redis 接收不同参数构成的命令。当命令接收到后就会被处理,然后响应发送给客户端。

    这是最简单的模型了,但有两点例外:

  • Redis支持 pipelining (后文会提及)。所以客户端可以一次发送多个命令,然后等待响应。
  • 当客户端订阅了一个 Pub/Sub channel, 该协议会改变语意而变成一个推送协议,也就是说客户端不用发送命令,因为服务端在收到消息后会自动给客户端发送新的消息(对客户端订阅了的channel)。

    除了这两点,Redis协议就是一个简单的 请求-响应 协议。

    RESP协议描述

    RESP协议在Redis 1.2引入,但它现在成为Redis 2.0的标准交互协议。你应该实现Redis客户端时采用该协议。

    RESP事实上是一个支持以下类型的序列化协议:Simple Strings, Errors, Integers, Bulk Strings 和 Arrays。

    RESP作为一种请求响应协议,在Redis中使用的方式如下:

  • 客户端发送命令到Redis服务器,以RESP Bulk Strings数组的方式。
  • 服务器根据不同的命令实现,返回相应的RESP实现。

     在RESP中,一些数据的类型由第一个字节确定:

  • 对于SImple Strings 响应的第一个字节是“+”
  • 对于Errors 响应的第一个字节是 "-"
  • 对于Integers 响应第一个字节是 ":"
  • 对于Bulk Strings 响应第一个字节是 "$" 
  • 对于Arrays 响应第一个字节是 "*" 

    另外RESP可以用特殊的Bulk String或数组来表示 Null 值,后文会提及。

    在RESP中协议不同部分总是用 "\r\n" (CRLF) 分隔。

 

    RESP Simple Strings

    Simple Strings通过以下方式编码:一个加号,后跟一个字符串, 字符串不包含CR或LF字符(不能有换行),以CRLF ("\r\n")结束。

    SImple Strings 以最小的代价来传输非二进制安全的字符串。例如很多Redis命令在成功时响应"OK",就是用RESP Simple String 来编码的5个字节:

"+OK\r\n"

 为了传输二进制安全的字符串,要用RESP Bulk Strings。

   当Redis响应一个Simple String时,客户端库应该给调用者返回从第一个"+"字符到字符串结尾的字符串,不包括CRLF 字节。

 

   RESP Errors

   RESP 对于error有一种特殊的数据类型。实际上error就像RESP Simple String一样,但第一个字符串是"-"而不是加号。在RESP中 Simple Strings和Errors两者的真正区别在于errors被客户端作为异常,构成Error类型的字符串就是字符串本身。基本格式是:

"-Error message\r\n"

   Error响应只会在有错误发生时才会发送,例如你操作了错误的数据类型,或者命令不存在等。当收到Error响应时,客户端应该抛出一个异常。

   以下是error响应的例子:   

-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

   在"-"到第一个空格或新行间的第一个词,表示返回的错误类型。这只是Redis自己的一种约定,并不是RESP Error 所规定的格式。

   例如,ERR 是通用错误,而 WRONGTYPE 是一种更加具体的错误,表示客户端尝试操作错误的数据类型。这称为 Error Prefix (Error前缀),客户端可从此得知服务器返回错误的类型而不需依赖于那个确切的消息描述,后者会随时改变。

   一个客户端的实现可能对不同的error返回不同类型的异常,或者向调用者返回代表错误的字符串。然而这种特性并不是必须的,因为这并没什么卵用,一些精简的客户端实现可能简单的返回一般的错误情况,例如 false。

 

   RESP Integers 

   这种类型就是一个代表整数的以CRLF结尾的字符串,并以“:”字节开头。例如 ":0\r\n", 或 ":1000\r\n" 都是整数响应。

   很多Redis命令返回RESP Integers, 像 INCR, LLEN 和 LASTSAVE。

   返回的整数并没什么特殊的含义,它就是 INCR 增加后的数字,LASTSAVE 的UNIX时间戳等。但返回的整数可以保证是在64位有符号整数的范围内。

   整数响应也被大量的用于表示true或false。例如EXISTS和 SISMEMBER 等命令会返回1表示true, 0表示false。

   以下命令会返回一个整数: SETNX, DEL, EXISTS, INCR, INCRBY, DECR, DECRBY, DBSIZE, LASTSAVE, RENAMENX, MOVE, LLEN, SADD, SREM, SISMEMBER, SCARD。

 

   RESP Bulk Strings

   Bulk Strings 用于表示一个二进制安全的字符串,最大长度为512M。

   Bulk Strings 的编码格式如下:

  • “$” 后跟字符串字节数(prefix length),以CRLF结束
  • 实际的字符串
  • CRLF结束

  所以字符串"foobar" 被编码成:

"$6\r\nfoobar\r\n"

  空字符串:

"$0\r\n\r\n"

  RESP Bulk String 也可以用一种代表Null值的特殊格式来表示不存在的值。这种特殊格式的长度值为-1, 并且没数据,所以Null表示为:

"$-1\r\n"

 这称为 Null Bulk String。

 当服务器返回Null Bulk String时,客户端API不应该返回空串,而是nil 对象。例如Ruby库应该返回 'nil' 而 C 库应该返回 NULL (或在返回的对象设置特殊的标记),等等。

 

 RESP Arrays

      客户端用RESP Arrays向Redis服务器发送命令。同样某些Redis命令要返回一个元素集合时也使用RESP Arrays作为返回的类型。一个例子是LRANGE 命令返回一个元素列表。

      RESP Arrays使用以下格式发送:

  • “*” 为第一个字节,后跟数组的元素个数,然后CRLF。
  • 然后是数组中的每一个RESP类型表示的元素。

     例如一个空数组表示为:

"*0\r\n"

     而有两个RESP Bulk Strings "foo" 和 "bar" 的数组编码为:

"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

    正如你所见,在数组前面的 *<count>CRLF 后,数组中的其他的数据类型一个接一个的连接在一起。例如一个有三个整数的数组编码如下:

"*3\r\n:1\r\n:2\r\n:3\r\n"

    数组可以包含混合类型,它不要求所有的元素都是相同的类型。例如,一个有4个interges和1个bulk string的数组可以编码为:

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

(为清晰起见响应被分为多行)。

服务器发送的第一行 *5\r\n 表示后跟有5个响应,然后每个代表元素的响应被发送。

Null 数组的概念同样存在,它是Null值的替代方式 (通常使用Null Bulk String,但由于历史原因我们有两种格式)。

例如当BLPOP命令超时,它返回一个长度为-1的Null 数组,如下所示:

"*-1\r\n"

在服务端返回Null数组时,客户端库API应该返回null对象而不是空数组。区分返回空的列表与其他的情况(如BLPOP命令超时的情况)是有必要的。

RESP允许数组的数组。例如一个含两个数组的数组编码如下:

*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Foo\r\n
-Bar\r\n

(为方便阅读以上格式分割成了多行)

 上面的RESP数据类型编码了有两个元素的数组,一个数组包含三个整数1,2,3,另一个数组包含一个Simple String和一个Error.

 

 数组中的Null元素

     数组中的元素可以为Null,Redis用它来表示该元素缺失并且不是空串。这种情况会发生在带有GET pattern 选项的SORT命令在指定key缺失时。含Null的数组响应的示例:

*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

第二个元素为Null。客户端库应该返回类似这样:

["foo",nil,"bar"]

注意这不是我们之前说的异常,它只是更具体地描述协议的一个例子。

 

发送命令给Redis服务器

    现在你已经很熟悉RESP序列化格式了,实现一个Redis客户端库是很简单了。我们再说一下客户端与服务端是如何交互的:

  • 客户端发送给Redis服务器一个只包含Bulk String的RESP数组。
  • Redis客户端返回给客户端合法的RESP类型的响应。

    所以一个典型的交互示例如下:

    客户端发送命令 LLEN mylist 去获取 mylist 的列表长度,然后服务端返回一个整数响应,如下所示:(C: 客户端,S:服务器端)

    

C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n

S: :48293\r\n

   像往常一样为方便阅读我们用新行分割了协议的不同部分,但实际上客户端发送的是整个  *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n  。

 

多命令和pipeling

      客户端可以用相同的连接发起多个命令。Pipeling的支持使得多个命令可以在客户端的一次写操作中被发送,而不需要等待服务器对前一个命令的响应才能发起下一个请求。所有的响应可以在最后被读到。

更多的信息请查阅 Pipelining 。

(译注:对于基于像TCP这样的流式协议,Pipeling 实际上是一种协议的实现技术,站在服务端的角度就算它一次收到了多个命令,它也不知道客户端是一次发送了多个命令还是分了多次发送,但当服务器端一次收到多个命令时确实可以做一些优化处理,比如 优化 RTT, 多个命令的返回调用一次write系统调用从而减少系统调用的次数,提高吞吐量。)

 

Inline命令

      某些时候你手头上只有telnet 并且需要向Redis服务器发送命令。尽管Redis协议实现简单但并不适合用作交互会话,而redis-cli 并不是总是可用。因此Redis也支持一种为人而设的特殊格式命令,它称为inline命令格式。

一下是一个 server/client 用inline命令通信的例子(服务器 S, 客户端 C)。

C: PING
S: +PONG

      下面是另一个用 inline 命令 返回一个整数的例子:

C: EXISTS somekey
S: :0

      基本上在telnet会话中你可以简单的写空格分割的参数。由于在协议请求中没有命令以 * 开头,Redis可以检测这种情况并处理命令。

 

高效解析Redis协议

     尽管Redis协议非常可读并且容易实现,它却可以兼得二进制协议的高效。

RESP使用长度前缀来传输bulk 数据,所以不需要像JSON一样扫描数据负载中的特殊符号,或者用引号括住数据负载。

Bulk和Multi Bulk长度的处理可以一次处理一个字符,同时可以扫描CR字符,像如下的C代码:

#include <stdio.h>

int main(void) {
    unsigned char *p = "$123\r\n";
    int len = 0;

    p++;
    while(*p != '\r') {
        len = (len*10)+(*p - '0');
        p++;
    }

    /* Now p points at '\r', and the len is in bulk_len. */
    printf("%d\n", len);
    return 0;
}

     当第一个CR被识别后,后面的LF可以忽略不处理。然后bulk数据可以一次读取而不需要分析 数据负载。最后剩下的CR和LF字符串可以丢弃不处理。

与二进制协议比较性能时,Redis协议在大部分的高级语言实现起来足够简单,减少了客户端软件的bug数量。

(译注:

1. 协议中的CR和LF相当于分割符,命令间存在多个CRLF不应影响后续解析,应为多个CRLF应被忽略掉。例如:

$> (printf "PING\r\nPING\r\nPING\r\n\r\n\rPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
+PONG

2. 对比一下memcached的协议,redis的协议确实设计得比较精简:

(1) 一致的请求形式。redis的请求都是以 Bluk String 数组发送,不同命令只是数组的元素个数不同,所有命令的处理可以先读取完整个数组再根据不同命令解析数组的参数;而不是像mc协议一样,不同请求的命令格式不同,那么在读取网络字节流的过程中就要对不同命令做不同的处理,增加了协议解析的难度。

(2) 长度前缀是高效解析协议的关键。字段长度信息并不是二进制协议的专利,文本协议也可以有。

posted @ 2018-06-19 11:17  JianxingLee  阅读(1115)  评论(0编辑  收藏  举报