redis学习--redis客户端协议(RESP协议)

    redis客户端与服务端通信,使用RESP(REdis Serialization Protocal,redis序列化协议)协议通信,该协议是专门为redis设计的通信协议,但也可以用于其它客户端-服务器通信的场景。RESP协议的设计初衷如下:

  • 实现简单;
  • 快速解析;
  • 可阅读;

    RESP可以用于序列化不同的数据类型,如:整型、字符串、数组...并且为错误提供专门的类型;客户端发送请求时,以字符串数组的作为待执行命令的参数。redis服务器根据不同的命令返回不同的数据类型。

    RESP是二进制安全协议,并且处理批量数据无须逐个请求处理,因为批量数据传输时,在请求参数中添加了数据长度作为前缀。传输层基于TCP协议,默认端口为6739。

注:RESP协议仅用作redis客户端和服务端之间通信。redis集群节点之间使用另一种二进制协议进行数据交换。

请求-响应模型

    redis客户端请求都是基于"命令 + 参数"形式的,对数据的CRUD操作以及对redis节点的控制操作都被抽象为命令。每个命令根据需要可以包含0到多个参数。比如:info命令用于查看当前redis服务器节点的运行状况,不需要参数;而get命令用于获取指定key对应的value值,因而需要指定一个key作为参数;mget用于同时获取多个key对应value值,则可以指定多个key作为参数;
大部分redis请求都是基于"请求-应答"方式的,但存在如下两种例外情况:
1)Pipeline支持:redis支持pipeline方式进行数据请求,整个请求过程类似一个流水线,客户端可以一次发送多个命令给服务端,然后异步等待结果返回,这样客户端可以自行决定一个合适的时机接收返回结果。通过pipeline的方式,相比于逐个发送命令同步处理,效率获得了极大的提升。
2)pub/sub支持:redis支持pub/sub模式的消息处理,允许客户端订阅服务端的一个channel。当有特定消息产生时,服务端会推送消息给对应channel的客户端,而客户端无需主动发送命令获取消息。此时,协议的语义发生了改变,由"请求-应答"方式变为了"订阅-发布"方式。

RESP协议描述

    RESP协议支持5种数据类型:简单字符串(Simple Strings)、错误数据(Errors)、整数(Integers)、批量字符串(Bulk Strings)、数组(Arrays);客户端请求服务器时,会以批量数据类型的数组进行请求封装;服务端发送响应给客户端时,根据命令实现的不同,返回相应的数据类型。不同的数据类型根据请求/响应报文的第一个字节进行区分:

  • 简单字符串以+开头
  • 错误数据以-开头
  • 整数以:开头
  • 批量字符串以$开头
  • 数组以*开头

RESP协议的不同部分使用"rn"(CRLF)进行分隔;

简单字符串类型(Simple Strings)

    以+开头,后面跟字符串,以rn结尾;字符串中不能包含r或者n字符。简单字符串类型都是单行数据,用以最小的开支传输非二进制安全的字符串;比如,许多命令回复OK作为操作成功的标识,编码为简单字符串类型后占用5个字节,如下:

"+OK\r\n"

注:
1)如果需要发送二进制安全的字符串,则需要改用批量字符串类型;
2)作为简单字符串类型的客户端实现,返回给调用者的内容应该是介于+和rn之间的内容;

错误类型(Errors)

    RESP中将错误作为一种专门的类型,格式类似于简单字符串类型,区别是:

  • 错误类型用于返回错误消息给客户端;
  • 错误类型以-开头;

错误类型用于发生错误时,返回消息给调用方,比如,使用了数据类型不支持的操作,或者使用了不存在的命令等。错误消息的格式如下:

"-Error message\r\n"

如下是错误消息返回样例:

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

注:作为客户端实现,当接收到错误类型消息时,应该向调用方抛出异常;上例中,-后面跟着的大写字符串ERR和WRONGTYPE用于标识错误类型,这是redis的内部规范而并非是RESP本身的规范。

