redis传输协议规范-下(Redis Protocol specification)
规范翻译上篇,请在我的历史文章查看
RESP数组
客户端使用RESP数组格式发送命令到Redis服务器(把命令参数和数据组装成一个数组发送给服务器)。同样,一些Redis命令返回集合类型也使用RESP数组。返回列表元素的 LRANGE命令就是一个例子。
RESP多数组使用以下格式:
- 一个*字符作为第一个字节,然后是数组中以十进制数表示的元素数量,最后是CRLF。
- 数组的每个元素都是单独的RESP类型。
所以一个空数组是这样的:
"*0\r\n"
而一个包含两个RESP大块字符串"foo"和"bar"的数组编码为:
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
在数组数量前缀 *<count>\r\n之后,构成数组的元素是按照其自描述的方式一个接一个地连接起来。例如,三个整数的数组编码如下:
"*3\r\n:1\r\n:2\r\n:3\r\n"
数组元素不需要是相同的类型。例如,一个由四个整数和一个大块字符串组成的列表可以按照如下方式进行编码:
*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个数组元素。紧接着是构成数组的每个元素。
空数组的概念也存在,它是指定空值的另一种方法(通常空值使用空大块字符串来表示,但由于历史原因,我们有两种格式)。
例如,当BLPOP命令超时时,它返回一个计数为-1的空数组,如下所示:
"*-1\r\n"
当Redis响应一个空数组时,客户端库API应该返回一个空对象,而不是一个空数组,这样才能区分服务器响应空列表和其他情况(例如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,另一个数组包含一个简单字符串和一个错误。
数组中的空元素(Null)
数组中的单个元素可以为空。Null表示元素是缺失的,而不是空字符串。当使用SORT命令带GET选项时时,如果redis缺少指定的键,就会发生这种情况。一个包含空元素的数组回复的例子:
*3\r\n $3\r\n foo\r\n $-1\r\n $3\r\n bar\r\n
第二个元素是空的。客户端库应该返回给调用方如下内容:
["foo",nil,"bar"]
请注意,这并不是前几节中所说的例外,而是一个更详细的示例。
向Redis服务器发送命令
既然您已经熟悉了RESP序列化格式,那么编写一个Redis客户端库就很容易了。我们进一步说明redis客户端与服务器端如何进行交互。
- 客户端向Redis服务器发送一个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作为一个整体。
多个命令和管道(pipelining)
客户端可以使用相同的连接发出多个命令。redis支持Pipelining,所以客户端可以通过一个写操作发送多个命令,而不需要在发出下一个命令之前读取上一个命令的服务器回复。所有的回复都可以在最后读取。
想了解更多信息,请查看我们关于管道的页面。
内联命令
有时你只能使用telnet,并且你想发送一个命令到Redis服务器。虽然Redis协议很容易实现,但在交互式会话(比如telnet)中使用并不理想,而Redis-cli客户端程序可能并不总是可用。因此,Redis也以一种专门为人类设计的特殊方式接受命令,被称为内联命令格式。
下面是一个使用内联命令进行服务器/客户端聊天的示例(服务器聊天以S:开始,客户端聊天以C:开始)
C: PING
S: +PONG
下面是另一个返回整数的内联命令的例子:
C: EXISTS somekey S: :0
基本上,你只需在telnet会话中编写空格分隔的参数。由于统一请求协议中没有以*开头的命令,所以Redis能够检测这个条件并解析命令。
高性能的Redis协议解析器
因为Redis协议具有很好的可读性并且容易实现,所以一般实现协议的性能几乎二进制协议相同。
RESP使用带前缀的长度来传输批量数据,因此不需要像对JSON那样扫描有效负载以获取特殊字符,也不需要引用需要发送到服务器的有效负载。
批量和多批量长度可以用每字符执行一次操作的代码进行处理,同时扫描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一起跳过,无需任何处理。然后可以使用一个不以任何方式检查负载的读操作来读取大容量数据。最后,剩余的CR和LF字符不经过任何处理就被丢弃。
在性能上几乎可以与二进制协议相比,但是Redis协议在大多数高级语言中实现起来要简单,减少了客户端软件中的bug数量。
参考资料: