文档:序列化协议规范

为了与 Valkey 服务器通信,Valkey 客户端使用一种名为 REdis 序列化协议(RESP)的协议。尽管该协议是为 Redis 设计的,但它被许多其他客户端-服务器软件项目所使用。

RESP 是以下考量因素之间的折衷方案

  • 易于实现。
  • 解析速度快。
  • 可读性强。

RESP 可以序列化不同的数据类型,包括整数、字符串和数组。它还具有错误专用类型。客户端将请求作为字符串数组发送到 Valkey 服务器。数组的内容是服务器应执行的命令及其参数。服务器的回复类型是命令特定的。

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

RESP 是您应该在 Valkey 客户端中实现的协议。

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

RESP 版本

RESP 协议的第一个版本是实验性的,从未被广泛使用。

下一个版本 RESP2 很快成为客户端与 Redis OSS 通信的标准方法。

RESP3 是 RESP2 的超集,主要旨在让客户端开发者的工作变得更容易一些。Redis OSS 6.0 引入了对 RESP3 功能的实验性可选支持(不包括流式字符串和流式聚合)。此外,HELLO 命令的引入允许客户端握手并升级连接的协议版本(参见客户端握手)。

直到 Redis OSS 7(含),RESP2 和 RESP3 客户端都可以调用所有核心命令。然而,不同协议版本的命令可能会返回不同类型的回复。

Valkey 的未来版本可能会更改默认协议版本,但 RESP2 不太可能被完全弃用。然而,未来版本中的新功能可能需要使用 RESP3。

网络层

客户端通过创建到 Valkey 服务器端口(默认为 6379)的 TCP 连接来连接 Valkey 服务器。

虽然 RESP 技术上并非 TCP 特定,但在 Valkey 的上下文中,该协议专门与 TCP 连接(或等效的面向流连接,如 Unix 套接字)一起使用。

请求-响应模型

Valkey 服务器接受由不同参数组成的命令。然后,服务器处理该命令并将回复发送回客户端。

这是最简单的模型;然而,也存在一些例外情况

  • Valkey 请求可以流水线化。流水线化使客户端能够一次发送多个命令,稍后等待回复。
  • 当 RESP2 连接订阅发布/订阅通道时,协议语义会发生变化,成为推送协议。客户端不再需要发送命令,因为服务器会在收到新消息后立即自动将其发送给客户端(对于客户端已订阅的通道)。
  • MONITOR 命令。调用 MONITOR 命令将连接切换到临时推送模式。此模式的协议未指定,但易于解析。
  • 保护模式。在保护模式下,从非回环地址到 Valkey 的连接将被服务器拒绝并终止。在终止连接之前,Valkey 无条件地发送 -DENIED 回复,无论客户端是否写入套接字。
  • RESP3 推送类型。顾名思义,推送类型允许服务器将带外数据发送到连接。服务器可以随时推送数据,并且该数据不一定与客户端执行的特定命令相关。
  • 当使用 RESP3 时,SUBSCRIBEUNSUBSCRIBE 命令及其模式和分片变体,返回错误回复或一个或多个推送回复,而没有任何常规带内回复。这被认为是这些命令的设计缺陷,但为了向后兼容,此行为得以保留。客户端需要对此行为进行补偿。

除了这些例外,Valkey 协议是一个简单的请求-响应协议。

RESP 协议描述

RESP 本质上是一种支持多种数据类型的序列化协议。在 RESP 中,数据的第一个字节决定了其类型。

Valkey 通常以以下方式使用 RESP 作为请求-响应协议

  • 客户端将命令作为批量字符串数组发送到 Valkey 服务器。数组中的第一个(有时是第二个)批量字符串是命令的名称。数组的后续元素是命令的参数。
  • 服务器回复一个 RESP 类型。回复的类型由命令的实现以及客户端的协议版本决定。

RESP 是一种二进制协议,使用标准 ASCII 编码的控制序列。例如,字符 A 的二进制字节值为 65。类似地,字符 CR(\r)、LF(\n)和 SP( )的二进制字节值分别为 13、10 和 32。

\r\n(CRLF)是协议的终止符,它总是分隔协议的各个部分。

RESP 序列化负载中的第一个字节始终标识其类型。后续字节构成该类型的内容。

我们将每种 RESP 数据类型分为简单批量聚合

简单类型类似于编程语言中的标量,表示简单的字面值。布尔值和整数就是这样的例子。

