使用 Go 语言实现模拟 redis 协议的服务(二)RESP协议

Redis 客户端与服务端通信,使用 RESP 协议,RESP 协议全程: Redis Serialization Protocol 即 Redis 序列化协议,专为 Redis 设计。RESP 协议是非TCP 专用的技术,但在 Redis 的环境中,该协议仅用于 TCP 连接。

RESP 可以序列化不同的数据类型,如整数(integers),字符串(strings),数组(arrays)。它还使用了一个特殊的类型来表示错误(errors)。请求以字符串数组的形式来表示要执行命令的参数从客户端发送到Redis服务器。Redis使用命令特有(command-specific)数据类型作为回复。RESP协议是二进制安全的,并且不需要处理从一个进程传输到另一个进程的块数据的大小,因为它使用前缀长度(prefixed-length)的方式来传输块数据的。

一、RESP 协议原理

在RESP协议中,一共将传输的数据分为5类最小单元类型,单元结束统一使用换行符号 \r\n (CRLF) 表示

  • 对于简单字符串,回复的第一个字节是“+”
  • 对于错误,回复的第一个字节是“ - ”
  • 对于整数,回复的第一个字节是“:”
  • 对于多行字符串,回复的第一个字节是“$”
  • 对于数组,回复的第一个字节是“*”

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简单字符串hello world
+hello wolrd\r\n
// 多行字符串hello world
$11\r\nhello world\r\n // $11 表示字符串长度为11
// 整数1024
:1024\r\n
// 数组[1,2,3]
*3\r\n:1\r\n:2\r\n:3\r\n
// 错误信息
-ERR Protocol error: invalid multibulk length

// 特殊情况
// NULL 用多行字符串表示,长度-1
$-1\r\n
// 空字符串用多行字符串表示,长度为0
$0\r\n\r\n

以上,可以用 telnetredis-cli 来连接 redis server,对比看下返回值结构,telnet 显示的就是未解析的 resp 协议字符串(\r\n 会换行)redis-cli 显示的则是解析过的结果:

redis-cli telnet
image image
有意思的对比:hgetall v.s. scan
image image

可以看到 scan 命令返回的是个二维数组,第一个 *2 表示有两个返回值,然后 *10 表示列表数组的10个元素

Redis客户端向服务器发送的指令,只有一种格式,就是多行字符串数组

比如:set username tom, 格式化之后的指令是:

1
2
3
4
5
6
7
8
9
10
*3\r\n$3\r\nset\r\n$8\r\nusername\r\n$3\r\ntom\r\n

格式解析(每一部分之间用\r\n间隔):
*3 => 参数数量为3
$3 => 第一个字符串长度为 3
set => 第一个字符串内容为 set
$8 => 第二个字符串长度为 8
username => 第二个字符串内容为 username
$3 => 第三个参数长度为 3
tom => 第三个参数内容为tom

二、RESP 编码解码的 GoLang 实现

1、RESP 解码(客户端消息解析)

前面说了 Redis 客户端向服务器发送的指令,只有一种格式,就是多行字符串数组。

先定义一个 Request 结构体,作为输入指令解析结果的标准化,返回给后续逻辑处理:

1
2
3
4
5
type Request struct {
Command string
Arguments [][]byte
Connection io.ReadCloser // 可接受 net.Conn
}

为方便解析和校验参数,读取输入参数的方式由原来的 conn.Read(buf) 替换为使用 bufio.NewReader(conn)

接下来开始解析(从 conn 开始)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 接前文(Go 语言实现TCP服务)
* conn 入参可以是 net.Conn 对象
*/
func NewRequest(conn io.ReadCloser) (*Request, error) {
reader := bufio.NewReader(conn)
// reader 读到第一个 \n 结束返回,下次从此处继续往后读,
// 此处 line 得到的值是 *%d\r\n 格式
line, err := reader.ReadString('\n')
if err != nil {
return nil, err
}

// 格式校验,入参多行字符串数组以 *%d\r\n 开头表示参数个数
if line[0] != '*' {
return nil, fmt.Errorf("new request error")
}

var argCount int // 解析参数个数
if _, err := fmt.Sscanf(line, "*%d\r\n", &argCount); err != nil {
return nil, fmt.Errorf("mailformed request: %s does not match %s", line, "*<#Arguments>")
}

// $<number of bytes of argument 1>CRLF
// <argument data> CRLF
command, err := readArgument(reader)
if err != nil {
return nil, err
}

arguments := make([][]byte, argCount-1)
for i := 0; i < argCount-1; i++ {
if arguments[i], err = readArgument(reader); err != nil {
return nil, err
}
}

return &Request{
Command: strings.ToUpper(string(command)),
Arguments: arguments,
Connection: conn,
}, nil
}

