redis.v5是一款基于golang的redis操作库,封装了对redis的各种操作
源码地址是
https://github.com/go-redis/redis
Redis客户端的工作本质上是基于tcp协议向redis server传输符合redis协议的命令请求,并根据redis协议解析server端的返回值
我们可以通过telnet工具来模拟这一过程,例如ping命令我们可以这样发送请求
$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
// 以下是发送的内容
*1
$4
PING
// 这是redis server返回内容
+PONG
所以要想理解redis客户端,首先要熟悉redis协议
redis的协议由请求协议和响应协议两部分组成,都是非常简单的通讯协议,易于程序解析,也方便人类进行阅读
需要注意一点的是早期版本的redis协议和如今的不太一样,所以特别提醒的是本文是基于redis 3.2.6版本。
请求协议:
* <参数数量> CR LF
$ <参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$ <参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
我们以开头的 telnet模拟发送 ping 命令 作为例子
其中第一行星号后面表示本次传输的命令个数。1表示本次请求只有一个参数,同样的道理对于get命令而言,参数是两个(get key),所以对于get参数而言应该写成2
紧接着后面开始一个一个传递请求参数,每一个参数用两行表示,其中上一行$n表示参数的字符数,下一行是参数的字符串
例如上面的例子,$4表示这个命令有4个字符,下一行的ping就是该命令的字符串表示
同样的道理,set命令可以这样写
*3
$3
SET
$3
key
$5
value
用byte数组可以这样写
"*3\r\n$3\r\nset\r\n$3key\r\n$5value\r\n"
返回值是
+OK
说明命令被成功解析并执行
响应协议:
说完了请求协议,我们再来看看响应协议,与拥有统一格式的请求协议相比,响应协议稍微复杂一些,原因也很简单,因为不同命令的响应结果是不同的,所以我们分别来看
首先redis返回文本的第一个字节标示了本次响应的类型,其中响应类型一共如下:
状态响应(status reply)的第一个字节是 "+"
错误响应(error reply)的第一个字节是 "-"
整数响应(integer reply)的第一个字节是 ":"
主体响应(bulk reply)的第一个字节是 "$"
批量主体响应(multi bulk reply)的第一个字节是 "*"
例如对ping命令来说,如果能够ping通,返回的是"+PONG",这是一个状态响应
状态响应
对于状态响应,一般的处理就是相客户端返回"+"之后的字符,例如ping命令返回"PONG",set命令返回"OK"错误响应
错误响应的处理与状态响应类似,因为从某种意义上讲,错误也是一种状态,只是一种特殊的状态而已,所以错误响应的处理就是返回"-"之后的字符整数响应
整数响应是处理例如INCR,TTL等命令的,这些命令直接返回一个整数,一般的处理就是返回":"之后的整数数字主体响应
主体响应是用来返回字符串,是最常见的响应形式,例如GET命令等所有获取字符串的命令,都是通过主体响应或者批量主体响协议应来获取的
主体响应的第一行"$"后面的数字表示返回字符串的长度,下一行返回字符串文本。如果该字符串为空,那么第一行将返回"$-1"批量主体响应
批量主体响应是server端批量返回字符串的协议,非常类似于请求协议,第一行"*"之后的数字表示本次返回的字符串一共多少个,然后以主体响应协议来返回字符串
好了,到这里我们就大致了解了redis的通讯协议。虽然我们是在分析别人写的代码,但纸上得来终觉浅,绝知此事要躬行,在分析源码的时候亲手敲一些代码是非常有益的。所以我用golang写了一个小程序来模拟redis的通讯协议,由于响应协议相对负责,我们暂时来模拟状态响应和主体响应两个协议
golang代码如下:
package main
import (
"fmt"
"os"
"net"
"strconv"
)
const (
RedisServerAddress = "127.0.0.1:6379"
RedisServerNetwork = "tcp"
)
type RedisError struct {
msg string
}
func (this *RedisError) Error() string {
return this.msg
}
// 连接到redis server
func conn() (net.Conn, error) {
conn, err := net.Dial(RedisServerNetwork, RedisServerAddress)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
return conn, err
}
// 将参数转化为redis请求协议
func getCmd(args []string) []byte {
cmdString := "*" + strconv.Itoa(len(args)) + "\r\n"
for _, v := range args {
cmdString += "$" + strconv.Itoa(len(v)) + "\r\n" + v + "\r\n"
}
cmdByte := make([]byte, len(cmdString))
copy(cmdByte[:], cmdString)
return cmdByte
}
func dealReply(reply []byte) (interface{}, error) {
responseType := reply[0]
switch responseType {
case '+':
return dealStatusReply(reply)
case '$':
return dealBulkReply(reply)
default:
return nil, &RedisError{"proto wrong!"}
}
}
// 处理状态响应
func dealStatusReply(reply []byte) (interface{}, error) {
statusByte := reply[1:]
pos := 0
for _, v := range statusByte {
if v == '\r' {
break
}
pos++
}
status := statusByte[:pos]
return string(status), nil
}
// 处理主体响应
func dealBulkReply(reply []byte) (interface{}, error) {
statusByte := reply[1:]
// 获取响应文本第一行标示的响应字符串长度
pos := 0
for _, v := range statusByte {
if v == '\r' {
break
}
pos++
}
strlen, err := strconv.Atoi(string(statusByte[:pos]))
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if strlen == -1 {
return "nil", nil
}
nextLinePost := 1
for _, v := range statusByte {
if v == '\n' {
break
}
nextLinePost++
}
result := string(statusByte[nextLinePost:nextLinePost+strlen])
return result, nil
}
func main() {
args := os.Args[1:]
if len(args) == 0 {
fmt.Println("usage: go run proto.go + redis command\nfor example:\ngo run proto.go PING")
os.Exit(0)
}
conn, _ := conn()
cmd := getCmd(args)
conn.Write(cmd)
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
res, _ := dealReply(buf[:n])
fmt.Println("redis的返回结果是 ", res)
}
运行代码:
// 测试PING命令
$go run proto.go PING
redis的返回结果是 PONG
// 测试SET命令
$go run proto.go SET key value
redis的返回结果是 OK
// 测试GET命令(GET一个存在的键)
$go run proto.go GET key
redis的返回结果是 value
// 测试GET命令(GET一个不存在的键)
$go run proto.go GET not_exist_key
redis的返回结果是 nil
一切ok!
PS:这段测试代码很潦草,很多异常情况没有考虑,主要是为了测试对redis的理解