前提#
最近学习Netty
的时候想做一个基于Redis
服务协议的编码解码模块,过程中顺便阅读了Redis
服务序列化协议RESP
,结合自己的理解对文档进行了翻译并且简单实现了RESP
基于Java
语言的解析。编写本文的使用使用的JDK
版本为[8+]
。
RESP简介#
Redis
客户端与Redis
服务端基于一个称作RESP
的协议进行通信,RESP
全称为Redis Serialization Protocol
,也就是Redis
序列化协议。虽然RESP
为Redis
设计,但是它也可以应用在其他客户端-服务端(Client-Server
)的软件项目中。RESP
在设计的时候折中考虑了如下几点:
- 易于实现。
- 快速解析。
- 可读性高。
RESP
可以序列化不同的数据类型,如整型、字符串、数组还有一种特殊的Error
类型。需要执行的Redis
命令会封装为类似于字符串数组的请求然后通过Redis
客户端发送到Redis
服务端。Redis
服务端会基于特定的命令类型选择对应的一种数据类型进行回复(这一句是意译,原文是:Redis replies with a command-specific data type
)。
RESP
是二进制安全的(binary-safe
),并且在RESP
下不需要处理从一个进程传输到另一个进程的批量数据,因为它使用了前缀长度(prefixed-length
,后面会分析,就是在每个数据块的前缀已经定义好数据块的个数,类似于Netty
里面的定长编码解码)来传输批量数据。
注意:此处概述的协议仅仅使用在客户端-服务端通信,Redis Cluster
使用不同的二进制协议在多个节点之间交换消息(也就是Redis
集群中的节点之间并不使用RESP
通信)。
网络层#
Redis
客户端通过创建一个在6379
端口的TCP
连接,连接到Redis
服务端。
虽然RESP
在底层通信协议技术上是非TCP
特定的,但在Redis
的上下文中,RESP
仅用于TCP
连接(或类似的面向流的连接,如Unix
套接字)。
请求-响应模型#
Redis
服务端接收由不同参数组成的命令,接收到命令并将其处理之后会把回复发送回Redis
客户端。这是最简单的模型,但是有两种例外的情况:
Redis
支持管道(Pipelining
,流水线,多数情况下习惯称为管道)操作。使用管道的情况下,Redis
客户端可以一次发送多个命令,然后等待一次性的回复(文中的回复是replies
,理解为Redis
服务端会一次性返回一个批量回复结果)。- 当
Redis
客户端订阅Pub/Sub
信道时,该协议会更改语义并成为推送协议(push protocol
),也就是说,客户端不再需要发送命令,因为Redis
服务端将自动向客户端(订阅了改信道的客户端)发送新消息(这里的意思是:在订阅/发布模式下,消息是由Redis
服务端主动推送给订阅了特定信道的Redis
客户端)。
除了上述两个特例之外,Redis
协议是一种简单的请求-响应协议。
RESP支持的数据类型#
RESP
在Redis 1.2
中引入,在Redis 2.0
,RESP
正式成为与Redis
服务端通信的标准方案。也就是如果需要编写Redis
客户端,你就必须在客户端中实现此协议。
RESP
本质上是一种序列化协议,它支持的数据类型如下:单行字符串、错误消息、整型数字、定长字符串和RESP
数组。
RESP
在Redis
中用作请求-响应协议的方式如下:
Redis
客户端将命令封装为RESP
的数组类型(数组元素都是定长字符串类型,注意这一点,很重要)发送到Redis
服务器。Redis
服务端根据命令实现选择对应的RESP
数据类型之一进行回复。
在RESP
中,数据类型取决于数据报的第一个字节:
- 单行字符串的第一个字节为
+
。 - 错误消息的第一个字节为
-
。 - 整型数字的第一个字节为
:
。 - 定长字符串的第一个字节为
$
。 RESP
数组的第一个字节为*
。
另外,在RESP
中可以使用定长字符串或者数组的特殊变体来表示Null
值,后面会提及。在RESP
中,协议的不同部分始终以\r\n
(CRLF
)终止。
目前RESP
中5种数据类型的小结如下:
下面的小节是对每种数据类型的更细致的分析。
RESP简单字符串-Simple String#
简单字符串的编码方式如下:
- (1)第一个字节为
+
。 - (2)紧接着的是一个不能包含
CR
或者LF
字符的字符串。 - (3)以
CRLF
终止。
简单字符串能够保证在最小开销的前提下传输非二进制安全的字符串。例如很多Redis
命令执行成功后服务端需要回复OK
字符串,此时通过简单字符串编码为5字节的数据报如下:
+OK\r\n
如果需要发送二进制安全的字符串,那么需要使用定长字符串。
当Redis
服务端用简单字符串响应时,Redis
客户端库应该向调用者返回一个字符串,该响应到调用者的字符串由+
之后直到字符串内容末尾的字符组成(其实就是上面提到的第(2)部分的内容),不包括最后的CRLF
字节。
RESP错误消息-Error#
错误消息类型是RESP
特定的数据类型。实际上,错误消息类型和简单字符串类型基本一致,只是其第一个字节为-
。错误消息类型跟简单字符串类型的最大区别是:错误消息作为Redis
服务端响应的时候,对于客户端而言应该感知为异常,而错误消息中的字符串内容应该感知为Redis
服务端返回的错误信息。错误消息的编码方式如下:
- (1)第一个字节为
-
。 - (2)紧接着的是一个不能包含
CR
或者LF
字符的字符串。 - (3)以
CRLF
终止。
一个简单的例子如下:
-Error message\r\n
Redis
服务端只有在真正发生错误或者感知错误的时候才会回复错误消息,例如尝试对错误的数据类型执行操作或者命令不存在等等。Redis
客户端接收到错误消息的时候,应该触发异常(一般情况就是直接抛出异常,可以根据错误消息的内容进行异常分类)。下面是错误消息响应的一些例子:
-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value
-
之后的第一个单词到第一个空格或换行符之间的内容,代表返回的错误类型。这只是Redis
使用的约定,不是RESP
错误消息格式的一部分。
例如,ERR
是通用错误,WRONGTYPE
则是更具体的错误,表示客户端试图针对错误的数据类型执行操作。这种定义方式称为错误前缀,是一种使客户端能够理解服务器返回的错误类型的方法,而不必依赖于所给出的确切消息定义,该消息可能会随时间而变化。
客户端实现可以针对不同的错误类型返回不同种类的异常,或者可以通过将错误类型的名称作为字符串直接提供给调用方来提供捕获错误的通用方法。
但是,不应该将错误消息分类处理的功能视为至关重要的功能,因为它作用并不巨大,并且有些的客户端实现可能会简单地返回特定值去屏蔽错误消息作为通用的异常处理,例如直接返回false
。
RESP整型数字-Integer#
整型数字的编码方式如下:
- (1)第一个字节为
:
。 - (2)紧接着的是一个不能包含