我正在参加「掘金·启航计划」。主要内容是对于校招练手项目的解析,促进自己更好地理解项目,理解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”的编码“-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
}
在协议解析器中,主要实现ParseStream和 ParseOne两个主要功能
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