Redis协议学习

REDIS协议定义

redis客户端使用一种叫做RESP(REdis Serialization Protocol)的协议与REDIS服务器通信。RESP协议虽然是为redis设计的,但是也适用于其他C/S模型的软件

RESP设计时考虑了以下几件事情

  • 实现起来简单
  • 解析起来快速
  • 开发人员易阅读

RESP可以序列化不同类型的数据,像数字、字符串、数组以及一种专门表示错误的类型。客户端发往服务器的命令以及参数被处理成字符串数组,redis服务器通过特定类型的数据格式响应客户端。

RESP是二进制安全的且不需要将数据从一个进程传输到另一个进程因为它使用了一个表示长度的前缀来传输数据

注: 这里只讨论客户端-服务器之间的通信, redis集群使用了一种不同的二进制协议来实现节点间的数据交换。

网络层

客户端通过6379端口创建一条TCP连接,连接到redis服务器。RESP协议本身不是必须要使用TCP协议,但是在redis中,这个协议只用TCP的连接。

请求-响应模型

redis服务器接收不同参数组装成的命令, 一旦接收到一个命令,服务器将进行处理并向客户端发送一个回复。

这几乎是一个最简单的模型,但是有下面两个例外

  • redis 支持pipelining,因此客户端一次可以发送多条命令
  • 当redis客户端订阅了一个channel,协议会变成一个推送协议,这意味着客户端不再需要发送命令,因为一旦服务器收到client订阅的channel上的新消息就会自动发送给客户端。

除了以上两个例外,redis协议是一个简单的请求-响应模型。

RESP协议描述

RESP协议在redis 1.2版本引入,但是直到redis 2.0版本才成为和redis 服务器通信的标准。

RESP协议实际上是一种序列化协议支持以下几种数据类型:简单字符串,错误信息,整形数字,多行字符串,数组。

redis中RESP协议的使用方式如下:

  • 客户端发送至服务器的命令为 多行字符串数组。
  • 服务器根据命令的实现回复RESP数据类型中的一种。

RESP协议中,数据的类型依赖第一个字节:

  • 简单字符串:第一个字节是+
  • 错误信息:第一个字节是-
  • 整形数字:第一个字节是:
  • 多行字符串:第一个字节是$
  • 数组:第一个字节是*

RESP可以通过多行字符串或者数组的变体来表示一个NULL值 RESP协议每个部分以固定的'\r\n'(CRLF)结尾

简单字符串 RESP Simple Strings

简单字符串通过这种方式编码:+开头,跟着不包含\r和\n的字符串(不允许新的行),以\r\n结尾。

简单字符串用最小的开销来传输非二进制安全的字符串,例如redis服务器回复’OK',用简单字符串只需要编码为下面5个字节的内容

"+OK\r\n"

如果需要发送二进制安全的字符串,可以用多行字符串替代。

当redis回复一个简单字符串时,客户端应该返回+号后面第一个字节开始到字符串最后但不包括最后的CRLF

错误信息RESP Error

redis有专门针对错误的数据类型,实际上错误信息和RESP的简单字符串十分相像,但是第一个字节是'-‘号。两者真正的区别是客户端对RESP中的错误信息认为是异常,错误信息类型的字符串描述的是一个错误的信息。基本的格式如下:

"-Error message\r\n"

错误回复只会在某些错误的情况下发生,比如客户端试图操作错误的数据类型,或者命令不存在等等。客户端在接收到错误信息回复时应该抛出一个异常。 下面是错误信息回复的例子

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

‘-‘后面第一个单词(直到第一个空格或者换行)代表服务器返回的错误类型。这个只是redis使用时的约定,不是RESP错误信息格式的一部分。

例如,ERR是一个宽泛的错误而WRONGTYPE则是一个更加明确的错误用来表示客户端试图在一个错误的数据类型上执行命令。这种被称为“错误前缀”的方式可以使客户端不用关心服务器返回的具体错误信息内容就能理解错误的类型,而错误信息的内容可能会随着时间变化。

客户端实现时可以针对不同的错误类型返回不同的异常,也可以直接返回错误信息给调用方。

但是这个特性不是特别重要因为它很少有用,甚至客户端可以简单的返回一个一般的错误条件,例如false。

整数 RESP Integers

这个类型只是一个CRLF结尾的字符串表示一个整数,第一个字节是’:',举个例子,":0\r\n" 或者":1000\r\n" 都表示一个整数的响应。

很多redis命令像INCR,LLEN,LASTSAVE等都返回整型数字。

服务器返回的整数没有特殊的含义,INCR返回的只是一个自增后的数字,LASTSAVE返回的只是一个UNIX时间戳,但是返回的这个整数可以保证在64位的范围内。

