本文地址:https://charpty.com/article/redis-protocol-resp
笔者博客地址: https://charpty.com
为了大家看整体源码方便,我将加上了完整注释的代码传到了我的github上供大家直接下载:
https://github.com/charpty/redis4.0-source-reading
全称为,是Redis的客户端与服务端进行通信的标准协议。客户端只要按照规则发送简单的几行字符串,服务端的响应同样也是简单的几行字符串,看名字就是为量身定制的,但是官方也建议可以扩展到其它场景使用。
协议的设计思想依旧和一样,追求极致的简单,追求快。所以协议具备容易理解、容易解析、容易实现的特点。使得用户可以使用任何流传输协议连接客户端,比如、、,也就是说仅仅使用命令就可以连接并进行日常操作。
协议通信模型
可以说的通信模型很简单:客户端发送一个指令给服务端,服务端接收指令并响应结果给客户端。这是非常简洁的通信模型,可以说就是简单的一问一答。
但由于的特性,有两点例外
1. 多问一答:支持使用将多个指令一次送给服务器并等待响应
2. 订阅发布:也支持消息订阅-发布模型,这时需要服务端主动给客户端发送消息
传输格式
作为一个数据传输协议,可以传输以下5种格式的数据
1.:一行简单字符串如,一般为服务器响应OK等字符串
2.:一个整数,很多命令如,会返回整数
3.:其实就是带有前缀参数的字符串,前缀表明了字符串的长度,形式,只要读L就知道V的长度
4.:这个就是将多种格式打包形成一个数组集合,一次传输多个上述格式的数组
5.:特殊情况,这是服务端响应的错误信息,一般也为一行字符串如
在协议中规定,使用第一个字节来区分数据的传输格式,第一个字节的内容代表了后面的数据是何种格式,分别用、、、、来表示上述5种数据传输格式。传输数据都统一使用换行符结尾,当然也使用表示另一行。
客户端向服务端发送消息时使用的是格式,也就是一个数组集合,数组里元素则都是,也就是(字符长度+字符值)形式的字符串。
服务端响应客户端请求时可以发送的格式就多了,以上5种格式都有可能,这根据具体命令的实现而定。
Simple Strings
协议使用第一个字节区分传输格式,而的第一个字节是,所以一定是号开头,结尾,中间是内容,并且只有一行,也就是有且只有一个。
注意这个,在协议里是强制使用作为换行分割的,也就是说单一个是可能出现问题的,会引起不可预见的异常。这也反应了协议设计的简单性原则,不愿意过多的在多系统之间纠结,统一约定使用。
最常见的就是经典的,只有一行且满足号开头,结尾,总共就5个字节。
每种格式都一样,前面第一个标志符和最后的换行符不表示具体业务含义,对上述的来说,中间那两个字才真的具有意义,客户端也仅需要展示两字。
Integers
整数格式就是一行普通的数字,以开头,以结尾,有且只有一行。
如就是一个典型的响应。
在中有很多响应是整数形式的命令:返回列表长度的、统计字符串字符个数的、原子自增长命令、查询KEY剩余有效时间的等命令。
Bulk Strings
大字符串是一种形式的字符串,它以开头,紧随其后的是一个数字,这个数字表示了实际的字符串数据有多少个字节,再之后则是一个换行以及后续实际的字符串数据。
如就是一个,第一行数据表示下一行的实际字符串值的长度为5,也就是下一次函数只要读7个字节(带结束符)就可以了,读取长度明确之后,少去了很多拆包、粘包判断,极大提高了协议解析效率。
特殊情况是当想表示一个串时的场景,空串可以使用表示,但我们知道编程语言都有一个和语言类似的表示空的常量,在中是,在中是,如何告诉程序端这个命令的返回值是一个呢。
协议规定,使用来表示一个串,官方还特别提醒了串和空串的区别。在客户端中,当服务端响应串时展示的结果是,当服务端响应空串时展示的是。
当命令需要返回一个长串字符时一般都使用传输格式,比如命令。更多的情况是,作为数组中的元素。
Arrays
其实交互格式最多的是数组格式,元素基本上都是。
格式的开始标志是,紧接着是一个数字如,表示该数组一共有多少个元素,第一行就结束了。接下来就是实际的元素数据,元素数据可以是或格式。
举个例子:
这个数组一共3个元素,这个可以从第一行中知道,第一个元素是一个整数,整数的标记位是,第二个元素也是个整数,值为,第三个元素则是一个格式,该大字符串一共有4个字节,可以从第四行的中知道,真实数据值是。
由上可见,格式也就是多个简单格式的组合,数组中的元素的类型可以各不相同,并且,数组是可以嵌套的,如下所示,为了展示清楚,我们每个都换下行。
这个数组一共有三个元素,由第一行的可知,两个元素的类型是数组,一个是整数。第一个数组就是上面举例的那个数组,第二个元素则是一个包含两个简单字符串的数组,最后一个元素则是一个整数。
和空类似,也使用来表示数组,也就是,使用表示空数组,也就是。当然数组里的元素也可以是,如。
客户端发送请求
客户端向服务端发送请求时必须使用格式,且元素格式也只能是,服务器响应格式则是根据具体命令实现而定。
最经典的(a已设值),通过命令可以得到如下结果。
类似的的命令格式则是,返回则是格式的。
关于Pipelining
服务端支持客户端一次性发送多个命令给服务端,由服务端一次性执行并一次性返回结果,类似,返回,也就是所谓的流水线。
其实的实现就是嵌套,我想通过签名嵌套数组的描述不难理解。
Errors
错误格式专门定义了一种格式用来传递错误信息,这种格式几乎和一样,唯一的区别是第一个标记字符是,而中间的字符串内容就是具体的错误信息。
一般来说错误信息分为两段,第一段是一个单词,比如:、,第一段是对这次错误的整体概述,也可以说是一个错误类型,像是错误编码。官方称之为。第一段之后紧接着就是一个空格,然后就是第二段。 第二段就是具体的错误信息,帮助客户端理解服务器出错具体原因。
错误发生的情况有很多:
1. 当你用命令操作一个时,将返回。
2. 当你随意输入一个命令时,将返回。
3. 当使用的offset参数输入一个负数时,将返回
Inline Commands
内置命令并不是一种标准格式,它更多的是一种解析手段,主要是为了方便没有客户端的场景,手里只有命令或者,这个时候让用户手敲一段来实现实在有点强人所难。
所以实现了一种所谓内置命令的形式的格式,让用户直接输入也依然能够返回结果信息。
几乎所有命令都支持以内置命令形式发送并解析,大多数常用且简单的命令,如、、、等都是支持的,唯一的区别是内置命令支持的长度是有限的,一次性发送过长的字节可能会丢失。
所以发送命令就两种形式,一种是元素为的数组,一种是内置命令。
协议实现
协议在并没有个专门的文件实现,而是主要包含在网络字节流处理过程中,这也是由于本身足够简洁,不需要一个专门的解释器。
所以也就是说主要实现都在文件中,实现分为两部分,一部分是接收字节流请求,并处理为内部的数据结构,供具体命令实现函数调用;另一部分是将内部的数据结构转换为字节流输出到客户端。
接收请求的处理函数的名称大多为,响应客户端的处理函数的名称大多为。
接收请求并解析
处理请求的函数入口是,它有两条分支:内置命令?:。也就是通过区分字节流第一个字符是不是(格式标志)来调用不同的函数解析请求。
当第一个字节是```*```时当普通命令处理,其它则都认为是内置命令。
内置命令解析
内置命令解析由函数实现
普通命令解析
普通命令解析由函数实现
组装响应格式
和解析传输格式一样,组装响应格式并输出的工作也是在中完成。将已知结构体解析为字节流肯定比从字节流解析出结构体要简单的多,少了很多不可预知性。所以如果不谈不深究网络交互,只将传输格式组装还是简单易懂的。
组装Bulk Strings数组结果
当比如要想返回一个字符串列表时,只要配合上设置数组长度的函数即可(其实数组也就是开头有一个表示数组长度)。
组装简单字符串结果
简单字符串是以'+'号开头,但在实现中并不全是组装而来的,而是直接输出已经预定好的字符串。像、这些都是预定义好了的字符串。
这些预定义的字符串都存在的结构体,属性名为,它的初始化在函数中。
组装的函数为,比较简单就不描述了。
总结
协议设计的原则是简单易解析,所以在通用性方面不能顾及的特别全,比如一定要使用而不能使用,但是与其增加一堆复杂逻辑处理这种场景不如大家都遵守默认约定。
我想这种简单的原则也是如此高效且受欢迎的原因吧。
领取专属 10元无门槛券
私享最新 技术干货