原文地址: 用 Go 来了解一下 Redis 通信协议
Go、PHP、Java... 都有那么多包来支撑你使用 Redis,那你是否有想过html
有了服务端,有了客户端,他们俩是怎样通信,又是基于什么通信协议作出交互的呢?git
基于咱们的目的,本文主要讲解和实践 Redis 的通信协议github
Redis 的客户端和服务端是经过 TCP 链接来进行数据交互, 服务器默认的端口号为 6379golang
客户端和服务器发送的命令或数据一概以 \r\n
(CRLF)结尾(这是一条约定)redis
在 Redis 中分为请求和回复,而请求协议又分为新版和旧版,新版统一请求协议在 Redis 1.2 版本中引入,最终在 Redis 2.0 版本成为 Redis 服务器通讯的标准方式安全
本文是基于新版协议来实现功能,不建议使用旧版(1.2 挺老旧了)。以下是新协议的各类范例:服务器
一、 格式示例app
*<参数数量> CR LF $<参数 1 的字节数量> CR LF <参数 1 的数据> CR LF ... $<参数 N 的字节数量> CR LF <参数 N 的数据> CR LF
在该协议下全部发送至 Redis 服务器的参数都是二进制安全(binary safe)的tcp
二、打印示例命令行
*3 $3 SET $5 mykey $7 myvalue
三、实际协议值
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
这就是 Redis 的请求协议规范,按照范例1编写客户端逻辑,最终发送的是范例3,相信你已经有大体的概念了,Redis 的协议很是的简洁易懂,这也是好上手的缘由之一,你能够想一想协议这么定义的好处在哪?
Redis 会根据你请求协议的不一样(执行的命令结果也不一样),返回多种不一样类型的回复。在这个回复“协议”中,能够经过检查第一个字节,肯定这个回复是什么类型,以下:
有了回复的头部标识,结尾的 CRLF,你能够大体猜测出回复“协议”是怎么样的,可是实践才能得出真理,斎知道怕是你很快就忘记了 😀
package main import ( "log" "net" "os" "github.com/EDDYCJY/redis-protocol-example/protocol" ) const ( Address = "127.0.0.1:6379" Network = "tcp" ) func Conn(network, address string) (net.Conn, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } return conn, nil } func main() { // 读取入参 args := os.Args[1:] if len(args) <= 0 { log.Fatalf("Os.Args <= 0") } // 获取请求协议 reqCommand := protocol.GetRequest(args) // 链接 Redis 服务器 redisConn, err := Conn(Network, Address) if err != nil { log.Fatalf("Conn err: %v", err) } defer redisConn.Close() // 写入请求内容 _, err = redisConn.Write(reqCommand) if err != nil { log.Fatalf("Conn Write err: %v", err) } // 读取回复 command := make([]byte, 1024) n, err := redisConn.Read(command) if err != nil { log.Fatalf("Conn Read err: %v", err) } // 处理回复 reply, err := protocol.GetReply(command[:n]) if err != nil { log.Fatalf("protocol.GetReply err: %v", err) } // 处理后的回复内容 log.Printf("Reply: %v", reply) // 原始的回复内容 log.Printf("Command: %v", string(command[:n])) }
在这里咱们完成了整个 Redis 客户端和服务端交互的流程,分别以下:
一、读取命令行参数:获取执行的 Redis 命令
二、获取请求协议参数
三、链接 Redis 服务器,获取链接句柄
四、将请求协议参数写入链接:发送请求的命令行参数
五、从链接中读取返回的数据:读取先前请求的回复数据
六、根据回复“协议”内容,处理回复的数据集
七、输出处理后的回复内容及原始回复内容
func GetRequest(args []string) []byte { req := []string{ "*" + strconv.Itoa(len(args)), } for _, arg := range args { req = append(req, "$"+strconv.Itoa(len(arg))) req = append(req, arg) } str := strings.Join(req, "\r\n") return []byte(str + "\r\n") }
经过对 Redis 的请求协议的分析,可得出它的规律,先加上标志位,计算参数总数量,再循环合并各个参数的字节数量、值就能够了
func GetReply(reply []byte) (interface{}, error) { replyType := reply[0] switch replyType { case StatusReply: return doStatusReply(reply[1:]) case ErrorReply: return doErrorReply(reply[1:]) case IntegerReply: return doIntegerReply(reply[1:]) case BulkReply: return doBulkReply(reply[1:]) case MultiBulkReply: return doMultiBulkReply(reply[1:]) default: return nil, nil } } func doStatusReply(reply []byte) (string, error) { if len(reply) == 3 && reply[1] == 'O' && reply[2] == 'K' { return OkReply, nil } if len(reply) == 5 && reply[1] == 'P' && reply[2] == 'O' && reply[3] == 'N' && reply[4] == 'G' { return PongReply, nil } return string(reply), nil } func doErrorReply(reply []byte) (string, error) { return string(reply), nil } func doIntegerReply(reply []byte) (int, error) { pos := getFlagPos('\r', reply) result, err := strconv.Atoi(string(reply[:pos])) if err != nil { return 0, err } return result, nil } ...
在这里咱们对全部回复类型进行了分发,不一样的回复标志位对应不一样的处理方式,在这里需求注意几项问题,以下:
一、当请求的值不存在,会将特殊值 -1 用做回复
二、服务器发送的全部字符串都由 CRLF 结尾
三、多条批量回复是可基于批量回复的,要注意理解
四、无内容的多条批量回复是存在的
最重要的是,对不一样回复的规则的把控,可以让你更好的理解 Redis 的请求、回复的交互过程 👌
写这篇文章的原由,是由于经常在使用 Redis 时,只是用,你不知道它是基于什么样的通信协议来通信,这样的感受是十分难受的
经过本文的讲解,我相信你已经大体了解 Redis 客户端是怎么样和服务端交互,也清楚了其所用的通信原理,但愿可以对你有所帮助!
最后,若是想详细查看代码,右拐项目地址:https://github.com/EDDYCJY/re...
若是对你有所帮助,欢迎点个 Star 👍