likes
comments
collection
share

你需要了解的Redis信息之:RESP/RESP3通讯协议

作者站长头像
站长
· 阅读数 10

一、概要:

任何基于CS模式的通讯程序,都会协定客户端与服务器之间的交互逻辑与通讯协议。通讯协议要么基于文本格式,如xml、json等格式,要么基于二进制格式,如protobuf、ice等。Redis作为标准CS架构,也不例外。但是它并未采取业界普遍使用的通用协议,而是自定义一种叫做RESP的协议。

RESP全称:Redis serialization protocol 。中文意思是Redis序列化协议,是英文REdis Serialization Protocol 的首部字符缩写。从1.2版本开始引入,到2.0版本正式标准化,沿用至今。6.0版本开始更新为RESP3协议,目前RESP和RESP3共存。

RESP3和RESP类似,但是表现力比RESP更丰富,支持更多的数据类型。虽然它是为了弥补RESP协议不足而设计出来的,但是它并非RESP的第三本的意思,而是一种全新设计的协议,RESP3中的数字3仅仅是名称的一部分,而不是版本号之类的代表信息。

本文按照时间顺序,沿这RESP这一协议的变迁过程来描述该通讯协议的点点滴滴。

二、RESP协议:

2.1 、数据类型:

任何通讯协议基本上都会定义协议格式和支持的数据类型,RESP协议也不例外。RESP目前支持简单字符串、Errors、整数、Bulk Strings(复合字符串)、数组类型。每种数据类型都通过特定的类型标识区分,不同的类型内部采取不同的编码结构。

2.1.1、简单字符串类型(Simple String):

简单字符串是一种非二进制安全的字符串数据类型。使用 + (加号) 作为类型标识符,数据终结跟着回车换号符。因此该数据类型的格式可以定义为:类型标识符(" + ")+数据内容+回车换行符( "\r\n" )。

如:Set命令的响应。

"+OK\r\n"

"+hello\r\n"

2.1.2、错误响应数据类型(Errors):

RESP 把错误信息定义为一种单独的类型,其内容类似简单字符串类型,但是错误类型使用 -(减号)作为类型标识符,终结符是回车换行,具体数据内容放在两者之间。

数据格式可定义为:类型标识符( "-" )+数据内容+回车换行符( "\r\n" )。

但是错误类型的数据有一个通用的约定:错误类型+错误描述。这是属于Redis的一个约定,并非是RESP协议规范中的定义。主要是为了方便客户端在无需对错误描述进行扫描就快速获取错误类型。

以下是相关错误类型的例子:

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

2.1.3、整数类型:

整数类型顾名思义就是一个整数数据的封装。RESP 对于整数类型的使用  : (冒号) 作为其类型标识符。终结符是回车换行。中间数据部分则是对应的整数。格式如下:类型标识符( ":" )+数据内容+回车换行( "\r\n" )。

如:":10000\r\n"。这一字符串,代表的是整数10000。

Redis 许多命令返回值都是整数类型。如INCR 、LLEN 和 LASTSAVE等。

此外Redis也会整数类型也可以用来代表真假。1位真,0为假。

如: ":1\r\n"。如果用来表示返回值,则表示为真的返回值

2.1.4、复合字符串(Bulk Stirngs):

Redis底层支持的字符串是二进制安全的。但目前为止介绍的无论是简单字符串,还是错误类型都是非二进制安全的。那么RESP有没有其方式来支持这种二进制安全的字符串呢?答案就是接下来介绍的符合字符串类型,英文名称叫:Bluk Strings。该类型支持最长为512MB大小的单个二进制字符串。

这种数据类型,其数据包含两部分内容:数据长度和数据内容。RESP协议则使用 $(美元符) 来标识这种复合字符串类型,而以回车换行符终结。

此外数据长度和数据内容也通过一个回车换行符号来进行区分。

其格式定义如下:类型标识( "$" )+长度信息+回车换行( "\r\n" )+数据+( "\r\n" )。

举个例子,字符串”hello”,如果使用复合字符串如何编码呢?

"$5\r\nhello\r\n"

