如何手搓一个简易的Redis客户端(RESP协议原理及使用)
客户端完整代码(已封装成模块包,可直接导入使用)
Gitee:https://gitee.com/dpwgc/easy-go-redis
Github:https://github.com/dpwgc/easy-go-redis
什么是RESP协议
RESP是一个基于TCP的应用层协议,通过TCP传输数据并根据规则解析数据。Redis服务端与客户端之间就采用RESP通讯。
RESP的优点
简单、易读、二进制安全、解析速度快、可序列化多种类型的数据(整数、字符串、数组及错误类型)。
RESP示例
- 这是一条Redis指令,插入一个键为test,值为hello的string类型对象:
SET test hello
实际上Redis客户端并不会直接将这条命令通过TCP发送给Redis服务端,而是要将命令转成RESP格式后,再发送该命令。
- 这是"SET test hello"这条命令转成RESP格式后的样子:
*3
$3
SET
$4
test
$5
hello
可以很轻易地找到规律,第一行*3表明该命令由三部分构成(SET, KEY, VALUE),第二行$3则表示该命令的第一部分("SET"字符串)长度为3。
在RESP中,数据的类型取决于第一个字节:
-
‘+’:表示简单字符串。例:上文的SET命令在插入成功后,Redis服务端会返回 “+OK” 字符串。
-
‘-’:表示错误类型。例:向Redis发送auth密码登录命令后,如果服务端报错,则会返回:"-ERR Client sent AUTH, but no password is set"。
-
‘$’:表示下面的一行是命令的一部分,后面跟着的数字是该部分字符串的长度
-
‘*’:表示数组,数组的元素是命令的各个部分。例:上文的SET命令就是一个长度为3的数组[set, test, hello]
在RESP协议中,不同部分以 “\r\n”(CRLF)结束。上文RESP格式的SET命令转成字符串后的样子:
*3\r\n$3\r\nSET\r\n$4\r\ntest\r\n$5\r\nhello\r\n
编写Redis客户端(Golang)
搭建TCP连接
//创建TCP连接
func newConn(addr string, port string) (*net.TCPConn, error) {
tcpAddr := fmt.Sprintf("%s:%s", addr, port)
server, err := net.ResolveTCPAddr("tcp4", tcpAddr)
if err != nil {
return nil, err
}
//建立服务器连接
conn, err := net.DialTCP("tcp", nil, server)
if err != nil {
return nil, err
}
return conn, nil
}
//从TCP连接中读取数据
func read(conn *net.TCPConn) (string, error) {
buffer := make([]byte, 1024)
msg, err := conn.Read(buffer) //接受服务器信息
if err != nil {
return "", nil
}
//[:msg]去除buffer数据前的无效字节
return string(buffer[:msg]), nil
}
//向TCP服务端发送数据
func send(conn *net.TCPConn, data string) (string, error) {
_, err := conn.Write([]byte(data)) //给服务器发信息
if err != nil {
return "", err
}
//获取服务端的反馈信息
return read(conn)
}
与Redis服务端建立连接
// Conn 与redis建立连接
func Conn(addr string, port string) (*net.TCPConn, error) {
//调用TCP建立连接
return newConn(addr, port)
}
向Redis服务端发送命令
// Do 执行命令
func Do(conn *net.TCPConn, arr ...string) (string, error) {
//RESP命令字符串
cmd := ""
//获取数组长度
count := strconv.Itoa(len(arr))
//拼接命令字符串
cmd = fmt.Sprintf("%s%s%s\r\n", cmd, "*", count)
//循环拼接arr数组
for _, a := range arr {
size := strconv.Itoa(len(a))
cmd = fmt.Sprintf("%s%s%s\r\n", cmd, "$", size)
cmd = fmt.Sprintf("%s%s\r\n", cmd, a)
}
//调用TCP发送数据
return send(conn, cmd)
}
解析Redis服务端发来的消息
// Analytic 解析Redis返回的数据(将RESP的字符串格式转为string字符串数组)
func Analytic(data string) []string {
var res []string
arr := strings.Split(data, "\r\n") //按CTRL切割RESP字符串
size := len(arr) - 1 //去掉末尾的空字符串
//如果返回的RESP字符串是单行
//例:
//+OK
if size == 1 {
res = append(res, arr[0])
return res
}
//如果返回的RESP字符串是双行
//例:
//$4
//test
if size == 2 {
res = append(res, arr[1])
return res
}
//如果返回的RESP字符串是多行(三行及以上)
//例:
//*2
//$4
//test
//$5
//hello
for i := 2; i < size; i = i + 2 {
res = append(res, arr[i])
}
return res
}
关闭连接
// Close 关闭连接
func Close(conn *net.TCPConn) error {
err := conn.Close()
if err != nil {
return err
}
return nil
}