16 进制 0d 0a
就是 \r\n
。
RESP 就是 Redis 服务端和客户端之间进行通信的协议,它是建立在 TCP 之上的一种简单的应用层协议。你可以把它理解成 HTTP 协议,不过它更加的简单。
它支持很多数据类型,这里列举几个常用的:
简单字符串是格式是:以 +
开始,接着是字符串(不允许含有 \r
或者 \n
),结尾是 \r\n
。它用于传输短的,极小开销的非二进制字符串。例如:+OK\r\n
,这个通常是用于服务端基于客户端的响应。如下图所示:
批量字符串的格式是:以 $
开始,接着是字符串的长度,\r\n
分隔,字符串本身,\r\n
结尾。例如:$5\r\nhello\r\n
,获取上次设置的键值:
这是抓包获取的结果,右下角红色框选的即为服务器的响应的解码形式(左侧为16进制形式)。你在程序层面看到的是 CrazyDragon
,但是在网络层面它是这样的形势:$11\r\nCrazyDragon\r\n
。
数组的格式是:以 *
开头,接着是数组元素的个数,\r\n
分隔,然后是数组的每一个元素。例如:*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
表示 "hello"
和 "world"
两个字符串。现在我们用批量命令 mget
获取两个 author:001
和 author:002
的值。
同样的抓包分析它,可以看到返回的即是 Arrays
格式的数据,把它写成这样的形式就更加明了了:*2\r\n$11\r\nCrazyDragon\r\n$3\r\nTom\r\n
,只不过它不显示转义符。
这里只是简单介绍一下 RESP,具体的规范请直接查看官方文档,并自己抓包分析:
Redis serialization protocol specification
在 Redis 中,实现批量操作可以通过管道或者事务来实现,它的主要区别是:
MULTI
EXEC
WATCH
DISCARD
来完成的。当用户输入 MULTI,接下来输入的指令,会被服务端存储起来,最后当作一个原子性的指令执行。刚才在第一部分,我们已经知道了它们之间的通信协议。所以,我们就可以从网络的层次来了解这个过程,因为之前这些区别都是从文本获取的,大多数人只是机械的记忆了区别,却没有真正的看过它们的区别。既然我们可以通过网络抓包来分析,那么就来做一些有意义的事情吧!所以,我们就来从 RESP 的角度来看一下它们的区别。
既然需要查看 Redis 的网络通信,那么自然是需要一个 Redis 了。这里我是下载的 github 上面的一个 windows 便携版。这个版本使用起来很方便,只是测试的话,不需要去启动虚拟机,然后再启动 docker 容器了。然后还需要一个抓包软件,这里推荐使用 WireShark,在官网下载安装即可。
先启动 redis 服务端,再启动一个客户端并连接,然后启动 wireshark 并设置好过滤条件即可。
启动 Redis 服务器:
启动 Redis 客户端,并插入三条数据:
启动 WireShark,选择 loopback 适配器(因为这里用的是 127.0.0.1
loopback 地址):
然后进入抓包页面,这里会实时显示已经获取的包,但是它会显示所有经过 loopback 的包(各种协议和端口),显然我们是不需要那么多的,太多了也会形成干扰,所以要过滤一下:tcp.port == 6379
。
进行端口过滤之后,这个页面就没有数据了。因为这时还没有网络通信,所以这时没有捕获的网络数据包:
这里通过开启一个事务来获取数据,在事务中执行三次 get 指令,最后执行整个事务,这里的演示比较简单(TX 即是 Transaction 的简写)。
这里我们不关注这个结果,我们关注的是它执行的过程,直接看网络抓包的结果,下图即是上面整个事务从开始到执行成功的网络包。从协议列(protocol),可以看出来 RESP 是基于 TCP 的,所以每一个 RESP 之前都需要 TCP 的三次握手建立连接。
注:这里不得不说一句:WireShark 真是一个伟大的软件!它是直接识别了 RESP 了。所以,多了解一些东西,原来透明i的概念就会显现出来了。
我们这里只需要重点关注应用层的 RESP 即可,所以传输层的 TCP 报文也过滤掉:tcp.port == 6379 and resp
这样再看这个协议的交互就清晰多了,它还是很常见的 Request-Response 模式。我们来看一个 Request 和一个 Response。
把数据给复制出来(选择以可打印的格式):
下面两个分别是请求报文和响应报文(不是同一个请求和响应报文),因为换行符 \r\n
不可见,所以我给它手动补上了(你复制下来不是转移字符)。
*2\r\n$3\r\nget\r\n$10\r\nauthor:002\r\n
*3\r\n$11\r\nCrazyDragon\r\n$3\r\nTom\r\n$5\r\nPeter\r\n
前面我们已经简单了解了 RESP,这里大家看到这个报文应该就能知道它的意思了。
老实说,只是使用管道和事务的话,是很难了解它们的区别。
直接看抓包的信息,这里可以看出,通过管道执行,它就是将命令按照 RESP 的格式给拼接起来,然后直接发送出去了,响应也是每一条命令的执行结果以及最终返回的数据。
不过这里有点不对劲,因为管道命令里面也是事务。我去看了一下,是因为 Python 的 redis 库默认的管道命令是原子性的。不过,这里并不需要,我们给它关掉,重新抓一个包吧。
这样就是最传统的非原子性的管道了,下面是它的报文(这里是直接复制的报文,我就不把 \r\n
打出来了,不过你应该知道的),可以看出来它们就是简单的命令拼接。
*2
$3
GET
$10
author:001
*2
$3
GET
$10
author:002
*2
$3
GET
$10
author:003
通过网络抓包分析,我们可以清晰的看出来。事务的每一条指令都会进行一次网络请求,所以在 QUEUED 阶段失败了,整个事务就失败了,因为服务端是可以感知的,成功了它才会发送 QUEUED 指令。而管道呢,则是把若干条指令拼接起来一次性发送,它最大的作用是节省了多次建立连接所需要的时间(不要小看了每次建立断开连接是开销,累计起来是很庞大的!)。
前面我们简单了解了 RESP,以及在此基础上去观察事务和管道命令在执行上面的区别。那么我们还能用它来做什么呢?来整一个活!
代码示例:
package main
import (
"fmt"
"net"
"strings"
)
func main() {
DragonRedisClient()
}
func DragonRedisClient() {
// 连接到 TCP 的 6379 端口
conn, err := net.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 执行命令:set love "I love you yesterday and today."
var builder strings.Builder
builder.WriteString("*3") // 3 个字符串
builder.WriteString("\r\n")
builder.WriteString("$3") // set 字符串长度
builder.WriteString("\r\n")
builder.WriteString("set") // set
builder.WriteString("\r\n")
builder.WriteString("$4") // love 字符串长度
builder.WriteString("\r\n")
builder.WriteString("love") // love 键名
builder.WriteString("\r\n")
builder.WriteString("$31") // love 值内容长度
builder.WriteString("\r\n")
builder.WriteString(`I love you yesterday and today.`) // love 值内容
builder.WriteString("\r\n")
// 发送请求报文
_, err = conn.Write([]byte(builder.String()))
if err != nil {
fmt.Println(err)
return
}
// 读取响应报文
resp := make([]byte, 20)
n, err := conn.Read(resp)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(resp[:n]))
}
执行结果:
网络数据包(前面搞错了长度,导致请求一直解析失败,哈哈,最下面才是成功的。):
在终端查看命令,第一个 get love
是在未执行程序前,第二个是在执行程序后:
所以,你说这算是个什么东西?当然了,这里是非常简陋的一个代码,哈哈。