七的博客

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 升级对原有的客户端也没有特别大的影响,这种兼容性对于客户端生态是非常有好处的。

4. 参考链接