整数也被广泛的用作true或false返回,例如EXISTS或SISMEMBER会返回1来表示true, 返回0表示false。

其他命令像ADD,SREM,SETNX也会返回1表示命令的确执行了,否则返回0

下面这些命令会返回整数响应: SETNX, DEL, EXISTS, INCR, INCRBY, DECR, DECRBY, DBSIZE, LASTSAVE, RENAMENX, MOVE, LLEN, SADD, SREM, SISMEMBER, SCARD

多行字符串 RESP Bulk Strings

多行字符串用来表示最大为512M的二进制安全的字符串,它通过下面的方式编码:

  • $加上字符串的字节数(长度前缀)以及CRLF结尾
  • 实际的字符串数据
  • 结尾CRLF

因此字符串"foobar"将被编码成如下:

"$6\r\nfoobar\r\n"

一个空字符串:

"$0\r\n\r\n"

RESP 多行字符串还可以用一种特殊的格式来表示Null,这个特殊的格式字符串的长度是-1,并且没有数据段,因此Null表示成这样:

"$-1\r\n"

这被称为Null Bulk String. 当服务器返回了Null Bulk String时,客户端的库函数不应该返回一个空字符串,而应该返回一个空对象。例如 Ruby的库应该返回’nil’,而C语言的库应该返回NULL(或者在返回的对象中设置一个特殊的标志位)

数组 RESP Arrays

客户端使用RESP 数组向redis服务器发送命令。类似的特定的redis命令也会使用RESP数组类型将结果返回给客户端。LRANGE命令就是一个例子。 RESP 数组使用下面的格式发送:

  • ‘*‘加上一个十进制的数字(数组中元素的数量)以及CRLF结尾
  • 数组中每个元素都是一个的RESP类型

因此一个空数组表示为

"*0\r\n"

两个多行字符串"foo" 和"bar" 组成的数组将被编码成:

"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

可以看到*CRLF是数组的前缀部分,剩下的数据部分由一个接一个的RESP 类型的数据组成,举个例子,一个包含了三个整数的数组编码如下:

"*3\r\n:1\r\n:2\r\n:3\r\n"

数组里可以包含不同的类型,例如下面一个数组包含了4个整数和一个多行字符串:

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

(为了看得清晰,结果被分割成了多行)

同样的,也存在Null数组的概念,同时也可以认为表示Null(通常使用Null Bulk String, 但是因为历史原因,两种都可以)

例如,当BLPOP命令超时后,服务器会返回一个Null数组:

"*-1\r\n"

客户端函数应该返回一个空的对象而不是一个空的数组。这是必须的,因为要区分一个空的数组和一个不同的条件(例如BLPOP命令的超时条件)

在RESP中数组也可以包含数组,下面给了一数组中包含两个数组的编码示例

*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Foo\r\n
-Bar\r\n

(方便阅读,结果被分割成了多行)

上面的RESP数据类型被编码成一个包含两个元素的数组,第一个元素是一个包含3个整数1,2,3的数组,第二个元素是一个包含了简单字符串和错误信息的数组。

数组中的Null元素

数组中单个的元素也可以是Null, Redis的响应中使用它来表示元素的缺失而不是空字符串。在使用SORT命令时GET模式匹配选项没有特定的key匹配时可能会出现。一个包含了Null元素的数组回复如下:

*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

第二个元素是Null, 客户端的库应该返回类似如下的内容:

["foo",nil,"bar"]

注意:这不是之前所说的异常,而是一个进一步阐释协议的例子。

给Redis服务器发送命令

客户端和服务器之间的交互:

  • 客户端发送给服务器一个由Bulk Strings组成的RESP 数组
  • 服务器响应任一合法的RESP数据类型给客户端

举一个典型的交互例子:客户端发送命令 LLEN mylist 来获取名为mylist的列表的长度,服务器服务器返回一个整数类型的响应(下面例子中C代表客户端,S代表服务器)。

C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n

S: :48293\r\n

和之前一样为了简化,我们按行分割了协议的不同部分,实际的交互过程中客户端把" *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n"作为一个整体发送给服务器。

动手实践

使用python socket实现了一个redis协议的解析器,效果如下:

[aidu35@aidu35 py]$ python redis_cli.py 
redis-cli> SET name gerrard EX 10
OK
redis-cli> HGETALL name
WRONGTYPE Operation against a key holding the wrong kind of value
redis-cli> TTL name
10
redis-cli> GET name
gerrard
redis-cli> KEYS *
website
苏州
weather
name

代码放在github上了 redis-parser

参考

Redis Serialization Protocol


wechat
微信扫一扫,订阅我的博客动态^_^