RESP 字符串分为简单字符串或批量字符串。简单字符串绝不包含回车符(\r)或换行符(\n)。批量字符串可以包含任何二进制数据,也可以称为二进制blob。请注意,批量字符串可能会被客户端进一步编码和解码,例如使用宽多字节编码。

聚合类型,如数组和映射,可以具有不同数量的子元素和嵌套级别。

下表总结了 Valkey 支持的 RESP 数据类型

RESP 数据类型最低协议版本类别第一个字节
简单字符串RESP2简单+
简单错误RESP2简单-
整数RESP2简单:
批量字符串RESP2聚合$
数组RESP2聚合*
空值RESP3简单_
布尔值RESP3简单#
双精度浮点数RESP3简单,
大数RESP3简单(
批量错误RESP3聚合!
原文字符串RESP3聚合=
映射RESP3聚合%
集合RESP3聚合~
推送RESP3聚合>

简单字符串

简单字符串以加号(+)字符开头,后跟一个字符串。该字符串不得包含 CR(\r)或 LF(\n)字符,并以 CRLF(即 \r\n)终止。

简单字符串以最小的开销传输短的非二进制字符串。例如,许多 Valkey 命令在成功时只回复“OK”。此简单字符串的编码是以下 5 个字节

+OK\r\n

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

要发送二进制字符串,请改用批量字符串

简单错误

RESP 有特定的错误数据类型。简单错误(或简称错误)类似于简单字符串,但它们的第一个字符是减号(-)。RESP 中简单字符串和错误之间的区别在于,客户端应将错误视为异常,而错误类型中编码的字符串本身就是错误消息。

基本格式是

-Error message\r\n

Valkey 仅在出现问题时才回复错误,例如,当您尝试对错误的数据类型进行操作,或者当命令不存在时。客户端在收到错误回复时应抛出异常。

以下是错误回复的示例

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

- 之后,直到第一个空格或换行符的第一个大写单词表示返回的错误类型。这个词被称为错误前缀。请注意,错误前缀是 Valkey 使用的一种约定,而不是 RESP 错误类型的一部分。

例如,在 Valkey 中,ERR 是一个通用错误,而 WRONGTYPE 是一个更具体的错误,表示客户端尝试对错误的数据类型进行操作。错误前缀允许客户端在不检查确切错误消息的情况下理解服务器返回的错误类型。

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

然而,这样的功能不应被视为至关重要,因为它很少有用。此外,更简单的客户端实现可以返回通用错误值,例如 false

整数

此类型是以 CRLF 终止的字符串,表示一个有符号的 64 位十进制整数。

RESP 以以下方式编码整数

:[<+|->]<value>\r\n
  • 冒号(:)作为第一个字节。
  • 可选的加号(+)或减号(-)作为符号。
  • 一个或多个十进制数字(0..9)作为整数的无符号十进制值。
  • CRLF 终止符。

例如,:0\r\n:1000\r\n 是整数回复(分别为零和一千)。

许多 Valkey 命令返回 RESP 整数,包括 INCRLLENLASTSAVE。整数本身除了在返回它的命令上下文中外,没有特殊含义。例如,它是 INCR 的增量数字,LASTSAVE 的 UNIX 时间戳等。然而,返回的整数保证在有符号 64 位整数的范围内。

在某些情况下,整数可以表示布尔值的真和假。例如,SISMEMBER 返回 1 表示真,0 表示假。

其他命令,包括 SADDSREMSETNX,在数据更改时返回 1,否则返回 0。

批量字符串

批量字符串表示单个二进制字符串。该字符串可以是任意大小,但默认情况下,Valkey 将其限制为 512 MB(参见 proto-max-bulk-len 配置指令)。

RESP 以以下方式编码批量字符串

$<length>\r\n<data>\r\n
  • 美元符号($)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为字符串的长度(以字节为单位),作为无符号十进制值。
  • CRLF 终止符。
  • 数据。
  • 一个最后的 CRLF。

因此,“hello”字符串编码如下

$5\r\nhello\r\n

空字符串的编码是

$0\r\n\r\n

数组

客户端将命令作为 RESP 数组发送到 Valkey 服务器。类似地,一些返回元素集合的 Valkey 命令使用数组作为其回复。例如,LRANGE 命令返回列表的元素。

RESP 数组的编码使用以下格式

*<number-of-elements>\r\n<element-1>...<element-n>
  • 星号(*)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为数组中元素的数量,作为无符号十进制值。
  • CRLF 终止符。
  • 数组中每个元素的附加 RESP 类型。

所以一个空数组就是以下内容

*0\r\n

而由两个批量字符串“hello”和“world”组成的数组的编码是

*2\r\n$5\r\nhello\r\n$5\r\nworld\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
$5\r\n
hello\r\n

(原始 RESP 编码为可读性被拆分为多行)。

服务器发送的第一行是 *5\r\n。这个数值告诉客户端,接下来将有五种回复类型。然后,每个后续回复都构成数组中的一个元素。

所有聚合 RESP 类型都支持嵌套。例如,一个包含两个数组的嵌套数组编码如下

*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Hello\r\n
-World\r\n

(原始 RESP 编码为可读性被拆分为多行)。

上述编码了一个包含两个元素的数组。第一个元素是一个数组,其中又包含三个整数(1、2、3)。第二个元素是另一个包含一个简单字符串和一个错误的数组。

注意:在某些地方,RESP 数组类型可能被称为多批量。两者是相同的。

空值

空数据类型表示不存在的值。

在 RESP3 中,空值使用下划线(_)字符编码,后跟 CRLF 终止符(\r\n)。以下是空值的原始 RESP 编码

_\r\n

RESP2 具有两个专门用于表示空值的特殊值,称为“空批量字符串”和“空数组”。这种双重性一直是一种冗余,对协议本身没有增加任何语义价值。RESP3 中引入的空值类型旨在纠正这一错误。客户端应以相同的方式处理所有这些空值表示。例如,Ruby 库应返回 nil,而 C 库应返回 NULL(或在回复对象中设置一个特殊标志)。

空批量字符串

虽然 RESP3 有专门的空值数据类型,但 RESP2 没有。相反,由于历史原因,RESP2 中空值的表示是通过批量字符串数组类型的预定形式实现的。

空批量字符串表示不存在的值。当目标键不存在时,GET 命令返回空批量字符串。

它被编码为一个长度为负一(-1)的批量字符串,如下所示

$-1\r\n

当服务器回复空批量字符串而不是空字符串时,Valkey 客户端应返回 nil 对象。

空数组

虽然 RESP3 有专门的空值数据类型,但 RESP2 没有。相反,由于历史原因,RESP2 中空值的表示是通过批量字符串数组类型的预定形式实现的。

空数组是表示空值的另一种方式。例如,当 BLPOP 命令超时时,它返回一个空数组。

空数组的编码是一个长度为 -1 的数组,即

*-1\r\n

当 Valkey 回复空数组时,客户端应返回一个空对象而不是一个空数组。

数组中的空元素

数组的单个元素可以为空。这在 Valkey 回复中用于表示这些元素缺失而不是空字符串。例如,当 SORT 命令与 GET pattern 选项一起使用时,如果指定的键缺失,则可能发生这种情况。

以下是一个包含空元素的数组回复示例,表示为 RESP2 空批量字符串

*3\r\n
$5\r\n
hello\r\n
$-1\r\n
$5\r\n
world\r\n

上面,第二个元素是空值。客户端库应向其调用者返回类似这样的内容

["hello",nil,"world"]

布尔值

RESP 布尔值编码如下

#<t|f>\r\n
  • 井号字符(#)作为第一个字节。
  • t 字符表示真值,f 字符表示假值。
  • CRLF 终止符。

双精度浮点数

双精度 RESP 类型编码双精度浮点值。双精度浮点数编码如下

,[<+|->]<integral>[.<fractional>][<E|e>[sign]<exponent>]\r\n
  • 逗号字符(,)作为第一个字节。
  • 可选的加号(+)或减号(-)作为符号。
  • 一个或多个十进制数字(0..9)作为无符号十进制整数值。
  • 可选的点号(.),后跟一个或多个十进制数字(0..9)作为无符号十进制小数部分。
  • 可选的大写或小写字母 E(Ee),后跟可选的加号(+)或减号(-)作为指数符号,以一个或多个十进制数字(0..9)作为无符号十进制指数值结尾。
  • CRLF 终止符。

以下是数字 1.23 的编码

,1.23\r\n

由于小数部分是可选的,因此整数值 10 既可以作为整数,也可以作为双精度浮点数进行 RESP 编码

:10\r\n
,10\r\n

在这种情况下,Valkey 客户端应分别返回原生整数和双精度浮点值,前提是其实现语言支持这些类型。

正无穷大、负无穷大和 NaN 值编码如下

,inf\r\n
,-inf\r\n
,nan\r\n

大数

此类型可以编码超出有符号 64 位整数范围的整数值。

大数使用以下编码

([+|-]<number>\r\n
  • 左括号字符(()作为第一个字节。
  • 可选的加号(+)或减号(-)作为符号。
  • 一个或多个十进制数字(0..9)作为无符号十进制值。
  • CRLF 终止符。

示例

(3492890328409238509324850943850943825024385\r\n

大数可以是正数或负数,但不能包含小数。用支持大数类型的语言编写的客户端库应返回大数。当不支持大数时,客户端应返回字符串,并在可能的情况下向调用者指示回复是一个大整数(取决于客户端库使用的 API)。

批量错误

此类型结合了简单错误的目的和批量字符串的表达能力。

它编码为

!<length>\r\n<error>\r\n
  • 感叹号(!)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为错误的长度(以字节为单位),作为无符号十进制值。
  • CRLF 终止符。
  • 错误本身。
  • 一个最后的 CRLF。

按照惯例,错误以一个大写(空格分隔)的单词开头,传达错误消息。

例如,错误“SYNTAX invalid syntax”由以下协议编码表示

!21\r\n
SYNTAX invalid syntax\r\n

(原始 RESP 编码为可读性被拆分为多行)。

原文字符串

此类型类似于批量字符串,额外提供了关于数据编码的提示。

原文字符串的 RESP 编码如下

=<length>\r\n<encoding>:<data>\r\n
  • 等号(=)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为字符串的总长度(以字节为单位),作为无符号十进制值。
  • CRLF 终止符。
  • 恰好三(3)个字节表示数据的编码。
  • 冒号(:)字符分隔编码和数据。
  • 数据。
  • 一个最后的 CRLF。

示例

=15\r\n
txt:Some string\r\n

(原始 RESP 编码为可读性被拆分为多行)。

一些客户端库可能会忽略此类型与字符串类型之间的区别,并在两种情况下都返回原生字符串。然而,交互式客户端,例如命令行界面(例如 valkey-cli),可以使用此类型并知道其输出应原样呈现给用户,而无需引用字符串。

例如,Valkey 命令 INFO 输出的报告包含换行符。当使用 RESP3 时,valkey-cli 正确显示它,因为它作为原文字符串回复发送(其三个字节为“txt”)。然而,当使用 RESP2 时,valkey-cli 被硬编码为查找 INFO 命令以确保其正确显示给用户。

映射

RESP 映射编码键值对的集合,即字典或哈希表。

它编码如下

%<number-of-entries>\r\n<key-1><value-1>...<key-n><value-n>
  • 百分号字符(%)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为映射中条目或键值对的数量,作为无符号十进制值。
  • CRLF 终止符。
  • 映射中每个键和值的两个附加 RESP 类型。

例如,以下 JSON 对象

{
    "first": 1,
    "second": 2
}

可以这样在 RESP 中编码

%2\r\n
+first\r\n
:1\r\n
+second\r\n
:2\r\n

(原始 RESP 编码为可读性被拆分为多行)。

映射的键和值都可以是 RESP 的任何类型。

Valkey 客户端应返回其语言提供的惯用字典类型。然而,低级编程语言(例如 C 语言)可能会返回一个数组以及类型信息,以向调用者指示它是一个字典。

注意:RESP2 没有映射类型。RESP2 中的映射由一个包含键和值的扁平数组表示。第一个元素是键,后跟对应的值,然后是下一个键,依此类推,如:key1, value1, key2, value2, ...

集合

集合有点像数组,但它们是无序的,并且只应包含唯一元素。

RESP 集合的编码是

~<number-of-elements>\r\n<element-1>...<element-n>
  • 波浪号(~)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为集合中元素的数量,作为无符号十进制值。
  • CRLF 终止符。
  • 集合中每个元素的附加 RESP 类型。

如果其编程语言中提供原生集合类型,客户端应返回该类型。另外,在没有原生集合类型的情况下,可以使用与类型信息结合的数组(例如在 C 语言中)。

推送

RESP 的推送包含带外数据。它们是协议请求-响应模型的一个例外,并为连接提供了一种通用的推送模式

推送事件的编码类似于数组,仅在第一个字节上有所不同

><number-of-elements>\r\n<element-1>...<element-n>
  • 大于号(>)作为第一个字节。
  • 一个或多个十进制数字(0..9)作为消息中元素的数量,作为无符号十进制值。
  • CRLF 终止符。
  • 推送事件中每个元素的附加 RESP 类型。

推送的数据可以位于 RESP 任何数据类型之前或之后,但绝不能位于其内部。这意味着客户端不会在映射回复的中间找到推送数据。这也意味着推送数据可以出现在命令回复之前或之后,也可以单独出现(无需调用任何命令)。

客户端应通过调用实现推送数据处理的回调函数来响应推送。

客户端握手

新的 RESP 连接应通过调用 HELLO 命令开始会话。这种做法实现了两件事

  1. 它允许服务器向后兼容 RESP2 版本。这在 Valkey 中是必需的,以使向协议版本 3 的过渡更平稳。
  2. HELLO 命令返回有关服务器和协议的信息,客户端可以将其用于不同目的。

HELLO 命令具有以下高级语法

HELLO <protocol-version> [optional-arguments]

命令的第一个参数是我们希望连接设置的协议版本。默认情况下,连接以 RESP2 模式开始。如果我们指定一个过大且服务器不支持的连接版本,它应该回复一个 -NOPROTO 错误。示例

Client: HELLO 4
Server: -NOPROTO sorry, this protocol version is not supported.

此时,客户端可以尝试使用较低的协议版本。

同样,客户端可以轻松检测到一个只能使用 RESP2 的服务器

Client: HELLO 3
Server: -ERR unknown command 'HELLO'

然后客户端可以继续使用 RESP2 与服务器通信。

请注意,即使协议版本受支持,HELLO 命令也可能返回错误,不执行任何操作并保持 RESP2 模式。例如,当在命令的可选 !AUTH 子句中使用无效身份验证凭据时

Client: HELLO 3 AUTH default mypassword
Server: -ERR invalid password
(the connection remains in RESP2 mode)

HELLO 命令的成功回复是一个映射回复。回复中的信息部分取决于服务器,但某些字段是所有 RESP3 实现的强制性字段

  • server:“redis”(或其他软件名称)。
  • version:服务器版本。
  • proto:RESP 协议的最高支持版本。

在 Valkey 的 RESP3 实现中,还发出以下字段

  • id:连接的标识符(ID)。
  • mode:“standalone”、“sentinel”或“cluster”。
  • role:“primary”或“replica”。
  • modules:已加载模块列表,作为批量字符串数组。

向 Valkey 服务器发送命令

现在您已熟悉 RESP 序列化格式,您可以使用它来帮助编写 Valkey 客户端库。我们可以进一步明确客户端和服务器之间的交互方式

  • 客户端向 Valkey 服务器发送一个只包含批量字符串的数组
  • Valkey 服务器回复客户端,发送任何有效的 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

多命令和流水线

客户端可以使用同一连接发出多个命令。支持流水线,因此客户端可以通过单个写入操作发送多个命令。客户端可以跳过读取回复,继续一个接一个地发送命令。所有回复都可以在最后读取。

更多信息,请参见流水线

内联命令

有时您可能需要向 Valkey 服务器发送命令,但只有 telnet 可用。虽然 Valkey 协议易于实现,但它不适用于交互式会话,并且 valkey-cli 可能并非始终可用。因此,Valkey 也接受内联命令格式的命令。

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

C: PING
S: +PONG

这是另一个服务器返回整数的内联命令示例

C: EXISTS somekey
S: :0

基本上,要发出内联命令,您需要在 telnet 会话中写入以空格分隔的参数。由于没有命令以 *(RESP 数组的标识字节)开头,Valkey 会检测到这种情况并内联解析您的命令。

Valkey 协议的高性能解析器

虽然 Valkey 协议可读性强且易于实现,但其实现可以表现出与二进制协议相似的性能。

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 字符在不进行额外处理的情况下被丢弃。

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

Valkey 客户端作者提示

  • 出于测试目的,可以使用 Lua 的类型转换来让 Valkey 回复所需的任何 RESP2/RESP3 类型。例如,RESP3 双精度浮点数可以这样生成
    EVAL "return { double = tonumber(ARGV[1]) ]: " 0 1e0