七的博客

Redis客户端跟服务端通信协议

网络通信

Redis客户端跟服务端通信协议

在学习或者编写 Redis 客户端库的代码时,对 Redis 客户端跟服务端使用的通信协议必须要有一定的了解。这样才能看懂报文的结构,并解析出报文的意思。

Redis 使用 RESP 协议跟 Redis 服务端通信,全程是 Redis Serialization Protocol 。 这个协议虽然是专门给 Redis 设计,但是也可以用作客户端-服务端的通信。

在 Redis 早期版本中,是没有这个协议的,也就是一开始的时候不能自己去定制 Redis 的客户端。这个协议的特点就是:

  • 实现起来比较简单
  • 解析起来比较方便快速,开销小。
  • 报文对人可读性比较强,这样调试起来更方便。

从上面几点来看,因为这种协议本身设计的比较简单,所以比起纯二进制协议的话通信效率上稍微就会低一点。而且因为不支持数据压缩,在传输大量重复数据的时候效率也不高。 但是在大多数情况下,网络延迟跟 Redis 服务器本身的处理速度才会是瓶颈,协议本身不会是瓶颈。

Redis 集群之间的通信不使用这个协议,而是使用了其他的二进制协议。

1. 协议特点

RESP 协议支持序列化不同的数据类型,比如整数、字符串、数组等等。 也有专门用于识别错误的类型。

请求是以字符串数组的形式从客户端发送到 Redis 的服务器,Redis 会以特定命令的数据类型进行回复。

RESP 是二进制安全的。这意味着协议可以处理任何类型的数据,包括包含空字节的二进制数据,而不会被误解为是一个字符串终止符。

在传输非常多的数据的时候不需要对数据进行处理,因为它使用前缀长度来传输大量数据。 这种方式可以让接收方能够准确地根据这个长度字段来读取数据,不需要通过特殊的终止符来确定数据的边界。

2. 协议传输

RESP 是使用 TCP 协议去通信的,TCP 协议是一种可靠的、面向连接的协议,可以确保数据按顺序无错地传输。

所以可以直接利用一些 TCP 调试工具,连接 Redis 的 6379 端口,然后发起调试命令就可以观察到应答的结果。

如果你是 Linux 用户,可以直接使用 Netcat 这个工具,在命令行中进行调试。

# 使用 nc 命令连接本地的6379端口
nc localhost 6379

# 然后输入 PING
PING
+PONG

# 就可以观察到 Redis 服务端返回的 +PONG 应答

或者更简单点,直接在命令行中使用 telnet 工具也可以:

$ telnet localhost 6379
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


# 然后输入 PING
PING
+PONG

3. 协议描述

RESP 实际上是一种序列化协议,支持以下数据类型:简单字符串、错误、整数、批量字符串和数组。

在 RESP 中,大部分数据的类型取决于第一个字节:

  • 对于简单字符串类型,回复的第一个字节是 “+“,后面再跟上具体的字符串内容和 “\r\n”
  +OK\r\n
  • 对于错误信息,回复的第一个字节是 “-” , 后面再跟上错误消息和 “\r\n”
  -ERR unknown command 'foobar'\r\n
  • 对于整数类型,回复的第一个字节是 “:” ,后面再跟上整数值和 “\r\n”
  :1000\r\n
  • 对于批量字符串类型,回复的第一个字节是 “$” , 后面再跟上字符串长度。 然后是 “\r\n”,接着是实际字符串和 “\r\n”。
  $5\r\nhello\r\n
  • 对于数组类型,回复的第一个字节是 “*” , 后面再跟上数组元素数量,然后是 “\r\n”,接着是每个元素的序列化形式。
  *2\r\n$5\r\nhello\r\n$5\r\nworld\r\n

上面每一个类型后面都会以 “\r\n” (CRLF) 结束,CRLF 代表的是回车换行,是许多文本协议用来表示行结束的标准方式。如果缺少这个的话,那么客户端解析应答的时候不知道什么时候这条消息结束。

3.1 简单字符串

简单字符串的编码方式如下: 一个加号字符,后跟一个不能包含 CR 或 LF 字符 (不允许换行) 的字符串,以 CRLF (即 “\r\n”) 结束。

比如许多 Redis 命令在成功时只回复 “OK” , 作为 RESP 简单字符串 , 它被编码为以下 5 个字节:

+OK\r\n

3.2 错误信息

错误信息与 RESP 简单字符串完全是一样的 ,只不过第一个字符是减号 ‘-’ 而不是加号。RESP 中简单字符串和错误的真正区别在于,客户端将错误视为异常,字符串内容就是错误消息本身。

-Error this is a message\r\n

’-’ 后的第一个单词,一直到第一个空格或换行符,表示返回的错误类型。这只是 Redis 使用的惯例,不是 RESP 错误格式的一部分。

这个就有点像编程语言中的异常类型一样,比如 Java 中你可以去区分不同的业务异常,或者是运行时异常等做不同的处理。 如果要是根据文本消息去判断异常的类型很容易出错,针对不同的错误类型做不同的区分处理可以提高程序的稳定性。

3.3 整数

