1、概述
Redis客户端使用名为RESP(Redis序列化协议)的协议与Redis服务器进行通信。
RESP具有如下特点:
- 易于实现
- 快速解析
- 高可读性
RESP可以序列化不同的数据类型,如整数、字符串、数组。还有一种特定的错误类型。
请求将要执行的命令作为字符串数组从Redis客户端发送到Redis服务器。Redis使用特定数据类型的命令进行回复。
RESP是二进制安全的,不需要处理一个进程传输到另一个进程的批量数据,因为它使用前缀长度来传输批量数据。
客户端连接到Redis服务器,是创建TCP连接到端口6379。
虽然RESP在技术上是非TCP特定的,但在Redis的上下文中,协议仅用于TCP连接(或类似的面向流的连接,如Unix套接字)。
2、请求响应模型
Redis接受不同参数组成的命令。
最简单模型:
请求-响应:收到命令后,将其进行处理并将回复发送回客户端。
其他模型:
模型1:Redis支持流水线操作,因此客户端可以一次发送多个命令,并等待稍后的回复。
模型2:当Redis客户端处于Pub/Sub时,协议会更改语义并成为推送协议,即客户端不再需要发送命令,因为服务器会在它们接收到命令时自动向客户端发送新消息。
3、RESP协议描述
RESP协议在Redis 1.2中引入,是和Redis 2.0中的Redis服务器通信的标方式。也是Redis客户端中应该实现的协议。
RESP支持的数据类型包括:单行字符串、错误信息、整型、多行字符串、数组。
数据类型可以通过第一个字节进行判断,规则如下:
1)单行(simple Strings):回复的第一个字节是“+”
2)错误(Errors)信息:回复的第一个字节是“-”
3)整型数字(Integers):回复第一个字节是“:”
4)多行字符串(Bulk Strings):回复的第一个字节是“$”
5)数组(Arrays):回复的第一个字节是“*”
在RESP中,协议的不同部分始终以“\r\n”(CRLF)结束。
需要注意的是:执行同样的一个命令,成功和失败返回的类型有可能一样,也有可能不一样。
比如:
SETNX成功和失败返回的类型一致,都是整数。
INCR成功的时候返回整型,失败的时候返回错误(Errors)信息。
3.1 单行字符串(+Simple Strings)
格式要求:+号字符,后跟不能包含CR或LF字符的字符串(不允许换行),由CRLF终止(即“\ r \ n”,对应十六进制 0x0D,0x0A)。
Simple Strings用于以最小的开销传输非二进制安全字符串。
比如:很多Redis命令成功回复时只有“OK”,因为RESP单行字符串使用以下5个字节进行编码。
"+OK\r\n"
当Redis使用Simple String回复时,客户端库应该向调用者返回一个字符串,该字符串由“+”之后的第一个字符组成,直到字符串结尾,不包括最终的CRLF字节。
对应hiredis的返回值为:REDIS_REPLY_STATUS
示例:
void testSimpleString(redisContext *context)
{
// Set Key Value
const char *key = "str";
const char *val = "Hello World";
/*SET key value */
redisReply *reply = (redisReply *)redisCommand(context, "SET %s %s", key, val);
printf("reply->type: %d\n", reply->type);
if (reply->type == REDIS_REPLY_STATUS)
{
/*SET str Hello World*/
printf("SET %s %s\n", key, val);
}
printf("reply->str: %s\n", reply->str);
freeReplyObject(reply);
}
程序输出:
reply->type: 5
SET str Hello World
reply->str: OK
抓包显示:
抓包的时候加上-XX,比如:
sudo tcpdump -i any dst host 127.0.0.1 and port 6379 -XX
如果使用SETNX 去测试,成功返回1,错误返回0,该命令和SET的返回是不一样的。
3.2 错误信息(- Errors)
RESP具有错误的特定数据类型。错误回复仅在发生错误时发送。
RESP中单行字符串和错误之间的真正区别在于客户端将错误视为异常,组成错误类型的字符串是错误消息本身。
1)格式
"-Error message\r\n"
2)错误回复示例
-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value
“-”之后的第一个单词,直到第一个空格或换行符,表示返回的错误类型。 这只是Redis使用的约定,不是RESP错误格式的一部分。
ERR是一般错误,而WRONGTYPE是一个更具体的错误,意味着客户端尝试对错误的数据类型执行操作。 这称为错误前缀,是一种允许客户端理解服务器返回的错误类型的方法,而不依赖于给定的确切消息,这可能随时间而变化。
客户端实现可以针对不同的错误返回不同类型的异常,或者可以通过直接将错误名称作为字符串提供给调用者来提供捕获错误的通用方法。一般有限的客户端实现可能只返回通用的错误条件,例如false。
3)示例
127.0.0.1:6379> SET teacher darren
OK
127.0.0.1:6379> INCR teacher
(error) ERR value is not an integer or out of range
抓包数据如下:
4)hiredis实现
void testError(redisContext *context)
{
// Set Key Value
printf("SET\n");
const char *key = "teacher";
const char *val = "Joe";
/*SET key value */
redisReply *reply = (redisReply *)redisCommand(context, "SET %s %s", key, val);
printf("reply->type: %d\n", reply->type);
if (reply->type == REDIS_REPLY_STATUS)
{
/*SET str Hello World*/
printf("SET %s %s\n", key, val);
}
printf("reply->str: %s\n", reply->str);
freeReplyObject(reply);
// INCR Key
printf("\nINCR\n");
/*INCR key value */
reply = (redisReply *)redisCommand(context, "INCR %s", key);
printf("reply->type: %d\n", reply->type);
if (reply->type == REDIS_REPLY_ERROR)
{
/*INCR key*/
printf("INCR %s failed:%s\n", key, reply->str);
} else if (reply->type == REDIS_REPLY_INTEGER) {
printf("INCR %s = %lld\n", key, reply->integer);
}
freeReplyObject(reply);
}
程序输出:
SET
reply->type: 5 (REDIS_REPLY_STATUS)
SET teacher Joe
reply->str: OK
INCR
reply->type: 6 (REDIS_REPLY_ERROR)
INCR teacher failed:ERR value is not an integer or out of range (此时如果incr的是一个本身不在的key则返回1)
3.3 整型数据(: Integers)
整型数据类型是一个CRLF终止的字符串,表示一个以“:”字节位前缀的整数。
如:“:0\r\n”或“:1000\r\n”是整数回复。
许多Redis命令返回RESP 整型,如INCR, LLEN 和LASTSAVE。
返回的整数没有特殊含义,它只是INCR的增量值,LASTSAVE的UNIX时间等等。 但是,返回的整数应证在有符号的64位整数范围内。
整数回复也被广泛使用,以便返回真或假。 例如,EXISTS或SISMEMBER之类的命令将返回1表示true,0表示false。
如果实际执行操作,其他命令(如SADD,SREM和SETNX)将返回1,否则返回0。
以下命令将回复整数回复:SETNX,DEL,EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE,RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。
示例
SET count 10
INCR count
void testIntegers(redisContext *context)
{
// Set Key Value
printf("SET\n");
const char *key = "count";
const char *val = "10";
/*SET key value */
redisReply *reply = (redisReply *)redisCommand(context, "SET %s
%s", key, val);
printf("reply->type: %d\n", reply->type);
if (reply->type == REDIS_REPLY_STATUS)
{
/*SET str Hello World*/
printf("SET %s %s\n", key, val);
}
printf("reply->str: %s\n", reply->str);
freeReplyObject(reply);
// INCR Key
printf("\nINCR\n");
/*INCR key value */
reply = (redisReply *)redisCommand(context, "INCR %s", key);
printf("reply->type: %d\n", reply->type);
if (reply->type == REDIS_REPLY_ERROR)
{
/*INCR key*/
printf("INCR %s failed:%s\n", key, reply->str);
} else if (reply->type == REDIS_REPLY_INTEGER) {
printf("INCR %s = %lld\n", key, reply->integer);
}
freeReplyObject(reply);
}
程序输出:
SET
reply->type: 5
SET count 10
reply->str: OK
INCR
reply->type: 3 (REDIS_REPLY_INTEGER)
INCR count = 11
抓包显示:
3.4 多行字符串($ Bulk Strings)
多行字符串用于表示长度最大为512 MB的单个二进制安全字符串。
1、编码格式特点
1)一个“$”字节后跟组成字符串的字节数(一个前缀长度),由CRLF终止。
2)字符串数据
3)最终的CRLF
如:字符串“foobar”的编码为"$6\r\nfoobar\r\n"
;空字符串编码为:"$0\r\n\r\n"
。
RESP 多行字符串也可用于使用用于表示Null值的特殊格式来表示值的不存在。 在这种特殊格式中,长度为-1,并且没有数据,因此Null表示为:"$-1\r\n"
。
当服务器使用Null 多行字符串回复时,客户端库API不应返回空字符串,而应返回nil对象。 例如,Ruby库应返回’nil’,而C库应返回NULL(或在reply对象中设置特殊标志),依此类推。
2、示例
1)命令
127.0.0.1:6379> MSET king redis darren avmedia
OK
127.0.0.1:6379> MGET king darren
1) "redis"
2) "avmedia"
2)抓包显示
3)hiredis编码
void testBulkStrings(redisContext *context)
{
printf("MSET\n");
/*MSET key value [key value ...]*/
redisReply *reply = (redisReply *)redisCommand(context, "MSET Lily redis Lucy avmedia");
if (reply->type == REDIS_REPLY_STATUS) {
printf("MSET king redis darren avmedia\n");
}
freeReplyObject(reply);
printf("\nMGET\n");
/*MGET key [key ...]*/
reply = (redisReply *)redisCommand(context, "MGET Lily Lucy");
printf("reply->type: %d\n", reply->type);
if (reply->type == REDIS_REPLY_ARRAY) {
printf("MGET Lily Lucy\n");
redisReply **pReply = reply->element;
int i = 0;
size_t len = reply->elements;
//hello world good
for (; i < len; ++i) {
printf("%s \n", pReply[i]->str); // 如果没有数据时为null
}
printf("\n");
}
freeReplyObject(reply);
}
程序输出:
MSET
MSET Lily redis Lucyav media
MGET
reply->type: 2 (REDIS_REPLY_ARRAY)
MGET Lily Lucy
redis
(null)
3.5 数组(* Arrays)
1、编码格式特点
1)*字符作为第一个字节,后跟数组中的元素个数作为十进制数,后跟CRLF。
2)数组的每个元素的附加RESP类型。
空数组:"*0\r\n"
;两个RESP批量字符串“foo”和“bar”的数组编码为:"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
;
三个整数的数组编码:"*3\r\n:1\r\n:2\r\n:3\r\n"
数组可以包含混合类型,元素不必具有相同的类型。 例如,四个整数和批量字符串的列表可以编码如下:
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n
服务器发送的第一行是* 5 \ r \ n
,以指定将跟随五个回复。 然后发送构成多重回复项目的每个回复。
Null 数组的概念也存在,并且是指定Null值的替代方法(通常使用Null 多行字符串,但由于历史原因,我们有两种格式)。
例如,当BLPOP命令超时时,它返回一个计数为-1的Null数组,如:"*-1\r\n"
。
当Redis使用Null数组回复时,客户端库API应返回空对象⽽不是空数组。 这是区分空列表和不同条件(例
如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
第二个元素是Null。 客户端库应返回如下内容:["foo",nil,"bar"]
。
注意,这不是前面部分中所述的例外,二只是进一步指定协议的示例。
2、示例
127.0.0.1:6379> LPUSH list hanmei lucy lily jack
(integer) 4
127.0.0.1:6379> LRANGE list 0 2
1) "jack"
2) "lily"
3) "lucy"
127.0.0.1:6379> LRANGE list 0 -1
1) "jack"
2) "lily"
3) "lucy"
4) "hanmei"
可以使用LRANGE list 0 2
和LRANGE list 0 -1
观察抓包数据。
4、发送命令到Redis服务端
1)客户端和服器之间的交互流程
- 客户端向Redis服务器发送仅由Bulk Strings组成的RESP阵列。
- Redis服务器回复发送任何有效RESP数据类型作为回复的客户端。
2)典型的交互
客户端发送命令LLEN mylist以获取存储在密钥mylist中的列表⻓度,服务器回复一个Integer回复,如下(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整体。