2. Redis通信协议

454 阅读2分钟

我正在参加「掘金·启航计划」。主要内容是对于校招练手项目的解析,促进自己更好地理解项目,理解redis。
Redis自2.0版本后,使用统一的基于TCP的应用层协议RESP(Redis Serialization Protocal).是一种二进制安全的文本协议,工作于TCP协议上。RESP以行为单位,客户端和服务器发送到命令或数据以\r\n(CRLF)作为换行符。

二进制安全:允许协议中出现任意字符而不会导致故障。如C语言的字符串以\0为为结尾,因此不允许字符串中间出现\0,而Go语言的string允许。因此我们说Go语言的string是二进制安全的,而C语言的字符串不是二进制安全的。

RESP的二进制安全允许我们在key或value中包含\n\r这样的特殊字符。使用redis存储protobuf、msgpack等二进制数据时,二进制安全性尤为重要。

RESP定义了5种格式:

  • 对于简单字符串,回复的第一个字节是“+”
  • 对于Errors,回复的第一个字节是“-”
  • 对于整数,回复的第一个字节是“:”
  • 对于Bulk Strings,回复的第一个字节是“$”
  • 对于数组,回复的第一个字节是“ ***** ”
  • Error:类似于简单字符串。 -Error message\r\n 仅在问题出现时发送。
  • Simple String:+OK\r\n 中间的字符串不能包含CR或LF字符的字符串,不允许换行符,且以CRLF终止(\r\n)
  • Integer: 以CRLF结尾的字符串,表示一个整数,前缀为:。如:0\r\n。返回的整数在有符号的64位整数范围内。
  • Bulk String:批量字符串。表示最长为512MB的单个二进制安全字符串。如“hello”的编码“5\nhello˚\n˚"。也可以使用特殊格式表示值的不存在来表示Null,如"5\r\nhello\r\n"。也可以使用特殊格式表示值的不存在来表示Null,如"-1\r\n",这是Null Bulk String
    • 前缀为$字符串的字节数,以CRLF终止
    • 字符串数据
    • CRLF
  • Array
    • *作为第一个字节,后跟数组中的元素数为十进制数,CRLF
    • Array的每个元素的附加RESP类型

协议解析器

协议解析器将实现TCP服务器中handler接口,充当应用层服务器。

协议解析器将接收Socket传输的数据,并将数据还原成为[][]byte格式。如"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\value\r\n"会被还原成[][]byte格式。

来自客户端的请求均为数组格式,它在第一行中标记保文总行数,并使用CRLF作为分行符。

bufio标准库可以将从reader中读取的数据缓存到buffer中,直到遇到分隔符或读取完毕返回,所以使用reader.ReadBytes('\n')来保证每次读取到完整的一行。

RESP是二进制安全的协议,允许在正文中使用CRLF字符。ReadBytes('\n')会将换行符误认为多行,因此需要io.ReadFull(reader,msg)来读取指定长度的内容。

在这一部分的主要内容,是作为协议解析器,按照resp的规定,解析客户端传递的数据,将数据还原成为[]byte

了解交互过程

  • 客户端向redia服务器发送一个仅包含Bulk String的Resp数组,在第一行中标记保文总行数
  • redis服务端回复客户端,发送有效的resp数据类型作为回复

定义协议解析器的接口

客户端的数据使用resp协议进行解析,返回redis.Reply或error。

//Payload store redis.Reply or error
type Payload struct {
	Data redis.Reply
	Err  error
}

其中,对于解析后返回的Reply的总接口,需要所有解析类型都要实现。

//interface/redis/reply.go
// Reply resp 信息接口
type Reply interface {
	ToBytes() []byte
}

对于输入的数据,会将状态封装成为readState,反应当前的过程

type readState struct {
	readingMultiLine  bool     //多行读入
	expectedArgsCount int      //Reply中数据个数
	msgType           byte     //根据前缀确定字符类型
	args              [][]byte //解析后的数据
	bulkLen           int64    //缓冲区中读入的参数的长度
	readingRepl       bool
}

在协议解析器中,主要实现ParseStreamParseOne两个主要功能

ParseStream(io.Reader)<-chan *Payload

通过io.Reader读取数据,通过channel将结果返回给调用者

ParseOne(data []byte)(redis.Reply,error)

解析[]byte,以redis.Reply的格式返回。相当于得到*Payload后,返回详细数据。

两个方法都涉及到了一个核心方法,parse0(io.Reader,chan<-*Payload) 特别解析一些。

parse0(io.Reader,chan<-*Payload)

  • defer 恢复panic
  • 来自客户端的请求均为数组格式,在第一行中标记保文总行数,并使用CRLF作为分行符。bufio标准库可以将从reader中读到的数据缓存到buffer中,直到遇到分隔符或读取完毕后返回。
	defer func() {
		if err := recover(); err != nil {
			logger.Error(err, string(debug.Stack()))
		}
	}()
  • 开启带有缓冲区的Reader,for读入客户端的输入。读入的方法需要兼容单行和多行的数据类型,自定义工具方法readLine(*bufio.Reader,state *readState).开始对输入进行判断,并且将状态写入ReadState。

bufReader := bufio.NewReader(reader)

