乐趣区

【Redis源码研究】Redis的RESP协议

作者:张仕华
resp 协议
redis 客户端和服务端交互使用的是 redis 作者制定的一个协议,叫 resp(REdis Serialization Protocol)。
具体分如下几个层次

基于 tcp
请求响应模式, 但在两种情况下不再是简单的请求和响应模式 (下文介绍)
支持五种类型的数据,分别是简单字符串,错误,整型,bulk strings , 数组

客户端发给服务端的命令都会序列化为 array, 而服务端返回给客户端的可以为如上任意一种类型,各简单举例如下

简单字符串, 第一个 byte 为 ‘+’, 例如 “+OKrn”
错误,第一个 byte 为 ’-‘, 例如 “-Error messagern”
整型, 第一个 byte 为 ’:”, 例如 “:10rn”
bulk strings, 第一个 byte 为 ”$”, 例如 “$6rnfoobarrn”(此处为美元符号,没有前边的反斜杠)
数组, 第一个 byte 为 ”, 例如 ”2rn$3\r\nfoo\r\n$3rnbarrn”

具体介绍参考:https://redis.io/topics/protocol
sub pattern
请求响应模式有两种特殊情况

1.pipeline 模式,客户端同时发送多条命令到服务端
2.sub pattern: 当客户端执行 subscribe 命令后, 不再要求客户端发送命令,当有其他客户端在订阅渠道上 publish 消息后, 服务端会主动 push 信息到客户端

我们拿 redis-cli 客户端试一下执行 subscribe
127.0.0.1:6379> subscribe foo
Reading messages… (press Ctrl-C to quit)
1) “subscribe”
2) “foo”
3) (integer) 1
可以看到,redis-cli 会阻塞,当另起一个客户端,publish 消息后,会收到该消息并打印
~$redis-cli
127.0.0.1:6379> publish foo bar
(integer) 1
~$redis-cli
127.0.0.1:6379> subscribe foo
Reading messages… (press Ctrl-C to quit)
1) “subscribe”
2) “foo”
3) (integer) 1
1) “message”
2) “foo”
3) “bar”
直观感觉是服务端阻塞了,没有返回数据给客户端。但看 redis 源码,实际服务端并没有阻塞,并且可以在连接上继续接收并处理命令
void subscribeCommand(client *c) {
int j;
// 将订阅的渠道加入相应结构体并直接返回
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
// 将客户端置 CLINET_PUBSUB 标记
c->flags |= CLIENT_PUBSUB;
}
当客户端置了 CLINET_PUBSUB 标记后, 命令处理会做如下特殊逻辑
int processCommand(client *c) {

// 当置 CLIENT_PUBSUB 标记后, 只有 ping/subscribe/unsubscribe/psubscribe/punsubscribe 命令能够执行
if (c->flags & CLIENT_PUBSUB &&
c->cmd->proc != pingCommand &&
c->cmd->proc != subscribeCommand &&
c->cmd->proc != unsubscribeCommand &&
c->cmd->proc != psubscribeCommand &&
c->cmd->proc != punsubscribeCommand) {
addReplyError(c,”only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context”);
return C_OK;
}

}
godis-cli
如上文所示, 当客户端执行 subscribe 命令后, 实际上是可以继续订阅或者取消订阅渠道,并且可以执行 ping 命令。redis-cli 客户端实际上是自己阻塞了,如下代码:
if (config.pubsub_mode) {
if (config.output != OUTPUT_RAW)
printf(“Reading messages… (press Ctrl-C to quit)\n”);
// 进入死循环,一直等待服务端发送消息
while (1) {
if (cliReadReply(output_raw) != REDIS_OK) exit(1);
}
}
那么,我们可以拿 go 写一个不阻塞的版本,并且可以测试 redis 的 subscribe 模式。效果如下
>bogon:godis-cli $ go run godis-cli.go
> set k1 v1
OK
> get k1
v1
> subscribe foo
subscribe foo 1
[sub]>subscribe foo1//sub 模式下可以继续订阅其他渠道
subscribe foo1 2
[sub]> unsubscribe foo1// 取消订阅
unsubscribe foo1 1
[sub]> ping//sub 模式也可以执行 ping
pong
[sub]> get k1 //sub 模式下不能执行 get 命令
Redis Error: only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context
[sub]> exit
exit sub pattern….
>get k1// 退出 sub 模式后可以继续正常执行 get 命令
v1
> exit
由于 godis-cli 直接实现了 RESP 协议,虽然只是为了观察 subscribe pattern 的效果,但实际上可以支持所有 redis 命令的执行
具体代码地址见:https://github.com/erpeng/god…

退出移动版