整型(Integers)

    可以理解为表示整数的简单字符串类型;以:开头,以rn结尾,中间的字符串都由整数组成;比如:1000编码后变为如下格式:

:1000\r\

    redis中的许多命令都返回整数,比如:incr、llen、lastsave等,这些命令返回的整数意义由命令本身的含义决定,incr表示对一个整数进行原子自增运算,llen返回指定list的长度,lastsave则是返回一个时间戳。整型的范围不能超过64位有符号数的范围;
    整型类型也可以用于标识状态,比如:EXIST命令,用于判断一个key是否存在,存在返回1,不存在返回0;DEL命令,用于删除一个key,如果执行了删除操作,且删除成功返回1,如果未执行操作,返回0;

批量字符串类型(Bulk Strings)

    批量字符串类型,用于表示二进制安全的字符串,最大长度支持512MB。格式如下:

  • 以$开头,紧接着是一个用于标识包含传输字符串字节个数的数字,以rn结尾;
  • 待传输的字符串数据;
  • 以rn结尾标识消息的结束;

比如:字符串"foobar"编码后,格式如下:

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

空字符串的表示:如果消息为空字符串"",则编码如下:

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

Null值的表示:如果消息返回Null值,则编码如下:

"$-1\r\n"

注:区别null值和空字符串的不同,作为redis客户端实现,当接收到服务端返回的Null值时,不应该对调用方返回空字符串,而应该返回一个null值,不同的编程语言环境对null值的表示不同,比如:java中为null,C语言中为NULL,Ruby中为nil;

数组类型(Arrays)

redis客户端请求命令均是存放在数组类型中,同样,如果一个命令需要返回多条数据也可以使用数组类型,比如LRANGE命令,使用数组类型返回list中的一个多条数据;数组类型的编码规则如下:

  • 以*开头,随后跟一个用于标识数组元素数量的实数,以rn结尾;
  • 跟随多个数组元素,数量与上面指定的数量相同,每个数组元素都是嵌套RESP中的一种数据类型的数据;
  • 一个数组类型不要求其数组元素为同一种类型,可以是混合类型;
  • 数组类型同样可以嵌套数组类型;

比如:存放"foo"和"bar"两个批量字符串的数组,编码后格式如下:

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

空数组的表示:

"*0\r\n"

Null数组的表示:

"*-1\r\n"

注:区别Null数组和空数组的不同,作为redis客户端实现,当接收到服务端返回的Null数组时,不应该对调用方返回空数组;

嵌套数组的表示:[ [ 1, 2, 3 ], [ "Foo", "Bar" ] ]

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

数组中Null元素的表示:[ "foo", null, "bar" ]

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

内联命令(Inline Commands)

为了方便通过telnet等工具人工与redis服务器进行交互操作,redis允许一种称为内联命令的格式进行交互。
比如:
1)客户端发送PING,服务端返回PONG:

C: PING
S: +PONG

2)判断一个key是否存在:

C: EXISTS somekey
S: :0

RESP协议的高性能

    因为RESP协议中对于批量字符串和数组类型都会通过在内容前面加一个长度标识(prefixed lengths)来表示批量字符串的长度或者数组包含的元素个数,因此解析的时候,不需要像json等格式一样,为了查找某个特殊字符,就需要逐个扫描全部报文,而是可以根据这些长度标识跳过不关注的字符;从而提高解析的性能。这使得RESP协议既保持了文本协议的可读性和简单性,又具有和二进制协议接近的性能。
如下为C语言查找长度标识的示例:

#include <stdio.h>

int main(void) {
    unsigned char *p = "$123\r\n";
    int len = 0;

    p++;
    while(*p != '\r') {
        len = (len*10)+(*p - '0');
        p++;
    }

    /* Now p points at '\r', and the len is in bulk_len. */
    printf("%d\n", len);
    return 0;
}

参考

RESP协议官方文档:Redis Protocol specification

上一篇:Elastic Stack学习--elasticsearch部署


下一篇:jvm学习--类加载器