网络通信离不开各种各样的协议,著名的tcp,http等协议构建了我们常见的web应用。http协议是基于tcp的应用层协议。同样的,redis的协议也是基于tcp的应用层协议,即RESP(REdis Serialization Protocol)。
RESP设计巧妙,它的追求在于下面三个方面:
- 易于实现
- 解析高效
- 易于人读
协议基础
RESP协议规定,客户端通过tcp网络连接到redis服务器。通信过程类似一问一答的方式,与HTTP类似,客户端发送命令请求到服务器,服务器执行时候返回结果。
客户端和服务器发送的命令或数据一律以 \r\n
(CRLF)结尾。正式因为CRLF的存在,让redis的解析十分方便和可读。
请求协议
我们知道,redis数据库操作的命令无非就三种,第一中是不带参数的命令,例如PING
, FLUSHDB
;其次是带有参数的读取命令,例如GET key
,LLEN queue
;还有带有参数并设置值的写操作,例如 SET hello world
,CONFIG SET TIMEOUT 30
。
命令和参数之间,通常都是用空格隔开。RESP协议规定,这样的请求中,命令和各参数之间必须使用CRLF分割,同时还必须提供命令头标签,即token。字节串的第一个字符到CRLF的内容即为头标签。
参数中的字符串和数字
字串
一个redis数据库操作包含命令+参数
。当然有的命令参数可以省略。无论编码命令还是参数,其编码方式都是一样的。
命令中,无论请求和返回的结果,无怪乎就只有字符串和数字。编码字符串的格式如下:
$ + string_length + string + CRLF
,即以$
符号开头,然后跟字节串的长度,然后跟着一个CRLF,再然后就是字节串本身,最后以一个CRLF结尾。
例如 hello
编码成 $5\r\nhello\r\n
, 21.7
编码成$3\r\n21.7\r\n
。其中$5\r\n
和$3\r\n
都是Token。
数字
数字类型和字符串类似,不同的在于数字使用:
开头,同时不需要说明数字的长度。
: + number + \r\n
,即:
开头,然后跟着数字,最后以CRLF结尾即可。
注意,redis中绝大多数参数都是字串,只有返回响应的时候会处理数字类型,例如incr
操作。其他情况下,请求的参数,还是返回响
应中的数字,其实都是数字字符串。例如下面是一个集合,返回元素的时候,里面的元素都是字串
127.0.0.1:6379> sadd set hello
(integer) 1
127.0.0.1:6379> sadd set 世界
(integer) 1
127.0.0.1:6379> sadd set 1
(integer) 1
127.0.0.1:6379> sadd set 21.7
(integer) 1
127.0.0.1:6379> smembers set
1) "1"
2) "21.7"
3) "\xe4\xb8\x96\xe7\x95\x8c"
4) "hello"
sadd的set的参数中,1
,和21.7
都是字串类型,返回自然是字串,而返回表示sadd成功的则是数字类型。所有请求的命令都是字串类型,哪怕写的是数字字面量,redis最终还是当成字串处理。
编码命令
上面我们了解单独的命令和参数的编码方式。下面介绍命令+参数的组合编码方式。
组合编码也很简单,无非就是将编码的命令(字符串)拼接起来,同时再最开始追加一个* + 组合命令或参数的个数
的方式:
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
例如 PING
编码字符串的方式为$4\r\nPING\r\n
,组合的编码为*1\r\n$4\r\nPING\r\n
。即
*1
$4
PING
GET hello
编码为*2\r\n$3\r\nGET\r\n$5\r\nhello\r\n
*2
$3
GET
$5
hello
GET
和hello
两个字符串分别编码,然后组合起来,编码了两个参数,因此*
后面跟着是2。
再看一个例子,SET 中国 21.7
的将编码成*3\r\n$3\r\nSET\r\n$6\r\n\xe4\xb8\xad\xe5\x9b\xbd\r\n$4\r\n21.7\r\n
*3
$3
SET
$6
\xe4\xb8\xad\xe5\x9b\xbd
$4
21.7
汉字中国
的按照utf-8
编码的方式编码成bytes为*\xe4\xb8\xad\xe5\x9b\xbd
,一个汉字三个字节,因此中国
的长度是6。
python3的字串原生支持
unicode
,计算机操作字串的时候是unicode,当需要网络传输和写入文件的时候,都必须把unicode的字串编码成bytes结构。同理,当从网络或者文件中读取数据的时候,也需要解码成unicode。目前国际通用的编码方式以utf-8
为主。
值得注意的是,将命令参数组合的编码方式,不仅是请求的时候如此,在redis的响应回复中,其中的多批量回复(multi-bulk reply)也是采用了同样的编码方式返回。
无论单独编码数字,字串还是命令组合,我们都把开头到第一个CRLF
的内容看成头标签(Token
)。
响应协议
既然有请求的协议规定,当然也有响应回复的协议。请求的命令已经介绍了三种,其差别就在于参数的个数。对于响应的回复,则会比请求的情况更多。
RESP协议规定,所有答复的第一个字节规定了响应答复的类型,后除了类型之外,和请求中编码字符串和数字的类似。响应大概类型如下:
- 状态回复(status reply)的第一个字节是
+
- 错误回复(error reply)的第一个字节是
-
- 整数回复(integer reply)的第一个字节是
:
- 批量回复(bulk reply)的第一个字节是
$
- 多条批量回复(multi bulk reply)的第一个字节是
*
状态回复
对于客户端命令,执行一个查询或者一个写入操作,通常会返回操作的是否成功的状态判定。状态回复是单行回复。例如上面ping命令的返回就是ok。
ok的状态的编码返回为+OK\r\n
,虽然OK
也是字串,但是在状态中返回,我们只需要知道状态结果,不需要显示的告诉返回多少字符,后面可以知道,字符串编码的时候需要标记字符数,纯粹是为了读取socket的时候,确定分包边界。由于状态回复只是单行字串,因此最后一个CRLF就能确定包的分界。
错误回复
与状态回复类似,错误回复的第一个字符是-
,然后跟着错误的类型。错误类型之后以一个空格结束,然后就是错误的信息(msg),错误信息是一段文字,可以包含空格,错误信息结束之后也依然是一个CRLF表示回复结束。
例如命令不存在的时候会返回错误:
127.0.0.1:6379> hello
(error) ERR unknown command 'hello'
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range
返回的错误编码回复依次是-ERR unknow command 'hello\r\n'
和-ERR value is not an integer or out of range\r\n
整数回复
所谓整数回复,即返回数字类型的回复。整数回复以:
开头,其后跟着数字,最后以CRLF结尾。这和之前介绍的数字编码的方式一样。
例如:0\r\n
和 :1000\r\n
都是整数回复。什么时候返回数字类型呢?一般是incr数字操作和一些返回数字表示逻辑的操作,例如DEL EXISTS等。
127.0.0.1:6379> INCRBYFLOAT float 1.2
"1.2"
127.0.0.1:6379> INCRBYFLOAT float 1.2
"2.4"
127.0.0.1:6379> INCRBY integer 2
(integer) 2
127.0.0.1:6379> INCRBY integer 2
(integer) 4
127.0.0.1:6379> DEL integer
(integer) 1
INCRBY 和 INCRBYFLOAT都是自增参数的数字,前者返回的是数字类型,后者返回的是字符串类型。也就是我们接下来介绍的批量回复。
批量回复
所谓的批量回复,只redis返回的二进制安全的字串。很大查询返回都属于批量回复。例如上面的例子中,INCRBYFLOAT的返回即使批量回复。
批量回复以$
符号开头,然后跟着字符串的长度值,然后就是CRLF和实际的字串数据,最后以CRLF结束。和字符串的编码一模一样。
例如查询命令 get hello
,返回结果为world
的情况下,批量回复的原始数据编码为$5\r\nworld\r\n
。
当请求的key的value不存在的时候,将会返回-1
用于表示长度的值。例如get non-existing-key
将收到$-1\r\n
的返回。对于这种空回复,客户端语言就可以灵活处理,比较推荐的做法就是使用编程语言表示对象不存在的值,例如python的None
,Golang的nil
。
多批量回复
常见的返回是批量回复,此外还有一个多批量回复,顾名思义,就是多个批量回复的组合。操作序列或者集合类的数据结构,就会返回多批量回复。例如上面的smembers set
命令的返回将会是:
*4\r\n$1\r\n1\r\n$3\r\n21.7\r\n$6\r\n\xe4\xb8\x96\xe7\x95\x8c\r\n$5\r\nhello\r\n
这和我们编码请求的方式一模一样。由此可见,RESP的设计非常优雅。
多批量回复是批量回复的组合,那么也会有返回-1
的情况。此外,当读取一个集合,如果集合没有元素,则不是返回nil,而是返回空集合的结果,接多批量回复的结果为*0\r\n
。
官网的还给出了一个例子关于多批量回复的空元素。例如下面的多重批量回复:
*3
$3
foo
$-1
$3
bar
其中, 回复中的第二个元素为空。因此最终转换成python的对象应该是这样的:["foo", None, "bar"]
。
总结
RESP的协议基于tcp的应用层协议,主要用到了字符和数字的编码和CRLF的组合编码方式。我们认识到字串的编码和数字编码的方式。请求的时候使用类似多批量回复方式编码,回复的响应有多种,可以根据第一个字节做不同情况的处理,比如状态回复,错误回复,批量回复和多批量回复。
对于多批量回复,空和无穷的方式也需要考虑,前者会返回一个0表示空,后者会返回-1表示无穷大(block情况也类似)。
了解了协议的编码方式,下一步,我们就使用代码实现这个协议的编码和解码过程。