跳转到主要内容

协议规范

KeyDB 客户端使用名为 RESP (KeyDB 序列化协议) 的协议与 KeyDB 服务器通信。虽然该协议是专门为 KeyDB 设计的,但它也可用于其他客户端-服务器软件项目。

RESP 是以下几点之间的折衷方案:

  • 易于实现。
  • 解析速度快。
  • 人类可读。

RESP 可以序列化不同的数据类型,如整数、字符串、数组。还有一种特定类型用于错误。请求以字符串数组的形式从客户端发送到 KeyDB 服务器,表示要执行的命令的参数。KeyDB 回复命令特定的数据类型。

RESP 是二进制安全的,不需要处理从一个进程传输到另一个进程的大量数据,因为它使用前缀长度来传输大量数据。

注意:此处概述的协议仅用于客户端-服务器通信。KeyDB 集群使用不同的二进制协议来在节点之间交换消息。

网络层#

客户端通过创建到端口 6379 的 TCP 连接来连接到 KeyDB 服务器。

虽然 RESP 在技术上不特定于 TCP,但在 KeyDB 的上下文中,该协议仅用于 TCP 连接(或等效的面向流连接,如 Unix 套接字)。

请求-响应模型#

KeyDB 接受由不同参数组成的命令。一旦收到命令,它就会被处理并将回复发送回客户端。

这是可能的最简单模型,但有两个例外:

  • KeyDB 支持管道(本文档稍后介绍)。因此客户端可以一次发送多个命令,稍后等待回复。
  • 当 KeyDB 客户端订阅 Pub/Sub 通道时,协议会改变语义并成为一个 *推送* 协议,也就是说,客户端不再需要发送命令,因为服务器会在收到新消息(针对客户端订阅的通道)时自动将其发送给客户端。

除了上述两个例外,KeyDB 协议是一个简单的请求-响应协议。

RESP 协议描述#

RESP 协议成为与 KeyDB 服务器通信的标准方式。这是您应该在 KeyDB 客户端中实现的协议。

RESP 实际上是一种序列化协议,支持以下数据类型:简单字符串、错误、整数、批量字符串和数组。

RESP 在 KeyDB 中作为请求-响应协议的使用方式如下:

  • 客户端将命令作为批量字符串的 RESP 数组发送到 KeyDB 服务器。
  • 服务器根据命令实现用 RESP 类型之一回复。

在 RESP 中,某些数据类型的类型取决于第一个字节:

  • 对于简单字符串,回复的第一个字节是 "+"
  • 对于错误,回复的第一个字节是 "-"
  • 对于整数,回复的第一个字节是 ":"
  • 对于批量字符串,回复的第一个字节是 "$"
  • 对于数组,回复的第一个字节是 "*"

此外,RESP 能够使用批量字符串或数组的特殊变体来表示 Null 值,具体如下文所述。

在 RESP 中,协议的不同部分始终以 "\r\n" (CRLF) 终止。

RESP 简单字符串#

简单字符串的编码方式如下:一个加号字符,后跟一个不能包含 CR 或 LF 字符(不允许换行)的字符串,以 CRLF(即 "\r\n")终止。

简单字符串用于以最小的开销传输非二进制安全字符串。例如,许多 KeyDB 命令在成功时只回复 "OK",作为 RESP 简单字符串,它用以下 5 个字节编码:

"+OK\r\n"

为了发送二进制安全字符串,则使用 RESP 批量字符串。

当 KeyDB 回复一个简单字符串时,客户端库应该向调用者返回一个由 '+' 字符后面的第一个字符到字符串末尾(不包括最后的 CRLF 字节)组成的字符串。

RESP 错误#

RESP 有一个专门用于错误的数据类型。实际上,错误与 RESP 简单字符串完全相同,但第一个字符是减号 '-' 而不是加号。RESP 中简单字符串和错误之间的真正区别在于,客户端将错误视为异常,而构成错误类型的字符串就是错误消息本身。

基本格式是:

"-Error message\r\n"

错误回复仅在发生错误时发送,例如,如果您尝试对错误的数据类型执行操作,或者命令不存在等等。当收到错误回复时,库客户端应抛出异常。

以下是错误回复的示例:

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

"-" 之后的第一个单词,直到第一个空格或换行符,表示返回的错误类型。这只是 KeyDB 使用的约定,不属于 RESP 错误格式的一部分。

例如,ERR 是通用错误,而 WRONGTYPE 是一种更具体的错误,意味着客户端尝试对错误的数据类型执行操作。这称为错误前缀,它是一种允许客户端理解服务器返回的错误类型而无需依赖可能随时间变化的精确消息的方式。

客户端实现可以针对不同的错误返回不同类型的异常,或者可以提供一种通用方法,通过直接向调用者提供错误名称作为字符串来捕获错误。

