用go实现简单的redis客户端

功能

redis客户端的职责非常简单,就是将命令按照RESP协议序列化后发给redis-server,再将返回数据按照RESP协议解析

RESP协议

主要规定就以下五条

  • For Simple Strings the first byte of the reply is “+”
  • For Errors the first byte of the reply is “-”
  • For Integers the first byte of the reply is “:”
  • For Bulk Strings the first byte of the reply is “$”
  • For Arrays the first byte of the reply is “*

同时每个部分结束时,都要加上\r\n两个字符

还有一些诸如null的表示方法的规定,详细看https://redis.io/topics/protocol

举个🌰

对于命令set key value

需要序列化为"*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"


接下来开始实现一个简单的redis客户端

tcp连接

和redis-server进行通讯,首先要建立tcp连接,redis-server默认监听6379端口

新建文件conn.go负责进行tcp连接

package myRedisCli

import (
	"log"
	"net"
)

const address = "127.0.0.1:6379"

type RedisConn struct {
	conn net.Conn
}

var redisConn RedisConn

func init() {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		panic(err)
	}
	redisConn.conn = conn
	log.Println("connection ready")
}

func getConn() *RedisConn {
	return &redisConn
}

这里只建立了一个连接,用单例的方式提供给全局使用

封装读写操作

将连接的读写操作封装为RedisConn的方法

加上日志监控

package myRedisCli

func (redisConn *RedisConn) read(data []byte) error {
	n, err := redisConn.conn.Read(data)
	if err != nil {
		Error.Printf("read data error: %+v", err)
	}
	Info.Printf("read %+v bytes", n)
	return err
}
package myRedisCli

func (redisConn *RedisConn) Write(data []byte) error {
	n, err := redisConn.conn.Write(data)
	if err != nil {
		Error.Printf("write data error: %+v\n", err)
	}
	Info.Printf("write %+v bytes", n)
	return err
}

协议层

按照RESP协议序列化命令字符串

因为我只实现测试了SETGET命令,所以还未全部实现所有RESP协议的转换

  • Simple String

序列化时,在前面加上+,在尾部加上\r\n

func wrapSimpleString(s string) []byte {
	return []byte("+" + s + "\r\n")
}

解析时,也是提取中中间的字符串即可

func parseSimpleString(data []byte) string {
	var msg []byte
	for _, v := range data {
		if v == '+' {
			continue
		} else if v == '\r' {
			break
		}
		msg = append(msg, v)
	}
	return string(msg)
}
  • Bulk String

比起Simple String稍微复杂点,需要将字符串的长度信息也序列化进字节数组内

func wrapBulkString(s string) []byte {
	var res []byte
	n := strconv.FormatInt(int64(len(s)), 10)
	res = append(res, []byte("$"+n+"\r\n")...)
	res = append(res, []byte(s+"\r\n")...)
	return res
}

解析时,先提取出字符串长度,然后就可以找到字符串在字节数组中的实际位置

func parseBulkString(data []byte) string {
	var length, msg []byte
	idx := 1
	for data[idx] != '\r' {
		length = append(length, data[idx])
		idx++
	}
	n, _ := strconv.ParseInt(string(length), 10, 64)
	msg = data[idx+2 : idx+2+int(n)]
	return string(msg)
}
  • Intergers

数字和简单字符串一样简单,123序列化为:123\r\n

func wrapInteger(n int64) []byte {
   s := strconv.FormatInt(n, 10)
   return []byte(":" + s + "\r\n")
}
func parseInteger(data []byte) int64 {
	var msg []byte
	for _, v := range data {
		if v == ':' {
			continue
		} else if v == '\r' {
			break
		}
		msg = append(msg, v)
	}
	n, _ := strconv.ParseInt(string(msg), 10, 64)
	return n
}
  • Arrays

根据参数的实际类型,用不同的协议规则序列化

func wrapArray(s ...interface{}) []byte {
	var res []byte
	n := strconv.FormatInt(int64(len(s)), 10)
	res = append(res, []byte("*"+n+"\r\n")...)
	for _, v := range s {
		switch v.(type) {
		case string:
			res = append(res, wrapBulkString(v.(string))...)
		case int64:
			res = append(res, wrapInteger(v.(int64))...)
		case int:
			res = append(res, wrapInteger(int64(v.(int)))...)
		}
	}
	return res
}
  • Errors

如果发送的命令格式有误,会收到redis-server回复的Error信息

解析过程和简单字符串一样

func parseError(data []byte) string {
	var msg []byte
	for _, v := range data {
		if v == '+' {
			continue
		} else if v == '\r' {
			break
		}
		msg = append(msg, v)
	}
	return string(msg)
}

最后再设置一个路由,将不同前缀的字节数组发给不同的解析函数

func ParseRsp(data []byte) (interface{}, error) {
   switch data[0] {
   case '-':
      return "", errors.New(parseError(data))
   case '+':
      return parseSimpleString(data), nil
   case '$':
      return parseBulkString(data), nil
   default:
      return nil, nil
   }
}

应用层

有了协议层提供的序列化和解析方法,就可以开始组装命令

SET方法

仅仅实现了SET key value两个参数的形式,多个参数其实也只需确保正确序列化就可以

func (redisConn *RedisConn) Set(key, value string) (bool, error) {
	set := []interface{}{"SET", key, value}
	data := wrapArray(set)
	err := redisConn.Write(data)
	if err != nil {
		return false, err
	}
	rsp := make([]byte, 1024)
	err = redisConn.read(rsp)
	if err != nil {
		return false, err
	}
	ok, err := ParseRsp(rsp)
	if err != nil || ok.(string) != "OK" {
		return false, err
	}
	return true, nil
}
GET方法

仅仅实现了GET key单个参数的形式

func (redisConn *RedisConn) Get(key string) (string, error) {
	get := []interface{}{"GET", key}
	data := wrapArray(get)
	err := redisConn.Write(data)
	if err != nil {
		return "", err
	}
	rsp := make([]byte, 1024)
	err = redisConn.read(rsp)
	if err != nil {
		return "", err
	}
	res, err := ParseRsp(rsp)
	if err != nil {
		return "", err
	}
	return res.(string), nil
}

写单测进行测试

package myRedisCli

import (
   "fmt"
   "testing"
)

func TestSet(t *testing.T) {
   conn := getConn()
   ok, err := conn.Set("123", "abc")
   if err != nil || !ok {
      fmt.Println("fail", err)
   } else {
      fmt.Println("success")
   }
}

func TestGet(t *testing.T) {
   conn := getConn()
   value, err := conn.Get("123")
   if err != nil {
      Error.Println("fail", err)
   } else {
      fmt.Println(value)
   }
}

启动redis-server后,用命令

go test -run TestSet
go test -run TestGet

即可分别测试两个命令

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值