Redis 客户端和 Redis 服务器使用 RESP 协议通信,RESP 是 REdis Serialization Protocol 的简称。尽管该协议是为redis设计的,你仍然可以把他用在其它的 client-server 架构的软件项目中。
RESP 协议是下面一些设计原则的折衷:
实现简单
解析(Parse)迅速
可读(Human Readable)
RESP 协议支持 integer、string、array 等类型的序列化,同时对错误也设计了特殊的类型来进行支持。请求从 client 端,以字符串数组的形式发送到 redis server 端。这些字符串数组代表着需要进行执行的命令。 server 端会以一种命令特化的数据类型进行响应。
RESP 协议二进制安全,不需要对从一个进程传送到另一个进程的批量(bulk)数据进行特殊处理,因为协议中定义了前置的length来标明批量数据的内容长度。
Note:这里的协议大纲只包含了 client-server 的通信方式。Redis Cluster 使用了其它的二进制协议来在集群节点间进行消息交换。
网络层
客户端用 TCP 连接到 redis 服务器的 6379 端口。
然而 RESP 协议在技术上也并没有为 TCP 做特化,只是在 Redis 协议的上下文中使用了 TCP 连接,或者等价的基于流(stream)的连接,例如Unix sockets。
请求-响应模型
redis 会接收(accept)不同类型参数的命令。一旦接收到命令便会进行处理并将响应发送回客户端。
这是最简单的模型了,不过还是要考虑两种例外:
redis 支持 pipeline(文档之后会提到)。所以对于客户端来说,可以一次性发送多条命令,然后等待稍后返回的所有响应。
当 redis 客户端订阅了一个 Pub/Sub 的 channel, 协议会改变语义,并切换为一种 push 协议,也就是说,在这个过程中,client 端不再需要发送命令,server 端一接收到新消息,会马上将这些消息发送给客户端 (当然是 client 端订阅了的 channel 们)。
除了上面提到的两种例外, redis 协议就是很简单的request-response 协议。
RESP protocol描述
RESP 协议是从 redis 1.2版本被引入的, 在 redis 2。0中成为了与 redis server 通信的标准方式。如果要实现一个你自己的 redis 的客户端,你也就需要实现这种协议。
RESP 实质上是一种支持下列数据类型的序列化协议:simple string,error,integer,bulk string 和 array。
在 redis 中使用 RESP 是下面这样的流程:
client 端向 redis server 端发送 RESP 的数组,数组内容为 bulk string。
server 会根据命令的具体实现,以 RESP 的其中一种类型进行响应。
在 RESP 中,数据的类型是依据第一个字节来判断的:
对于 simple string 来说,第一个字节是 "+"
对于 error 来说,第一个字节是 "-"
对于 integer 来说,第一个字节是 ":"
对于 bulk string 来说,首字节是 "$"
对于 array来说,首字节是 "*"
另外,RESP 可以以一个特殊的bulk string / array(稍后提到)来表示 Null 值。
在 RESP 协议中,协议的不同 part 之间会以"\r\n" (CRLF) 进行分隔。
RESP Simple Strings
Simple Strings 是按下面的方式进行编码: 一个加号,紧接着是不包含 CR 或者 LF 的字符串(不允许换行),最后以CRLF("\r\n")结尾。
Simple Strings 被用来传输非二进制安全的字符串,这种传输方式在传输上的开销很小。例如很多 redis 的命令会在成功的时候响应一个 "OK",这就是 RESP 一种 Simple String 的场景,这种情况下,OK 被编码为五个字节:
"+OK\r\n"
如果要传输二进制安全的字符串的话,就需要使用 RESP 定义里的 bulk string 了。
当 redis 用 simple string 进行响应的时候,client 端库需要返回给调用方的结果是从 + 号后面一直到整个字符串结束,但要去掉结尾的 CRLF 的两个字节。
RESP Errors
RESP 协议专门为错误定义了一种特定的数据类型。实际上 error 和 RESP simple string 很类似,只是第一个字符用 - 号 而不是 + 号。两者之间真正的区别实际上是在 client 端体现的,error 在客户端会被当作异常进行处理,构成 error 的字符串实际上也就是 error 信息自身。
基本的格式:
"-Error message\r\n"
出错的时候才会返回 error(废话),例如你对一种数据类型进行了错误的操作,或者你发送的 command 压根儿不存在等等。这时候 client 库就应该在接收到 error 的时候抛出异常(raise exception)什么的了。
下面是一些 error 响应的例子:
-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value
"-"号后的第一个单词,一直到第一个空格/新行,表示的是返回的错误类型。这些错误类型只是 redis 内部的一种约定,并不是 RESP 协议的规定。
例如,ERR 是一种一般类型的错误,然而 WRONGTYPE 就是一个相对来说更加具体的错误,表示客户端所要做的操作搞错了数据类型。ERR 和 WRONGTYPE 这两种字符串叫作 error 前缀,通过这种方式使客户端可以明白 server 端返回了什么类型的错误,而不需要去解析完整的错误信息,这些错误信息在未来还可能随时会发生变化。
client 端的实现上,可能会根据 server 端的不同错误来返回不同的异常类型,也可能只是简单地直接把 error 的名字以字符串的形式提供给调用方。
不过这种特性并不重要,实际应用中也没有太大用处,很多 client 的也只是有限地实现了错误处理,比如在出错的时候简单地返回了一个通用 false。
RESP Integers
这种类型是一个以 CRLF 结尾的代表 integer 的字符串,以冒号 ":" 开头。例如 ":0\r\n",或者 ":1000\r\n",都是 integer 响应。
许多 redis 的 command 会返回 RESP integer,例如 INCR, LLEN,LASTSAVE。
返回的这个 integer 没有什么特别的含义,只是代表 INCR 之后数值的结果、LASTSAVE 的 UNIX 时间戳等等。不过返回的这个 integer 会被确保在一个有符号的 64 bit位 integer 能表示的范围之内。
integer 响应也被延伸到了需要返回 true 和 false 的场景。例如像 EXISTS 或者 SISMEMBER 这样的命令会在 true 的时候返回1,false 的时候返回0。
像 SADD, SREM 和 SETNX 之类的命令会在操作确实执行(actually performed)了的时候返回1,其它情况下返回0。
下列这些命令会返回一个 integer:SETNX, DEL, EXISTS, INCR, INCRBY, DECR, DECRBY, DBSIZE, LASTSAVE, RENAMENX, MOVE, LLEN, SADD, SREM, SISMEMBER, SCARD。
RESP Bulk Strings
bulk string 被用来表示二进制安全的字符串串,bulk string的上限是 512 MB 大小。
bulk string 的编码方式是下面这种方式:
以 "$" 开头,紧跟着字符串表示的数据的总字节数,以 CRLF结尾。
然后是数据内容。
然后是最后的 CRLF。
所以 "foobar" 这个 string 会被编码成这样:
"$6\r\nfoobar\r\n"
一个空的字符串会像下面这样
"$0\r\n\r\n"
译注:和http、bthash之类的协议差不多,在头里给出长度信息。
RESP bulk string 也可以以特殊的格式,表示一种 "不存在" 的意思,不存在的意思其实就是通常的 Null value了。这种特殊格式下 length 头会被设置为 -1,并且没有任何的数据,所以 Null 是下面这样来表示的:
"$-1\r\n"
这被称为 Null bulk string。
server 响应为一个 Null bulk string 的时候,client 库不应该返回一个空字符串,而应该是一个 nil 的对象。例如 Ruby 库应该返回 'nil',而 C 库应该返回 NULL(或者在返回的对象上打上特殊标记),其它语言以此类推。
RESP Arrays
client 端向 redis server端发送命令使用的是 RESP array。相似的,redis 命令返回到客户端的元素元素集合使用 RESP array,这是响应的类型。例如 LRANGE 命令,会返回一个 list 里的一些元素。
RESP array会以下面这种格式进行发送:
以 * 字符开头,紧接着是十进制数表示的数组中的元素数量,然后是 CRLF
然后是数组中的各个元素,每一个都是 RESP 协议定义的一种类型
一个空数组是下面这样表示的:
"*0\r\n"
一个由 "foo" 和 "bar" 两个 RESP bulk string 构成的数组会被编码成下面这样:
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
可以看到 *<count>CRLF
是整个数组的前缀,其它的数组元素被一个一个拼接在后面。例如一个包含有三个 integer 元素的数组是像下面这样表示的:
"*3\r\n:1\r\n:2\r\n:3\r\n"
array也可以包含多种类型的元素,也就是说元素的类型没必要非得一致。例如,一个包含有四个 integer 和一个 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 表示有五个接下来的响应元素。紧跟着的每一个响应构成了这次传输 multi bulk 的元素。
Null array 的概念也是存在的,并且 Null array 也是表示 Null value的一种可选的方式(一般情况下都是使用 Null bulk string,不过因为历史原因,我们有两种表示格式)。
例如 BLPOP 命令超时了,就会返回一个 Null array,这里的 count 字段被设置为-1,如下例:
"*-1\r\n"
client 库的 API 应该返回一个 null 的对象,而不是 redis 返回的 Null array。这样做才能够区分出返回的是一个空列表,还是其它的情况(例如 BLPOP 命令里 timeout 的情况)。
数组的数组在 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 数据类型编码了一个二维数组,数组的两个元素也是数组,其中一个包含 integer 1, 2, 3,另一个数组包含了一个 simple string 和一个 error。
Null elements in Arrays
array 里的某个元素有可能是Null。在 redis 的响应里这可能会用来表示某个元素是不存在而且不是空字符串。这种情况可能会发生在执行 SORT 命令同时指定 GET pattern 选项时,某个 key 缺失的情景下。下面是一个包含有 Null 元素的 array的例子:
*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n
第二个元素是 Null。client 库解析后应该返回这种形式的数组:
["foo",nil,"bar"]
注意这种情况并不是前面小节中提到的异常情况,这个例子也只是为了进一步说明协议而已。
Sending commands to a Redis Server
既然你已经对 RESP 序列化格式比较熟悉了,写一个 redis 的 client 库也就很简单了。我们可以进一步地说明一下 client 和 server 是如何交互的:
client 端向 redis server 发送 RESP 数组,数组内容只有 bulk string
redis server 会向 client 端返回各种合法的 RESP 数据类型
例如下面这样,一个典型的交互过程:
client 端发送 LLEN mylist 命令来获取 key mylist 对应的 list 长度,server 以 integer 类型作为响应(C: 表示client,S: 表示server)。
C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n
S: :48293\r\n
像通常一样,为了简洁,我们将协议对应的不同 parts 用换行进行了分隔,不过实际上客户端发送的内容是*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n
这样的整体。
Multiple commands and pipelining
client 端可以使用相同的连接一次性发送多个命令。pipelining 的支持使得客户端只要一次 write 操作即可发送多条命令,在 write 某条命令期间也不需要去读取之前几条命令的服务端返回结果。而在服务端返回之后再进行统一读取。需要更多信息的话,请查询 Pipelining 相关的信息。
Inline Commands
有时候你可能手边只有 telnet 工具可以拿来测试向 redis server 发送命令。尽管 redis 协议在 client 端实现起来很简单,但初衷并不是用来在交互式终端 session里使用的,redis-cli 工具可能不是在任何环境下都可用。从这个角度出发,redis 也接受一种为人设计的特殊的命令,叫作inline command 格式(format)。
下面是一个 server/client 使用 inline command 进行通信的例子(server 通信用 S 开始,client 端用C 开始)
译注:实际测试的时候 C/S 端并不会带这些前缀,这里只是为了读着方便。
C: PING
S: +PONG
下面是另一个返回 integer 类型的 inline command 的例子:
C: EXISTS somekey
S: :0
基本上你只要在 telnet 会话中简单地写空格分隔的参数就好。由于这个通信过程没有用 RESP 协议定义的 * 作为开头,redis 能够检测到这种情况,并正确地解析你的命令。
High performance parser for the Redis protocol
虽然 redis 协议设计得十分对阅读友好(human readable)并且十分容易实现,但实际实现起来和二进制协议的性能依然比较接近。
RESP 使用了前缀的 length 来传输 bulk 数据,所以没有必要像 JSON 一类的数据结构需要通过扫描来查找特殊字符,也没必要在发送数据的时候给数据加上引号(quote the payload)之类的。
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 数据的读取只需要一次非常简单的 read 操作即可,不需要再去检查 payload 里的内容。最后结束的 CR 和 LF 字符则不做任何处理,直接被丢弃。
和二进制协议相比起来,redis 协议用高级语言实现起来也非常简单,从这方面也可以减少客户端软件的bug数量。