根据resp所支持的数据类型,初步分为需要单行读取的数据和需要多行读取的数据。因为对于string bulks和array这样的数据类型,按照规范会存在多个\r\n,仅使用readBytes('/n')无法满足数据类型读取要求,需要ReadFull()

  • 对于readLine方法。
    • 如果当前缓冲区读入为空,按行读取缓冲区信息。
    • 如果当前缓冲区读入不为空,通过state.bulkLen告诉下一行BulkString的长度,readFull指定长度全部读入。读完后将缓存区数据置空,返回数据
  • 根据state的readingMultiLine属性
    • 1.1 前缀为* 数据类型为数组
    • 1.2 前缀为$ 数据类型为Bulk Strings
    • 1.3 单行 转读取类型
    • 对于数据信息的解析,需要去除前缀和后缀换行符。并将数据如信息类型/能否读多行/数据大小/读取参数封装到state,方便下次读取;如果读取单行数据,那么直接根据前缀决定类型,根据类型确定读取方式。
    • 2.1 读取完成,将参数加入最终的参数数组
  • 当数据读取完全后,通过ch<-&Payload返回数据

读取类型

所有数据类型都需要实现redis.Reply()的ToBytes()根据不同的数据类型有不同的实现方式。

SimpleString

/*----simple string reply ----*/

//StatusReply store a simple  string
type StatusReply struct {
	status string
}

func MakeStatusReply(status string) *StatusReply {
	return &StatusReply{
		status: status,
	}
}

//ToBytes interface/reply
func (s *StatusReply) ToBytes() []byte {
	return []byte("+" + s.status + CRLF)
}

Error

/*------ Error Reply-----*/

//StandardErrReply server error
type StandardErrReply struct {
	status string
}

type ErrorReply interface {
	Error() string
	ToBytes() []byte
}

func MakeErrReply(status string) *StandardErrReply {
	return &StandardErrReply{
		status: status,
	}
}
func (e *StandardErrReply) ToBytes() []byte {
	return []byte("-" + e.status + CRLF)
}

Integer

/*---- Int Reply ---*/
type IntReply struct {
	Code int64
}

func MakeIntReply(code int64) *IntReply {
	return &IntReply{
		Code: code,
	}
}

//ToBytes format redis.Reply
func (i *IntReply) ToBytes() []byte {
	return []byte(":" + strconv.FormatInt(i.Code, 10) + CRLF)
}

Bulk String

/*-----Bulk reply----*/

type BulkReply struct {
	Arg []byte
}

func MakeBulkReply(arg []byte) *BulkReply {
	return &BulkReply{
		Arg: arg,
	}
}

func (reply *BulkReply) ToBytes() []byte {
	if len(reply.Arg) == 0 {
		return nullBulkReplyBytes
	}
	return []byte("$" + strconv.Itoa(len(reply.Arg)) + CRLF + string(reply.Arg) + CRLF)

}

Array

/*-----Multi Bulk Reply----*/

//MultiBulkReply array
type MultiBulkReply struct {
	Args [][]byte
}

func MakeMultiBulkReply(args [][]byte) *MultiBulkReply {
	return &MultiBulkReply{
		Args: args,
	}
}

func (b *MultiBulkReply) ToBytes() []byte {
	argLen := len(b.Args)
	var buf bytes.Buffer
	buf.WriteString("*" + strconv.Itoa(argLen) + CRLF)
	for _, arg := range b.Args {
		if arg == nil {
			//Bulk Strings
			buf.WriteString("$-1" + CRLF)
		} else {
			buf.WriteString("$" + strconv.Itoa(len(arg)) + CRLF + string(arg) + CRLF)
		}
	}
	return buf.Bytes()
}

如何实现resp协议?

协议的关键是维护了一个readState类型,记录了数据类型,缓冲区数据个数,解析后数据等各种属性。客户端给服务器传输数据时,是以Array的形式传递BlukString,在第一行会标记行数。因此从缓冲区读入数据后,会更新这些数据,确定下次读取数据是否需要换行,如果需要换行可以调用readAll读取指定大小的数据。当进行单行读取时,不同的数据类型有不同的读取规则,根据不同的读取规则,去除前后缀,进行信息读取。读取结束后,需要实现接口的ToBytes(),包装接口使其具有扩展性,根据不同类型的ToBytes()向channel进行数据的传递。

测试

func TestParseStream(t *testing.T) {
	replies := []redis.Reply{
		protocol.MakeIntReply(1),                 //Integer 1
		protocol.MakeStatusReply("OK"),           //Simple error OK
		protocol.MakeErrReply("ERR unknown"),     //Error ERR unknown
		protocol.MakeBulkReply([]byte("a\r\bb")), //测试二进制安全
		protocol.MakeNullBulkReply(),             //null Bulk String
		protocol.MakeMultiBulkReply([][]byte{
			[]byte("a"),
			[]byte("\r\n"), //array中二进制安全
		}),
		protocol.MakeEmptyMultiBulkReply(), //null array
	}
	reqs := bytes.Buffer{}
	for _, re := range replies {
		//将msg写入缓冲区
		reqs.Write(re.ToBytes())
	}
	reqs.Write([]byte("set a a" + protocol.CRLF))
	//将数据拷贝进数组 方便对比
	expected := make([]redis.Reply, len(replies))
	copy(expected, replies)
	//写入该数据是为了对比 text plain
	expected = append(expected, protocol.MakeMultiBulkReply([][]byte{
		[]byte("set"), []byte("a"), []byte("a"),
	}))
	ch := ParseStream(bytes.NewReader(reqs.Bytes()))
	i := 0
	for payload := range ch {
		if payload.Err != nil {
			if payload.Err == io.EOF {
				return
			}
			t.Error(payload.Err)
			return
		}
		if payload.Data == nil {
			t.Error("empty data")
			return
		}
		exp := expected[i]
		i++
		if !utils.BytesEquals(exp.ToBytes(), payload.Data.ToBytes()) {
			t.Error("parse failed" + string(exp.ToBytes()))
		}
	}
}

注释

strconv.ParseUint

类似于ParseInt,但是作用于无符号数,将string->uint

参考链接

redis.io/docs/refere…

www.cnblogs.com/Finley/p/11…