但是,此功能不应被视为至关重要,因为它很少有用,并且受限的客户端实现可以简单地返回通用错误条件,例如 false

RESP 整数#

此类型只是一个以 CRLF 终止的字符串,表示一个整数,前缀为 ":" 字节。例如,":0\r\n" 或 ":1000\r\n" 都是整数回复。

许多 KeyDB 命令返回 RESP 整数,例如 INCRLLENLASTSAVE

返回的整数没有特殊含义,它只是 INCR 的增量数字,LASTSAVE 的 UNIX 时间等等。但是,返回的整数保证在有符号 64 位整数的范围内。

整数回复也广泛用于返回 true 或 false。例如,EXISTSSISMEMBER 等命令将返回 1 表示 true,0 表示 false。

其他命令如 SADDSREMSETNX,如果操作实际执行,则返回 1,否则返回 0。

以下命令将回复整数:SETNXDELEXISTSINCRINCRBYDECRDECRBYDBSIZELASTSAVERENAMENXMOVELLENSADDSREMSISMEMBERSCARD

RESP 批量字符串#

批量字符串用于表示长度最大为 512 MB 的单个二进制安全字符串。

批量字符串的编码方式如下:

  • 一个 "$" 字节,后跟组成字符串的字节数(前缀长度),以 CRLF 终止。
  • 实际的字符串数据。
  • 一个最终的 CRLF。

因此,字符串 "foobar" 的编码如下:

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

当空字符串时,它只是:

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

RESP 批量字符串也可以用于使用特殊格式表示值的非存在性,该格式用于表示 Null 值。在这种特殊格式中,长度为 -1,没有数据,因此 Null 表示为:

"$-1\r\n"

这称为空批量字符串

当服务器回复空批量字符串时,客户端库 API 不应返回空字符串,而应返回 nil 对象。例如,Ruby 库应返回“nil”,而 C 库应返回 NULL(或在回复对象中设置特殊标志),依此类推。

RESP 数组#

客户端使用 RESP 数组向 KeyDB 服务器发送命令。同样,某些 KeyDB 命令向客户端返回元素集合时使用 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>CRLF 部分之后,构成数组的其他数据类型只是一个接一个地连接起来。例如,包含三个整数的数组编码如下:

"*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,以指定接下来将有五条回复。然后传输构成多批量回复项目的每条回复。

空数组的概念也存在,并且是指定 Null 值的另一种方式(通常使用 Null 批量字符串,但由于历史原因,我们有两种格式)。

例如,当 BLPOP 命令超时时,它会返回一个空数组,其计数为 -1,如以下示例所示:

"*-1\r\n"

当 KeyDB 回复空数组时,客户端库 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,另一个数组包含一个简单字符串和一个错误。

数组中的空元素#

数组的单个元素可以为空。这在 KeyDB 回复中用于表示这些元素缺失而不是空字符串。这在使用带有 GET *pattern* 选项的 SORT 命令时可能会发生,当指定的键缺失时。包含空元素的数组回复示例:

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

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

["foo",nil,"bar"]

请注意,这与前面章节中所述的并非例外,而只是进一步说明协议的示例。

向 KeyDB 服务器发送命令#

现在您已经熟悉 RESP 序列化格式,编写 KeyDB 客户端库的实现将变得容易。我们可以进一步说明客户端和服务器之间的交互方式:

  • 客户端向 KeyDB 服务器发送一个仅包含批量字符串的 RESP 数组。
  • KeyDB 服务器向客户端发送任何有效的 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 作为一个整体发送。

多个命令和管道#

客户端可以使用同一个连接来发出多个命令。支持管道,因此客户端可以通过单个写入操作发送多个命令,而无需在发出下一个命令之前读取前一个命令的服务器回复。所有回复都可以在最后读取。

欲了解更多信息,请查看我们的关于管道的页面

内联命令#

有时您手头只有 telnet,需要向 KeyDB 服务器发送命令。虽然 KeyDB 协议易于实现,但它不适合在交互式会话中使用,而且 KeyDB-cli 可能并不总是可用。因此,KeyDB 还接受一种特殊的人性化命令方式,称为内联命令格式。

以下是使用内联命令的服务器/客户端对话示例(服务器对话以 S: 开头,客户端对话以 C: 开头):

C: PING
S: +PONG

以下是返回整数的内联命令的另一个示例:

C: EXISTS somekey
S: :0

基本上,您只需在 telnet 会话中写入以空格分隔的参数。由于没有命令以 * 开头(* 用于统一请求协议),KeyDB 能够检测到这种情况并解析您的命令。

KeyDB 协议的高性能解析器#

尽管 KeyDB 协议非常人性化且易于实现,但其性能可以与二进制协议媲美。

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 字符,无需任何处理。

KeyDB 协议的性能与二进制协议相当,但在大多数高级语言中实现起来却明显简单得多,从而减少了客户端软件中的错误数量。