那么空字符串呢?空字符串实际上长度为0,内容为空,则可以采取以下方式:"$0\r\n\r\n" 这种格式还是表示存在字符串信息的,只不过字符串是空值。而对于完全是Null值的信息,符合字符串则使用长度为-1来表示:"$-1\r\n"。这种只有一个回车换行,数据部分直接是Null,称之为Null Bulk Stirng ,这点是需要注意的。

2.1.5、数组类型:

目前为止介绍的所有数据类型,都仅仅是代表一个具体数据项的简单类型,它们只能表示一个数据项,而 Redis 客户端给服务器发送一个命令就既包含一个命令字和多个参数,如果通过以上的数据类型,则需要对其进行拼装组合才能使用。因此 RESP 协议定义了一种叫做数组类型的数据类型,用来表示这些复杂的数据项组合。

数组类型它代表的一个完整的数据项,只是其内容包含了各种数据类型。有可能是简单类型、也可能是嵌套的数组类型。

RESP 对于数组类型,通过  * (星号)作为其类型标识符。其内部格式如下:

  • 首字符是*,后面紧跟着是十进制数,用于表示该数组后续元素的个数,接着是回车换行符号(“\r\n”);
  • 数组元素逐个排列,它们都是 RESP定义的数据类型。

因此格式为:类型标识("*") + 元素个数 + "\r\n" + < RESP类型的元素>(多个) 。【注意数组类型并没有回车换行符作为终结的,这点和其他的简单类型不太一样。】

例子:两个Bulk String字符串组成的数组如果使用数组编码。则为:

"*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n"

如果数据为空,即空数组。可以如下表示:

"*0\r\n"

数组元素既支持混合数据类型,也支持空元素。

如以下例子:

"*3\r\n$5\r\nhello\r\n$-1\r\n$5\r\nworld\r\n"。

第二个元素是Null。

2.1.6、小结:

RESP 协议定义的 5 种数据类型,包括 4 种简单类型和 1 种复合类型。简单类型都拥有相同的格式规范:以类型标识符开头,以回车换行符终结;用于表示单一的数据项。数组类型则基于表示多种数据内容的组合,既包括了其元素长度信息,也包括了每个元素的具体内容,元素类型,既可以是 4 种简单的类型,也可以是数组类型本身,甚至可以是 4 种简单类型的特例,比如 Null等。而且数组中元素类型不需要完全一致。一般来说 客户端发送请求都是使用数组类型,而服务端返回,则可能是简单类型,也可能是数组类型。 下面通过思维导图的方式来更加直观的总结这些数据类型的格式和相关限制:

你需要了解的Redis信息之:RESP/RESP3通讯协议

三、RESP3 协议:

2.1、背景:

RESP 协议设计巧妙,经过多年发展,足以证明其高效和稳定。但 Redis 发展到今天,这发展多年的RESP 协议也逐步呈现出一些缺点。主要有以下几点:

  • 没有足够的语义表达能力,能让客户端程序准确的理解什么样的转换是最合适的。比如命令LRANGE, SMEMBERS HGETALL三个命令,在当前RESP 协议中,其返回结果都是多批量回复。可实际上这三个命令返回分别是数组、集合和映射。
  • 缺少重要的数据类型:浮点数和布尔类型分别作为字符串和整数返回(如本文2.2.3 整数类型描述)。Null更有双重表示:null bulk 和 null multi bulk。因为没有能将null数组和null字符串区分开来的语义。
  • 无法返回二进制安全错误
  • 附加功能、新功能在原有协议的内置支持不好,如Pub/Sub模式等、带外数据传输、流式数据传输等等。

因此在2018年5月2号发布了 称之为 RESP3 的设计初稿1.0版本,获取社区反馈。历经1.1、1.2、1.3版本后设计成型,最终在 Redis6.0 版本支持 RESP3 新协议。

2.2、类型:

相对于 RESP 协议。RESP3 在措辞使用上更准确,它摒弃了 RESP 令人困惑的措辞,使用更容易理解的类型名称。

RESP 版本2中的数据类型,在RESP3 中使用定义如下:

  • 简单字符串:节省空间的非二进制安全字符串。本文的 2.1.1 简单字符串类型。
  • 简单错误:节省空间的非二进制安全错误代码和消息。本文的2.1.2 简单错误类型。
  • Number:有符号的64位范围内的整数。本文的2.1.1 整数类型。
  • Blob字符串:二进制安全字符串。本文的 2.1.4 复合字符串类型。
  • 数组 : N个其他类型的有序集合。本文的2.1.5 数组类型。

