0
点赞
收藏
分享

微信扫一扫

如何手搓一个简易的Redis客户端(RESP协议原理及使用)

small_Sun 2022-02-19 阅读 54

如何手搓一个简易的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
}
举报

相关推荐

0 条评论