Go 实现简易的 redis 客户端

今天突然想看看客户端是如何与 redis server 交互的,所以就想着简单实现一下 redis 的客户端。当我们在使用 redis 的时候,redis 官方也提供了 redis-cli 客户端予以使用,通过一下命令操作,那么依据此,是不是客户端可以这么做呢?是不是遵从着某种 特定的协议呢?

首先通过 Tcp 连接到 redis-server, 保证可通。利用 GO 提供的 net 包,可以很轻松的实现。但是在这之前先定义个 interface,面向对象嘛**#滑稽**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type redis interface {
set(key string, value string) (bool, error)
get(key string) string
del(key string) int
}
type Client struct {
Conn net.Conn
}
// 连接, 特简单
func connect(host string) net.Conn {
conn, err := net.Dial("tcp", host)
if err != nil {
log.Fatalln(err)
}
return conn
}

当使用 redis-cli 的时候,提供的 cli 命令操作。当然 redis 的提供的很多的 API 操作,单下面的例子就以 set get 为例。主要是操作字符串。对于 set 是这样的

1
2
> set blog njphper
> ok

类似这样的一个操作,如果将这里看成一个 im 服务的话, 说明在这里我们向 redis 服务器发送了一个“ set blog njphper” 字符串,redis-server 在收到这个字符串的后,进行了一系列操作,然后返回之后的状态。那么这里肯定会约束双方以怎么样的协议去发送以及返回。好了,这里就需要借助文档了,看一下 redis 协议文档 https://redis.io/topics/protocol
会看到以下信息:

In RESP, the type of some data depends on the first byte:

  • 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 “$” // bulk字符第一个返回$
  • For Arrays the first byte of the reply is “*” // 对于array第一个字符是 *

以上是服务端返回的信息,对于客户端而言,必须当 "\r\n" (CRLF) 结束,当然服务端也是,但是他们之间有一点区别。下面再说。因为 redis 的协议足够简单,所以操作起来还是很方便的。 这里实现以下 set , get 以及 del 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
func(client Client) set(key string, value string) (bool, error) {
var (
res bool
err error
)

client.Conn.Write([]byte(fmt.Sprintf("set %s %s \r\n", key, value)))
reader := bufio.NewReader(client.Conn)
line, _ , err := reader.ReadLine()
if err != nil {
log.Fatalln(err)
}
switch string(line[0]) {
case "+":
res, err = true, nil
case "-":
res, err = false, errors.New(string(line[1:]))
}
// 清空 buff
reader.Reset(client.Conn)
return res, err
}

// 获取字符串
func(client Client) get(key string) string {
_, err := client.Conn.Write([]byte(fmt.Sprintf("get %s \r\n", key)))
if err != nil {
log.Fatalln(err)
}
reader := bufio.NewReader(client.Conn)
// 第一行 redis 返回的状态,这里可以进行一些判断之类的
reader.ReadLine()
// 第二行才是 value 值
line, _ , err := reader.ReadLine()
// 清空 buff
reader.Reset(client.Conn)
return string(line)
}

// 删除字符串
func(client Client) del(key string) int {
_, err := client.Conn.Write([]byte(fmt.Sprintf("del %s \r\n", key)))
if err != nil {
log.Fatalln(err)
}
reader := bufio.NewReader(client.Conn)
line, _ , err := reader.ReadLine()
code, _ := strconv.Atoi(string(line[1:]))
return code
}

来测试一下看看,有没有成功?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 var client redis
conn := connects("127.0.0.1:6379")
client = Client{Conn: conn}
fmt.Println(client.set("hi", "见见空空"))
// 有返回值
fmt.Println(client.get("hi"))
// 设置
fmt.Println(client.set("name", "hello"))
// 获取
fmt.Println(client.get("name"))
// 删除 ,返回了 in(1)
fmt.Println(client.del("name"))
// nil
fmt.Println(client.get("name"))

这里只是简单了解一下 redis,如果需要更加健壮的 redis 客户端,还是找一些开源包比较靠谱,毕竟轮子不需要再造一遍,可以了解,但没必要在自己花费精力造一遍 。这里还要提一下,go 的 interface 真好用,个人比较虽然倾向这种隐示的实现