整数就是以 “:” 字节为前缀。许多 Redis 命令返回 RESP 整数 , 比如 INCR、LLEN 和 LASTSAVE。这些命令返回的整数值也没有特殊含义 , 就是 INCR 的自增数值等,不用单独判断处理。

:0\r\n
:1000\r\n

整数回复也常用于返回真或假。比如 EXISTS 或 SISMEMBER 命令将为真就会返回 1,为假就会返回 0。用整数来表示布尔值也是一种常见做法,特别是在不支持专门布尔类型的系统中。比如像 C 语言里面就没有布尔类型一说,通常都是用 0 表示 false,非 0 就表示 true。

其他命令如 SADD、SREM 和 SETNX,如果操作实际执行了,就会将返回 1,否则就会返回 0。

以下命令将回复整数回复: SETNX、DEL、EXISTS、INCR、INCRBY、DECR、DECRBY、DBSIZE、LASTSAVE、RENAMENX、MOVE、LLEN、SADD、SREM、SISMEMBER、SCARD。

3.4 批量字符串

批量字符串可以用来表示长度最多为 512 MB 的单个二进制安全字符串。

批量字符串的编码方式是: 一个 “$” 字节,后跟组成字符串的字节数 (前缀长度),然后以 CRLF 结束。

比如字符串 “foobar” 编码如下:

$6\r\nfoobar\r\n

空字符串:

$0\r\n\r\n

还可以用特殊格式表示不存在的值,这种格式用于表示空值。在这种特殊格式中,长度为 -1,并且没有数据。

$-1\r\n

客户端收到这种表示空值的报文的时候,应该给调用方返回编程语言的空对象。比如 C语言返回 NULL ,Java 语言返回 null 等。

3.5 数组

客户端可以使用 RESP 数组向 Redis 服务器发送命令。某些 Redis 命令在向客户端返回元素集合时也会使用 RESP 数组作为回复类型。

格式如下:

  • 一个 “*” 字符作为第一个字节,后面跟上数组中元素的数量作为十进制数,然后跟上换行符 CRLF。
  • 数组中每个元素的都需要加上 RESP 类型,这样才可以识别数组中元素的类型。

比如空数组:

*0\r\n

两个 RESP 批量字符串 “foo” 和 “bar” 的数组编码如下:

*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
  • 【*2\r\n】*2 表示是一个有2个元素的数组,”\r\n” 是行结束符。
  • \(3\r\nfoo\r\n】\)3表示是一个长度为 3 的批量字符串,”\r\n” 行结束符,”foo” 是实际的字符串内容,”\r\n” 再次用作结束符。
  • 【$3\r\nbar\r\n】这是第二个数组元素,格式同上,表示的是字符串 “bar”

包含三个整数的例子:

*3\r\n:1\r\n:2\r\n:3\r\n
  • 【*3\r\n】 表示3个元素的数组,”\r\n” 是行结束符。
  • 【:1\r\n】”:” 表示这是一个整数类型,”1” 是整数值,”\r\n” 是行结束符。
  • 【:2\r\n】”:” 表示这是一个整数类型,”2” 是整数值,”\r\n” 是行结束符。
  • 【:3\r\n】”:” 表示这是一个整数类型,”3” 是整数值,”\r\n” 是行结束符。

数组也可以混合类型,不用是一样的类型。下面是一个包含四个整数和一个批量字符串的例子 [1, 2, 3, 4, “foobar”]:

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

上面的回复被分成多行是为了更好分析报文,不然全部在一行看着晕。

  • 【*5\r\n】 * 表示是一个数组,长度为5 ,”\r\n” 是行结束符。

  • 【:1\r\n】 :表示整数类型,数值为1。

  • 【:2\r\n】 :表示整数类型,数值为2。

  • 【:3\r\n】 :表示整数类型,数值为3。

  • 【:4\r\n】 :表示整数类型,数值为4。

  • \(6\r\nfoobar\r\n】 "\)” 表示这是一个批量字符串。”6” 表示字符串长度为6。”\r\n” 行结束符。”foobar” 是实际的字符串内容。”\r\n” 是字符串的结束符。

空数组的概念也是存在的。比如 BLPOP 命令执行超时时,它返回一个计数为 -1 的空数组如下:

*-1\r\n

使用 -1 作为数组长度来表示空值是跟批量字符串中的表示方法是一样的设计思路。

也可以返回数组嵌套数组:

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

一样的为了更好阅读手动拆分成多行。

  • 【*2\r\n】 顶层数组,包含2个元素,这里2个元素是2个子数组。
  • 【*3\r\n】 第一个子数组,包含3个子元素。
  • 【:1\r\n】 第一个子数组的第一个元素,整数值 1。
  • 【:2\r\n】 第一个子数组的第二个元素,整数值 2。
  • 【:3\r\n】 第一个子数组的第三个元素,整数值 3。
  • 【*2\r\n】 第二个子数组,包含2个子元素。
  • 【+Foo\r\n】 第二个子数组的第一个元素,简单字符串”Foo”。
  • 【-Bar\r\n】 第二个子数组的第二个元素,错误消息”Bar”。

RESP 协议大概就上面这些内容,协议总体上来说还是比较简单的,稍微学习下对后续分析 Redis 客户端的代码是有非常大的帮助的。

参考链接