Redispy 源码学习(二) --- RESP协议简介

网络通信离不开各种各样的协议,著名的tcp,http等协议构建了我们常见的web应用。http协议是基于tcp的应用层协议。同样的,redis的协议也是基于tcp的应用层协议,即RESP(REdis Serialization Protocol)。

RESP设计巧妙,它的追求在于下面三个方面:

  • 易于实现
  • 解析高效
  • 易于人读

协议基础

RESP协议规定,客户端通过tcp网络连接到redis服务器。通信过程类似一问一答的方式,与HTTP类似,客户端发送命令请求到服务器,服务器执行时候返回结果。

客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。正式因为CRLF的存在,让redis的解析十分方便和可读。

请求协议

我们知道,redis数据库操作的命令无非就三种,第一中是不带参数的命令,例如PINGFLUSHDB;其次是带有参数的读取命令,例如GET keyLLEN queue;还有带有参数并设置值的写操作,例如 SET hello worldCONFIG SET TIMEOUT 30

命令和参数之间,通常都是用空格隔开。RESP协议规定,这样的请求中,命令和各参数之间必须使用CRLF分割,同时还必须提供命令头标签,即token。字节串的第一个字符到CRLF的内容即为头标签。

参数中的字符串和数字

字串

一个redis数据库操作包含命令+参数。当然有的命令参数可以省略。无论编码命令还是参数,其编码方式都是一样的。

命令中,无论请求和返回的结果,无怪乎就只有字符串和数字。编码字符串的格式如下:

$ + string_length + string + CRLF,即以$符号开头,然后跟字节串的长度,然后跟着一个CRLF,再然后就是字节串本身,最后以一个CRLF结尾。

例如 hello编码成 $5\r\nhello\r\n21.7编码成$3\r\n21.7\r\n。其中$5\r\n$3\r\n都是Token。

数字

数字类型和字符串类似,不同的在于数字使用:开头,同时不需要说明数字的长度。

: + number + \r\n,即:开头,然后跟着数字,最后以CRLF结尾即可。

注意,redis中绝大多数参数都是字串,只有返回响应的时候会处理数字类型,例如incr操作。其他情况下,请求的参数,还是返回响
应中的数字,其实都是数字字符串。例如下面是一个集合,返回元素的时候,里面的元素都是字串

127.0.0.1:6379> sadd set hello
(integer) 1
127.0.0.1:6379> sadd set 世界
(integer) 1
127.0.0.1:6379> sadd set 1
(integer) 1
127.0.0.1:6379> sadd set 21.7
(integer) 1
127.0.0.1:6379> smembers set
1) "1"
2) "21.7"
3) "\xe4\xb8\x96\xe7\x95\x8c"
4) "hello"

sadd的set的参数中,1,和21.7都是字串类型,返回自然是字串,而返回表示sadd成功的则是数字类型。所有请求的命令都是字串类型,哪怕写的是数字字面量,redis最终还是当成字串处理。

编码命令

上面我们了解单独的命令和参数的编码方式。下面介绍命令+参数的组合编码方式。

组合编码也很简单,无非就是将编码的命令(字符串)拼接起来,同时再最开始追加一个* + 组合命令或参数的个数的方式:

*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF

例如 PING 编码字符串的方式为$4\r\nPING\r\n,组合的编码为*1\r\n$4\r\nPING\r\n。即

*1
$4
PING

GET hello编码为*2\r\n$3\r\nGET\r\n$5\r\nhello\r\n

*2
$3
GET
$5
hello

GEThello两个字符串分别编码,然后组合起来,编码了两个参数,因此*后面跟着是2。

再看一个例子,SET 中国 21.7的将编码成*3\r\n$3\r\nSET\r\n$6\r\n\xe4\xb8\xad\xe5\x9b\xbd\r\n$4\r\n21.7\r\n

*3
$3
SET
$6
\xe4\xb8\xad\xe5\x9b\xbd
$4
21.7

汉字中国的按照utf-8编码的方式编码成bytes为*\xe4\xb8\xad\xe5\x9b\xbd,一个汉字三个字节,因此中国的长度是6。

python3的字串原生支持unicode,计算机操作字串的时候是unicode,当需要网络传输和写入文件的时候,都必须把unicode的字串编码成bytes结构。同理,当从网络或者文件中读取数据的时候,也需要解码成unicode。目前国际通用的编码方式以utf-8为主。

值得注意的是,将命令参数组合的编码方式,不仅是请求的时候如此,在redis的响应回复中,其中的多批量回复(multi-bulk reply)也是采用了同样的编码方式返回。

无论单独编码数字,字串还是命令组合,我们都把开头到第一个CRLF的内容看成头标签(Token)。

响应协议

既然有请求的协议规定,当然也有响应回复的协议。请求的命令已经介绍了三种,其差别就在于参数的个数。对于响应的回复,则会比请求的情况更多。

RESP协议规定,所有答复的第一个字节规定了响应答复的类型,后除了类型之外,和请求中编码字符串和数字的类似。响应大概类型如下:

  • 状态回复(status reply)的第一个字节是 +
  • 错误回复(error reply)的第一个字节是 -
  • 整数回复(integer reply)的第一个字节是 :
  • 批量回复(bulk reply)的第一个字节是 $
  • 多条批量回复(multi bulk reply)的第一个字节是 *