RESP3 重新引入了 11 种数据类型:

  • Null :单个空值,替换RESP 中的 *-1( 即:*-1\r\n) 和 $-1 ( 即:$-1\r\n) 空值。
  • Double:浮点数。
  • 布尔值:真或假。
  • Blog错误信息:二进制安全错误代码和消息。
  • Verbatim字符串:二进制安全字符串,内容直接呈现给人类,无需任何转义或者过滤。例如 LATENCY DOCETOR Redis 中的输出。
  • Map:键值对的有序集合。键和值可以是任何其他的 RESP3 类型。
  • Set:N个其他类型的无需集合。
  • 属性:与Map类型类似,但客户端应忽略属性类型继续读取回复,并将其作为附加信息返回给客户端。
  • Push:带外数据。格式类似数组类型,但是客户端应该只是检查第一个字符串元素,说明带外数据的类型,如果一个为这种特定类型的推送信息注册的回调,则调用回调。推送类型与回复无关、因为它们是服务器在连接中随时可能推送的信息,因此客户端如果正在读取命令的恢复,则应该继续读取。
  • Hello:类似Map类型,但仅在客户端与服务端简历连接时发送,以便以服务器名称、版本等不同信息欢迎客户端。
  • Big Number:不能用Number整数类型表示的大整数。

2.2.1 简单类型:

简单类型顾名思义是用来表达单一数据内容的基本类型。

2.2.1.1 简单字符串:

参考 2.1.1、简单字符串类型(Simple String)

2.2.1.2 简单错误:

参考 2.1.2、错误响应数据类型(Errors) 

2.2.1.3 数字 (Number):

参考:2.1.3、整数类型

2.2.1.4 Blog 字符串:

参考:2.1.4、复合字符串(Bulk Stirngs) 

2.2.1.5 空值:

Null类型,只有一个值:空。该类型使用 _ ( 下划线 ) 作为类型标识符。其格式是:类型标识符+CRLF。

如:"_\r\n" 

注:CRLF,用于代表回车换行,即"\r\n",下文直接使用CRLF代替,不再使用”\r\n”这种方式。

2.2.1.6 Double类型:

浮点数类型,使用 , (逗号) 作为类型标识符。其格式是:类型标识符+浮点数+CRLF。

如:",1.23\r\n" 表示浮点数:1.23

此类型还可以用来表示正、负无穷。

",inf\r\n" 表示正无穷;",-inf\r\n" 表示负无穷。

2.2.1.7 布尔值:

布尔值只有真、假两个取值,在RESP3之前版本是通过整数类型取值 1 和0 来表示。RESP3 直接定义了布尔类型。该类型使用 # (井字符) 作为类型标识符。而且真、假两个取值都固定使用tf分别代替。这两个值分别是 true  false的首字符。

真值: "#t\r\n"

假值:"#f\r\n"。

2.2.1.8 Blog错误类型:

RESP3 之前的错误类型是二进制安全的。RESP3 提供一种二进制安全的错误类型,换言之可以通过此类型返回二进制安全的错误信息。该类型使用 ! (感叹号) 作为类型标识符,内容格式类型 Blog字符串类型,即:类型标识符+长度信息+CRLF+错误信息+CRLF。另外这种错误类型也使用第一个单词全大写作为错误代码。如"SYNTAX invalid syntax",使用该协议表示如下:

"!21\r\nSYNTAX invalid syntax\r\n"

2.2.1.9 Verbatim字符串:

该类型相对Blog字符串类似Blog字符串,但表现形式更丰富。Verbatim字符串类型使用 = (等于号) 作为类型标识符,并且字符串数据前三个字符必须是txt或者是mkd,分别表示当前字符串是纯文本还是markdown文本;然后紧跟着第四个字符必须是冒号。最后才是真正的数据内容。整个数据长度信息包含了表示字符串类型和冒号。其格式可表达为:类型标识符+长度信息+CRLF+文本表示前缀+冒号+数据信息+CRLF。

如:"=15\r\ntxt:Some string\r\n" 。这里的15包括了txt:4个字节和Some string 11个字节。

2.2.1.10 Big Number:

