Redis客户端跟服务端通信协议V3
Redis客户端跟服务端通信协议V3
从 RESP2 协议发布到现在,以及 10多年时间了。 在这 10 多年的事件里, Redis 经历过很多版本的迭代,也加了很多的新特性。 但是客户端和服务端通信的协议一直都还是使用的 RESP2 ,这个简单高效的文本协议开始显示出了不足的地方。
比较明显的缺点:
- 不支持二进制安全的字符串,比如 \0 字符。
- 不支持更复杂的数据类型和无符号整数。
- 错误处理和类型区分不够完善,只能返回简单的错误信息。
- 无法直接表示出 NULL 或布尔值,需要通过字符串或整数来模拟。
- 后续 Redis 要是继续扩展新的数据类型,纯文本协议可能支持起来会比较费劲。
随着 Redis6.0 版本的发布,RESP3 协议也跟着一起发布出来。RESP3 协议解决了 RESP2 协议的一部分痛点,同时也为后续的扩展提供了一定的支持。
根据文档里的描述,总结出 RESP3 协议下面几点改进:
对复杂数据类型的支持,比如 Set、Map、Push 等类型。
支持二进制安全字符串,现在可以正确处理包含 \0 的数据。
增加了错误类型的细分,可以提供更加丰富的错误信息。客户端可以更好更简单的处理异常。
引入了原生的 NULL 和布尔值支持,现在不用通过 0、1、-1 等去判断了。
支持服务器直接向客户端推送消息,不用客户端请求。
官方依旧是在强调,即使 Redis 协议涉及的很简单,但是这不会是客户端-服务器通信的瓶颈。 反而协议简单可以让客户端库有更好的生态。
改进原因也是因为 RESP2 协议引入约六年后,RESP协议可以有很多改进的地方。特别是可以让客户端实现起来更简单,而且还支持新的功能。
1. 简单类型
1.1 Blob 字符串
跟之前的 RESP2 协议一样,表现形式为 $<length>\r\n<bytes>\r\n 。
比如字符串【hello world】协议编码为:
$11\r\n
helloworld\r\n
空字符串编码为:
$0\r\n\r\n
1.2 简单字符串
表现形式为 +<string>\r\n 。
【hello world】编码为:
+hello world\r\n
1.3 简单错误
跟简单字符串完全相同,但初始字节是【-】而不是【+】。
比如:
-ERR this is the error description\r\n
错误中的第一个词是大写的,代表的是错误代码。剩下的字符串是错误消息本身。
ERR错误代码是一个通用的错误代码。错误代码对客户端区分不同错误条件很有用,去匹配动态的错误消息文本容易出错。
1.4 数值
数值的通用形式为: <number>\r\n。
数字 1234 编码为:
:1234\r\n
有效数字范围是在有符号64位整数范围,更大的数字应使用大数值类型。
1.5 Null
null类型编码比较简单,直接为 : _\r\n 。
1.6 Double
编码形式为: <floating-point-number>\r\n 。
比如 1.23 编码为:
,1.23\r\n
缺少小数部分也是有效的,比如数字 10 可以同时使用数字或者 double 类型返回:
:10\r\n
,10\r\n
double 还可以返回正无穷或负无穷:
,inf\r\n
,-inf\r\n
1.7 Boolean
真值和假值仅用 #t\r\n 和 #f\r\n 表示。
在没有布尔类型的编程语言中,客户端库应向调用方返回用于表示真和假的规范值。比如像 C 语言应返回值为 0 或 1 的整数类型。
1.8 Blob错误
通用形式为: !<length>\r\n<bytes>\r\n。它跟字符串类型完全相同。
像简单错误类型一样,第一个大写词代表错误代码,这样方便客户端去处理错误。
比如错误 “SYNTAX invalid syntax” 协议表示:
!21\r\nSYNTAX invalid syntax\r\n
1.9 原始字符串
跟 Blob 字符串类型完全相同,但初始字节是【=】 而不是【$】。前三个字节提供了关于后续字符串格式的信息,可以是txt表示纯文本,或 mkd 表示markdown。第四个字节固定是【。】然后是实际的字符串。
比如下面是一个原始字符串的例子:
=15\r\n
txt:Some string\r\n
使用原始字符串,可以简化客户端的实现。 客户端不需要去管输出要不要转义。
1.10 大数
这个类型表示超出 Number 类型可表示的数值,通常就是比有符号64位数字范围还要大的数字。
通用形式为【(<big number>\r\n 】,看看下面这个例子:
(3492890328409238509324850943850943825024385\r\n
大数可以是正数或者负数,但不可以包含小数部分。
2. 复杂类型
2.1 Array
数组的聚合类型字符是 *, 要表示一个包含三个数字1、2、3的数组就要像下面这样:
*3\r\n:1\r\n:2\r\n:3\r\n
数组也是支持嵌套数组的。
2.2 Map
Map的表示与数组完全相同,但不是使用【* 】,而是用 【%】开始编码值。因为 Map 表示的是 key value ,所以后续元素的数量必须是偶数。
比如一个 JSON 数据:
{
"first":1,
"second":2
}
编码为:
%2\r\n
+first\r\n
:1\r\n
+second\r\n
:2\r\n
【%】字符后面跟随的是 key value 对的数量。
2.3 Set
Set与数组类型完全相同,但第一个字节是【~】而不是【*】。
比如:
~5\r\n
+orange\r\n
+apple\r\n
#t\r\n
:100\r\n
:999\r\n
2.4 Attribute
Attribute 类型与 Map 类型完全相同,但开头不是【%】而是使用【|】。
比如对 MGET a b 的命令回复:
|1\r\n
+key-popularity\r\n
%2\r\n
$1\r\n
a\r\n
,0.1923\r\n
$1\r\n
b\r\n
,0.0012\r\n
*2\r\n
:2039123\r\n
:9543892\r\n
MGET的实际回复只是两项数组 [2039123,9543892] ,但属性指定了原始命令中提到的 key 的请求频率。
2.5 Push
PUSH 类型用于异步地向客户端发送消息,这种类型的消息可以用来实现发布/订阅模式或其他事件通知。
语法是【%】后跟元素的数量,比如下面这个例子:
%3\r\n
+message\r\n
+mychannel\r\n
+Hello, World!\r\n
- %3 表示这是一个包含 3 个元素的 PUSH 消息。
- +message 是消息类型。
- +mychannel 是频道名称。
- +Hello, World! 是消息内容。
2.6 Streamed String
流式字符串这种类型文档里有说明,是一个预留的协议,暂时还没有做具体的实现。
使用场景: 有时候服务器会向客户端传输不知道具体大小的字符串,这种就可以称为流。
先看个例子:
$?\r\n
;4\r\n
Hell\r\n
;5\r\n
o wor\r\n
;1\r\n
d\r\n
;0\r\n
传输以 【\(?】 开始。使用跟普通字符串相同的前缀【\)】,但后面不是具体的数值而是一个问号,这样告诉客户端这是一个分块编码传输,服务端也还不知道最终大小。
后面不同的部分以如下方式传输:
;<count>\r\n
... count bytes of data ...\r\n
最后结束的时候,需要传输一个长度为 0 ,而且没有数据的报文:
;0\r\n
这样就可以表示一个完整流字符串传输流程。
2.7 HELLO
客户端连接 Redis 始终都会是发送一个特殊的命令 HELLO 去发起连接,这样做优如下好处:
- 服务端可以向后兼容 RESP2 版本。
- HELLO 命令可以返回有关服务器和协议的信息,客户端根据服务端应答的数据灵活的做不同的处理。
命令的格式如下:
HELLO <protocol-version> [AUTH <username> <password>]
目前只有 AUTH 选项可用,用于有密码的情况下对客户端进行身份验证。
默认情况下将以 RESP2 模式启动,如果指定一个不支持的协议,服务端将会返回错误。
Client: HELLO 4
Server: -NOPROTO sorry this protocol version is not supported
Hello 命令的回复依赖于服务端的实现,下面几个字段是强制会返回的:
* server: "redis"
* version: 服务器版本
* proto: 支持的 RESP 协议的最大版本
RESP3 协议中会额外返回下面几个字段:
* id: 客户端连接 ID
* mode: "standalone", "sentinel" 或 "cluster"
* role: "master" 或 "replica"
* modules: 作为字符串数组加载的模块列表
3. 总结
可以发现,RESP2 协议的这些设计要点还是被保留下来:
- RESP 协议继续保持人类可读。
- 设计非常简单,实现起来比较简洁易懂。
- RESP 依旧不比具有固定长度字段的二进制协议慢。 在许多情况下可能更紧凑,这跟 Redis 命令的特点有关系。
RESP3 中跟 RESP2 中保持等价的类型有:
- 数组 : N 个其他类型的有序集合
- Blob 字符串: 二进制安全的字符串
- 简单字符串: 空间高效的非二进制安全字符串
- 简单错误: 空间高效的非二进制安全错误代码和消息
- 数字: 有符号 64 位范围内的整数
同时 RESP3 中新引入的类型有:
- Null: 单个空值,替代 RESP v2 的 【*-1】 和 【$-1】 这两个空值。
- Double: 浮点数。
- Boolean: true 或者 false 。
- Blob error: 二进制安全的错误代码和消息。
- Verbatim string (原样字符串) : 不经任何转义或过滤直接显示给人类的二进制安全字符串。
- Map: 键值对的有序集合。键和值可以是任何其他 RESP3 类型。
- Set: N 个其他类型的无序集合。
- Attribute: 类似于 Map 类型,但客户端应继续读取回复,忽略属性类型,并将其作为附加信息返回给客户端。
- Push: 服务器可能在连接中随时推送信息给客户端,这种消息类型就是 PUSH 类型。
- Hello: 客户端和服务器之间建立连接时发送的消息类型。提供服务器名称、版本等信息。
- Big number: Number 类型无法表示的大数值。
RESP3 协议依旧保持是纯文本的协议,同时 RESP2 中的类型依旧兼容保持不变。
即使 Redis 升级对原有的客户端也没有特别大的影响,这种兼容性对于客户端生态是非常有好处的。