美文网首页网络编程魔法
Redispy 源码学习(二) --- RESP协议简介

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

作者: 人世间 | 来源:发表于2017-04-19 17:41 被阅读553次

    网络通信离不开各种各样的协议,著名的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情况也类似)。

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

    相关文章

      网友评论

        本文标题:Redispy 源码学习(二) --- RESP协议简介

        本文链接:https://www.haomeiwen.com/subject/qbehzttx.html