功能
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协议序列化命令字符串
因为我只实现测试了SET
和GET
命令,所以还未全部实现所有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
即可分别测试两个命令