Big Number 大数值类型,表示非常大的数字,超出了Number能表示的64位数字的范围。

该类型使用 ( (左括号) 作为类型标识符。数据格式:类型标识符+大数字串+CRLF。

比如:(3492890328409238509324850943850943825024385\r\n。则表示数字:3492890328409238509324850943850943825024385

Big Number能表示整数或者负数,但是不能包含小数部分。换句话说,这仅仅是表示整数的。

2.2.2 复合类型:

到目前为止所描述的都是简单类型,它们只能用来表示单个项目信息。但是 RESP3 的核心思想的能够从类型角度和协议角度表示不同的语义含义。因此RESP在其定义的简单类型之上,定义了相关符合类型,用于表现更复杂的语义。

RESP3 的复合类型中的元素,即可以是简单类型,也可以是复合类型,还可以是简单和复合类型的综合体。

2.2.2.1 数组类型:

[参考:2.1.5 数组类型。 ]

2.2.2.2 Map类型:

Map类型,即键值对类型,表现形式也是数组,但是它使用 (百分号) 作为类型标识符。而且元素中的数量必须是偶数,表示一个个键值对。基本上也可以称之为字典数据结构或者是哈希。

其格式可以定义为:类型标识符+键值对数量+CRLF+<键数据+CRLF+值数据+CRLF>(0-N个)。

比如 JSON格式的字符串 :

{

"first":1,

"second": 2

}

在 RESP3 协议可以表示为:

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

2.2.2.3 Set类型:

Set类型和数组类型完全相同,用来表示无序集合。使用 ~ (波浪符) 作为类型标识符。

格式定义为:类型标识符+元素个数+CRLF+<元素内容+CRLF>(0-N)个。

比如:“~5\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n”。

标识Set中包含5个元素。第一、二是是简单字符串orange和apple。第三个是布尔值true,第四、第五个元素分别是整数100和999。

2.2.2.4 Attribute类型:

Attribute,即属性类型。该类型和Map类似。使用 | (竖立符) 作为类型标识符。属性类型描述字典的方式和Map类型相同,但是在解析该数据时,不应将此类字典数据视为回复的一部分,而是考虑用于扩充回复的辅助数据。。

属性数据可以出现在表示给定类型协议的有效部分之前的任何位置,并且只会通知紧随其后的回复部分,如:

*3\r\n:1\r\n:2\r\n|1\r\n+ttl:3600:3\r\n

数组的第三个元素具有关联的辅助信息{ttl:3600}.

2.2.2.5 Push数据类型:

Push 推送类型,这其实是一种交互模式,在此之前,所有的协议都是适用于请求-响应交互模式下的。而推送则是另外一种交互模式。客户单连接上服务器后,服务器可能主动的给客户端发送其未明确请求的数据。这种推送可以是同步,也可以是异步。

对于 Redis 来说,至少在 3 种推送数据的场景:

  • Pub/Sub 发布订阅模式,客户端接收已经发布的数据;
  • MONITOR 命令实时监听 Redis 执行的命令;
  • Master-Replica 主从复制。(虽然这种模式下,是副本主动发起的连接,但是在增量同步场景下,其实是主给副不断的推送消息)。

考虑到主从复制是属于内部协议,我们可以忽略它,在此仅仅是考虑前两种模式,即 Pub/Sub 和 MONITOR。这两种模式有一个共同的问题:

  • 处于推送模式下的连接,属于连接的私有状态。协议层面上,从Pub/Sub获取到的消息,我们无法将其与其他的回复响应区分开来。
  • 通过该方式设置的连接只能用于Pub/Sub或者MONITOR模式,因为没有其他方式能告知收到的消息是推送数据还是命令响应。

此外用户Pub/Sub的连接,也不能同时用于MONITOR或者是其他任何推送通知。因此 RESP3 引入显式推送数据类型,试图解决上述问题。

协议层面上,RESP3 推送数据类型表示方式类似数组类型。但是(右尖括号) 作为类型标识符,并且数组的第一元素始终是String项,表示服务器正在发送给客户端推送的数据类型。

Push 数组中的所有其他字段都的类型相关的,这意味着根据数据类型字符串作为第一个参数,其余项目将按照不同的约定进行解析。RESP2 原有的推送数据,在RESP3中由推送类型 pubsubmonitor表示。

如以下例子:

>4\r\n
+pubsub\r\n
+message\r\n
+somechannel\r\n
+this is the message\r\n

以上例子为了简单描述使用了简单字符串,实际上 Redis是会使用Blog字符串代替。上面的例子数据类型是pubsub。对于这种类型,如果下一个元素是 message 我们知道它是 Pub/Sub消息 (其他子类型可能是订阅、取消订阅等),然后是频道名称和消息本身。

推送数据可能与任何协议数据交错,但是始终位于顶层,因此客户端永远不会在Map回复中找到推送数据的。

在此模式下,可以同时获取回复和推送消息,以任何方式交错,但是命令及其回复的顺序是不受影响的:如果调用了命令,则下一个回复接收到的讲师该命令的相关的信息,以此类推,哪怕之前还存在待消费的推送数据。

例如:在一个 GET key命令后,可以获得以下两个有效回复:

>4\r\n
+pubsub\r\n
+message\r\n
+somechannel\r\n
+this is message\r\n
$9\r\n
Get-Reply\r\n

或者是相反的顺序:

$9\r\n
Get-Reply\r\n
>4\r\n
+pubsub\r\n
+message\r\n
+somechannel\r\n
+this is message\r\n

不管顺序如何,它们都是两条完整的消息,对于上层处理消息的逻辑来说,很容易甄别、解析。

2.2.2.5 Streamed 字符串:

目前来说所有 RESP 协议定义的字符串,都是有一个长度前缀。通过其长度,可以获取后续具体数据的确切长度。但不幸的是,这并不是总是最佳的。有时候我们需要将一个大字符串传输到客户端、或者事先压根不知道传输的字符串大小。当前Redis内部已经使用了这个特性,如无盘的主从拷贝,第一次同步的RDB,直接输出到socket中,在这种情况下,无法提前知道正在传输的字符串最终长度。这在RESP2协议下,是协议的私有扩展。而对于 RESP3 ,则希望成为规范的一部分,该功能目前并未用户客户端和服务器之间的交互,但是不排除将来会用到,尤其是对于模块来说。

因此 RESP3 对该种情况引入了流式字符串类型。

该类型和普通Blog字符串使用相同的 美元符前缀,但是因为不确切知道具体数据长度,因此使用 ? (问号) 代替。因此这种类型的数据,其类型标识符可以认为是:$? 。然后数据采取分块编码的方式进行传输,每一块数据都包含以下格式:分号+块数据长度+CRLF+块数据+CRLF。最后使用固定 ;0\r\n,即0长度的数据,表示传输结束。

2.2.2.6、Streamed 符合类型:

除了字符串可能为止长度大小外,其他的复合类型,也可能存在这种预先未知长度大小的情况出现,比如数组、Set和Map,等可能在某种情况下存在长度未知的情况,对于此 RESP3 扩展了机制,支持这种复合类型数据的流式传输。

流式传输和预知固定长度传输的区别是,原先表示具体长度信息的位置,统一使用 ? (问号) 代替。如

传输集合 则是: ~? ,传输数组则是:*?,然后根据具体传输的符合数据类型,逐一元素传输,最后使用统一的END类型结束符 : .\r\n

2.2.2.7、小结:

RESP3 相对RESP 表现形式更丰富、语义更精确。

你需要了解的Redis信息之:RESP/RESP3通讯协议

2.3、改进的HELLO命令:

四、总结:

如果说 RESP简单明了,那么RESP3 就是丰富多彩。即优化了RESP语义表达不准确的问题,也提升了 RESP表现力不够丰富的缺陷。RESP3 重新设计增加了 11种新的数据类型,用于支持现有,以及将来可能会出现的各种有用场景。虽然数据类型丰富了,但是 RESP3 整体上来说,设计理念还是没有改变,依然是人类可读,简单明了,高效率的设计宗旨。

RESP 作为 Redis 客户端服务器通讯协议,一般情况下,应用开发人员都不会直接和它打交道。大家基本上是直接具体编程语言环境下的客户端开发库。但了解底层协议,对于提升使用这类开发库效率,排查问题等有很大的好处;并且对于底层协议的掌握,也能举一反三,在将来的业务开发和程序设计过程中借鉴一二。