// 解析参数内容
func readArgument(reader *bufio.Reader) ([]byte, error) {
line, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("mailformed request: %s does not match %s", line, "$<ArgumentLength>")
}

var argLength int
if _, err := fmt.Sscanf(line, "$%d\r\n", &argLength); err != nil {
return nil, fmt.Errorf("mailformed request: %s does not match %s", line, "$<ArgumentLength>")
}

// 通过入参长度获取参数内容
data, err := ioutil.ReadAll(io.LimitReader(reader, int64(argLength)))
if err != nil {
return nil, err
}
if len(data) != argLength { // 参数长度校验
return nil, fmt.Errorf("mailformed request: argument length %d does not match %d", argLength, len(data))
}
if b, err := reader.ReadByte(); err != nil || b != '\r' { // 参数格式 \r\n 校验
return nil, fmt.Errorf("mailformed request: line should end with CRLF")
}
if b, err := reader.ReadByte(); err != nil || b != '\n' {
return nil, fmt.Errorf("mailformed request: line should end with CRLF")
}
return data, nil
}

重点:

  • 解析参数开头格式,参数数量
  • 通过参数(含命令)长度读取参数内容
  • 校验结束符号 CRLF(\r\n)
2、RESP编码(服务端返回信息)

服务端返回信息有类型区分,可根据前文提到的五种数据类型分别组织:

1
2
3
4
5
6
7
8
9
10
11
12
// 错误信息
conn.Write([]byte("-EERROR" + errMsg + "\r\n"))
// 简单信息,入操作成功; 比如 +OK
conn.Write([]byte("+" + info + "\r\n"))
// 数字返回,如incr命令返回结果
conn.Write([]byte(":" + strconv.FormatInt(number, 10) + "\r\n"))
// null
conn.Write([]byte("$-1\r\n"))
// 字符串信息,比如get/hget
conn.Write([]byte("$" +strconv.Atoi(len(msg))+ "\r\n" + msg + "\r\n"))
// 返回多行字符串信息、嵌套数据,比如scan 0 返回只有一个key时
conn.Write([]byte("*2\r\n$1\r\n0\r\n*1\r\n$8\r\nusername\r\n")

为便于阅读和维护,对消息返回可做如下封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
type Reply io.WriterTo // interface, WriteTo方法往

type ErrorReply struct {
message string
}

func (er *ErrorReply) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write([]byte("-ERROR " + er.message + "\r\n"))
return int64(n), err
}
...

type BulkReply struct {
value []byte
}

func (r *BulkReply) WriteTo(w io.Writer) (int64, error) {
return writeBytes(r.value, w)
}

type MultiBulkReply struct {
values [][]byte
}

func (r *MultiBulkReply) WriteTo(w io.Writer) (int64, error) {
if r.values == nil {
return 0, fmt.Errorf("Multi bulk reply found a nil values")
}
if wrote, err := w.Write([]byte("*" + strconv.Itoa(len(r.values)) + "\r\n")); err != nil {
return int64(wrote), err
} else {
total := int64(wrote)
for _, value := range r.values {
wroteData, err := writeBytes(value, w)
total += wroteData
if err != nil {
return total, err
}
}
return total, nil
}
}

func writeNullBytes(w io.Writer) (int64, error) {
n, err := w.Write([]byte("$-1\r\n"))
return int64(n), err
}

func writeBytes(value interface{}, w io.Writer) (int64, error) {
if value == nil {
return writeNullBytes(w)
}
switch v := value.(type) {
case []byte:
if len(v) == 0 {
return writeNullBytes(w)
}
buf := []byte("$" + strconv.Itoa(len(v)) + "\r\n")
buf = append(buf, v...)
buf = append(buf, []byte("\r\n")...)
n, err := w.Write(buf)
if err != nil {
return 0, err
}
return int64(n), nil
case int:
wrote, err := w.Write([]byte(":" + strconv.Itoa(v) + "\r\n"))
return int64(wrote), err
}
return 0, fmt.Errorf("invalid type sent to WriteBytes")
}


【参考文档】

1、最详细的Redis通信协议规范

2、Redis深度历险 - 核心原理与应用实践(钱文品 著)


项目代码:https://github.com/silov/redis-protocol-cook