状态回复

对于客户端命令,执行一个查询或者一个写入操作,通常会返回操作的是否成功的状态判定。状态回复是单行回复。例如上面ping命令的返回就是ok。

ok的状态的编码返回为+OK\r\n,虽然OK也是字串,但是在状态中返回,我们只需要知道状态结果,不需要显示的告诉返回多少字符,后面可以知道,字符串编码的时候需要标记字符数,纯粹是为了读取socket的时候,确定分包边界。由于状态回复只是单行字串,因此最后一个CRLF就能确定包的分界。

错误回复

与状态回复类似,错误回复的第一个字符是-,然后跟着错误的类型。错误类型之后以一个空格结束,然后就是错误的信息(msg),错误信息是一段文字,可以包含空格,错误信息结束之后也依然是一个CRLF表示回复结束。

例如命令不存在的时候会返回错误:

127.0.0.1:6379> hello
(error) ERR unknown command 'hello'
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range

返回的错误编码回复依次是-ERR unknow command 'hello\r\n'-ERR value is not an integer or out of range\r\n

整数回复

所谓整数回复,即返回数字类型的回复。整数回复以:开头,其后跟着数字,最后以CRLF结尾。这和之前介绍的数字编码的方式一样。

例如:0\r\n:1000\r\n 都是整数回复。什么时候返回数字类型呢?一般是incr数字操作和一些返回数字表示逻辑的操作,例如DEL EXISTS等。

127.0.0.1:6379> INCRBYFLOAT float 1.2
"1.2"
127.0.0.1:6379> INCRBYFLOAT float 1.2
"2.4"
127.0.0.1:6379> INCRBY integer 2
(integer) 2
127.0.0.1:6379> INCRBY integer 2
(integer) 4
127.0.0.1:6379> DEL integer
(integer) 1

INCRBY 和 INCRBYFLOAT都是自增参数的数字,前者返回的是数字类型,后者返回的是字符串类型。也就是我们接下来介绍的批量回复。

批量回复

所谓的批量回复,只redis返回的二进制安全的字串。很大查询返回都属于批量回复。例如上面的例子中,INCRBYFLOAT的返回即使批量回复。

批量回复以$符号开头,然后跟着字符串的长度值,然后就是CRLF和实际的字串数据,最后以CRLF结束。和字符串的编码一模一样。

例如查询命令 get hello,返回结果为world的情况下,批量回复的原始数据编码为$5\r\nworld\r\n

当请求的key的value不存在的时候,将会返回-1用于表示长度的值。例如get non-existing-key将收到$-1\r\n的返回。对于这种空回复,客户端语言就可以灵活处理,比较推荐的做法就是使用编程语言表示对象不存在的值,例如python的None,Golang的nil

多批量回复

常见的返回是批量回复,此外还有一个多批量回复,顾名思义,就是多个批量回复的组合。操作序列或者集合类的数据结构,就会返回多批量回复。例如上面的smembers set命令的返回将会是:

*4\r\n$1\r\n1\r\n$3\r\n21.7\r\n$6\r\n\xe4\xb8\x96\xe7\x95\x8c\r\n$5\r\nhello\r\n

这和我们编码请求的方式一模一样。由此可见,RESP的设计非常优雅。

多批量回复是批量回复的组合,那么也会有返回-1的情况。此外,当读取一个集合,如果集合没有元素,则不是返回nil,而是返回空集合的结果,接多批量回复的结果为*0\r\n

官网的还给出了一个例子关于多批量回复的空元素。例如下面的多重批量回复:

*3
$3
foo
$-1
$3
bar

其中, 回复中的第二个元素为空。因此最终转换成python的对象应该是这样的:["foo", None, "bar"]

总结

RESP的协议基于tcp的应用层协议,主要用到了字符和数字的编码和CRLF的组合编码方式。我们认识到字串的编码和数字编码的方式。请求的时候使用类似多批量回复方式编码,回复的响应有多种,可以根据第一个字节做不同情况的处理,比如状态回复,错误回复,批量回复和多批量回复。

对于多批量回复,空和无穷的方式也需要考虑,前者会返回一个0表示空,后者会返回-1表示无穷大(block情况也类似)。

了解了协议的编码方式,下一步,我们就使用代码实现这个协议的编码和解码过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 编码发送数据到redis服务,客户端完成了第一个交互过程,即请求的过程。接下来客户端还要接受并解析服务端的响应回复...
    人世间阅读 1,191评论 0 1
  • 设计模式,即解决特定场景的问题的一系列方法与总结 程序设计六大原则: 一、单一职责原则:描述的意思是每个类都只负责...
    superzhan1992阅读 193评论 0 0
  • 不知道今天自己怎么了 可能病了 连脾气都跟着坏了 明明没什么的 都被弄得好像多大一件事的 你也是够忍得我的了 真是...
    小气鬼丶Sue阅读 172评论 0 0
  • 敬笃 尘世的光,守护着过往与理想,那些看似陌生的植物,表层的褶皱,都是岁月无情的伤痕。 人类就像候鸟一样,每天对着...
    山谷小道士阅读 579评论 0 8
  • 我一直觉得自己的天赋是随和,容易被接近,直到有勇气问别人眼中的自己才发现原来自己眼中的自己和别人眼中的自己完全不一...
    Irene_274a阅读 713评论 0 0