Redis序列化协议:RESP
RESP协议
读一下redis的序列化协议,顺便做点记录。
简介
RESP协议即 REdis Serialization Protocol(Redis序列化协议)。
RESP协议在Redis 1.2中引入,在Redis 2.0中成为了与Redis服务器通信的标准方式。这是所有Redis客户端都要遵循的协议,我们甚至可以基于此协议,开发实现自己的Redis客户端。该协议支持以下数据类型:简单字符串,错误类型,整数,批量字符串和数组。
官方文档:Redis serialization protocol specification | Redis
数据类型
简单字符串(Simple strings)
用于表示“非二进制安全”的字符串,开头一个加号( +
),后续字符串不能包含CR(\r)或者LF(\n),以CRLF(\r\n)结尾,举例:
"+OK\r\n"
简单错误类型(Simple errors)
错误类型和简单字符串类型相似,但以减号( -
)开头。错误类型包含的字符串就是错误信息,基本格式如下:
"-Error message\r\n"
当服务器出错时才会向发送错误类型的数据,比如客户端要求服务器执行一个不存在的命令。下面是一些错误类型的数据:
-ERR unknown command 'helloworld'
-WRONGTYPE Operation against a key holding the wrong kind of value
整数(Integers)
整数类型的数据以冒号(:)开头,以CRLF(\r\n)结尾。举例:
":1000\r\n"
批量字符串(Bulk strings)
用于表示“二进制安全的字符串”。该类型数据包含两部分,首先是头部信息:美元符号($)开头,后紧跟字符串长度,以CRLF结尾。紧接着是数据部分,一个任意字符串,并同样以CRLF结尾。最大长度500M。举例:
"$5\r\nhello\r\n"
"$0\r\n\r\n"
批量字符串用长度-1表示空数据(Null Bulk String):
"$-1\r\n"
数组(Arrays)
数组以星号(*)开头,数组的序列化格式如下:
*<number-of-elements>\r\n<element-1>...<element-n>
第一个字节为星号(*),然后跟随一个无符号十进制数,例如:6、10、999等等。
空数组(长度为0的数组):
*0\r\n
两个bulk strings组成的数组:
*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
三个整数组成的数组:
*3\r\n:1\r\n:2\r\n:3\r\n
数组可以包含混合数据类型,以下编码表示四个整数和一个批量字符串组成的数组(为了可读性,原始 RESP 编码被分成多行):
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$5\r\n
hello\r\n
所有聚合 RESP 类型都支持嵌套。例如,两个数组的嵌套数组编码如下(为了可读性,原始 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
空数组(数组类型的空对象):
*-1\r\n
当Redis回复空数组时,客户端应该返回空对象而不是空数组。这是区分空列表和不同条件(例如 BLPOP
命令的超时条件)所必需的。
数组中的空元素:
数组的单个元素可能是空的批量字符串。这在 Redis 回复中使用,以表明这些元素丢失且不是空字符串。例如,当 SORT
命令与选项一起使用时,如果指定的键丢失,则可能会发生这种情况。
以下是包含 null 元素的数组回复示例:
*3\r\n
$5\r\n
hello\r\n
$-1\r\n
$5\r\n
world\r\n
上面,第二个元素为空。客户端库应该向其调用者返回如下内容:
["hello",nil,"world"]
空值(Nulls)
null 数据类型表示不存在的值。
Nulls 的编码是下划线( _
)字符,后面是 CRLF 终止符()。Null 的原始 RESP 编码:
_\r\n
空批量字符串(Null Bulk String)、空数组(Null Arrays)和空值(Nulls)
由于历史原因,RESP2 采用两个特殊值来表示批量字符串和数组的空值。这种二元性一直是一种冗余,没有为协议本身增加任何语义价值,白白增加了复杂度。RESP3 中引入的 null 类型旨在修复此错误。
布尔值(Booleans)
RESP 布尔值编码如下:
#<t|f>\r\n
井号( #
)开头,“t” 表示true,“f” 表示false,以CRLF结尾。
双精度浮点数(Doubles)
双精度数编码如下:
,[<+|->]<integral>[.<fractional>][<E|e>[sign]<exponent>]\r\n
逗号开头,CRLF结尾。去掉逗号和CRLF,中间这部分是就是用来表示浮点数的,类似科学计数法,但又不是严格意义的数学上的科学计数法。这种表示方法很常见,你可能在C语言中,或者Python中,又或者一些计算器中见过这种表示方法。
举例:
2^{100}
-> 1.3e100
0.33^{-100}
-> 0.33e-100
-9.3^{-5.6}
-> -9.3e-5.6
你可能会问,{-9.3}^{-100}
怎么表示?不好意思,这是个算式,上面描述的表示方法是用来表示数字的。你需要将负号提到括号外面去,保证它不需要计算就能一眼看出正负。至于这个算式,它到底是正是负,需要计算之后才知道,这种就不行。
回到RESP序列化协议。
数字 1.23 的编码:
,1.23\r\n
由于小数部分是可选的(optional),因此整数值十 (10) 可以被 RESP 编码为整数和双精度:
:10\r\n
,10\r\n
正无穷大、负无穷大和 NaN 值编码如下:
,inf\r\n
,-inf\r\n
,nan\r\n
大数字(Big numbers)
大数字使用以下编码:
([+|-]<number>\r\n
左括号( (
)字符作为第一个字节。
举例:
(3492890328409238509324850943850943825024385\r\n
批量错误(Bulk errors)
这种类型将简单错误的目的与批量字符串的表达能力结合起来。
编码:
!<length>\r\n<error>\r\n
感叹号( !
)作为第一个字节,length表示错误消息的长度,以字节为单位。
例如,错误 “SYNTAX invalid syntax” 由以下协议编码表示(为了可读性,原始 RESP 编码被分成多行):
!21\r\n
SYNTAX invalid syntax\r\n
逐字字符串(Verbatim strings)
此类型类似于批量字符串,此外还提供有关数据编码的提示。
逐字字符串的 RESP 编码如下:
=<length>\r\n<encoding>:<data>\r\n
等号( =
)作为第一个字节。
举例(为了可读性,原始 RESP 编码被分成多行):
=15\r\n
txt:Some string\r\n
哈希表(Maps)
编码方式如下:
%<number-of-entries>\r\n<key-1><value-1>...<key-n><value-n>
百分号( %
)开头,后紧跟键值对数量。键和值都可以是任何 RESP 类型。
例如,以下 JSON 对象:
{
"first": 1,
"second": 2
}
可以像这样在 RESP 中编码(为了可读性,原始 RESP 编码被分成多行):
%2\r\n
+first\r\n
:1\r\n
+second\r\n
:2\r\n
集合(Sets)
集合有点像数组,但是是无序的,并且应该只包含唯一的元素。
编码方式如下:
~<number-of-elements>\r\n<element-1>...<element-n>
波形符 ( ~
) 作为第一个字节,后面紧跟元素数量。
推送(Pushes)
推送事件的编码方式与数组类似,仅第一个字节不同:
><number-of-elements>\r\n<element-1>...<element-n>
客户端握手(Client handshake)
新的 RESP 连接应通过调用 HELLO
命令来开始会话。这种做法完成了两件事:
- 它允许服务器向后兼容 RESP2 版本。 Redis 需要这样做,以便更温和地过渡到协议版本 3。
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'
请注意,即使支持该协议的版本, HELLO
命令也可能返回错误、不执行任何操作并保持在 RESP2 模式。例如,当在命令的可选 AUTH
子句中使用无效的身份验证凭据时:
Client: HELLO 3 AUTH default mypassword
Server: -ERR invalid password
(the connection remains in RESP2 mode)
对 HELLO
命令的成功回复是 Map 回复。回复中的信息部分取决于服务器,但某些字段对于所有 RESP3 实现都是必需的:
- server:服务器软件名
- version:服务器的版本。
- proto:RESP 协议支持的最高版本。
在 Redis 的 RESP3 实现中,还会返回以下字段:
- id:连接的标识符(ID)
- mode:"standalone", "sentinel" or "cluster"
- role:"master" or "replica"
- modules:已加载模块列表,一个类型为Bulk Strings的数组。
向 Redis 服务器发送命令(Sending commands to a Redis server)
典型的交互可能如下所示:
客户端发送命令 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
作为一个整体发送。
多命令和流水线(Multiple commands and pipelining)
客户端可以使用同一个连接发出多个命令。支持流水线,因此客户端可以通过单个写入操作发送多个命令。客户端可以跳过读取回复并继续一个接一个地发送命令。所有回复都可以在文末阅读。
有关详细信息,请参阅 管道化。
内联命令(Inline commands)
有时您可能需要向 Redis 服务器发送命令,但只有 telnet
可用。虽然 Redis 协议实现起来很简单,但它对于交互式会话来说并不理想,并且 redis-cli
可能并不总是可用。因此,Redis 也接受内联命令格式的命令。
以下示例演示了使用内联命令进行服务器/客户端交换(服务器聊天以 S:
开头,客户端聊天以 C:
开头):
C: PING
S: +PONG
这是内联命令的另一个示例,其中服务器返回一个整数:
C: EXISTS somekey
S: :0