redis.v5是一款基于golang的redis操作庫(kù),封裝了對(duì)redis的各種操作
源碼地址是
https://github.com/go-redis/redis
Redis客戶端的工作本質(zhì)上是基于tcp協(xié)議向redis server傳輸符合redis協(xié)議的命令請(qǐng)求,并根據(jù)redis協(xié)議解析server端的返回值
我們可以通過telnet工具來模擬這一過程天梧,例如ping命令我們可以這樣發(fā)送請(qǐng)求
$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
// 以下是發(fā)送的內(nèi)容
*1
$4
PING
// 這是redis server返回內(nèi)容
+PONG
所以要想理解redis客戶端召调,首先要熟悉redis協(xié)議
redis的協(xié)議由請(qǐng)求協(xié)議和響應(yīng)協(xié)議兩部分組成胡野,都是非常簡(jiǎn)單的通訊協(xié)議仁连,易于程序解析满葛,也方便人類進(jìn)行閱讀
需要注意一點(diǎn)的是早期版本的redis協(xié)議和如今的不太一樣镰矿,所以特別提醒的是本文是基于redis 3.2.6版本琐驴。
請(qǐng)求協(xié)議:
* <參數(shù)數(shù)量> CR LF
$ <參數(shù) 1 的字節(jié)數(shù)量> CR LF
<參數(shù) 1 的數(shù)據(jù)> CR LF
...
$ <參數(shù) N 的字節(jié)數(shù)量> CR LF
<參數(shù) N 的數(shù)據(jù)> CR LF
我們以開頭的 telnet模擬發(fā)送 ping 命令 作為例子
其中第一行星號(hào)后面表示本次傳輸?shù)拿顐€(gè)數(shù)。1表示本次請(qǐng)求只有一個(gè)參數(shù)秤标,同樣的道理對(duì)于get命令而言绝淡,參數(shù)是兩個(gè)(get key),所以對(duì)于get參數(shù)而言應(yīng)該寫成2
緊接著后面開始一個(gè)一個(gè)傳遞請(qǐng)求參數(shù)苍姜,每一個(gè)參數(shù)用兩行表示牢酵,其中上一行$n表示參數(shù)的字符數(shù),下一行是參數(shù)的字符串
例如上面的例子衙猪,$4表示這個(gè)命令有4個(gè)字符茁帽,下一行的ping就是該命令的字符串表示
同樣的道理,set命令可以這樣寫
*3
$3
SET
$3
key
$5
value
用byte數(shù)組可以這樣寫
"*3\r\n$3\r\nset\r\n$3key\r\n$5value\r\n"
返回值是
+OK
說明命令被成功解析并執(zhí)行
響應(yīng)協(xié)議:
說完了請(qǐng)求協(xié)議屈嗤,我們?cè)賮砜纯错憫?yīng)協(xié)議潘拨,與擁有統(tǒng)一格式的請(qǐng)求協(xié)議相比,響應(yīng)協(xié)議稍微復(fù)雜一些饶号,原因也很簡(jiǎn)單铁追,因?yàn)椴煌畹捻憫?yīng)結(jié)果是不同的,所以我們分別來看
首先redis返回文本的第一個(gè)字節(jié)標(biāo)示了本次響應(yīng)的類型茫船,其中響應(yīng)類型一共如下:
狀態(tài)響應(yīng)(status reply)的第一個(gè)字節(jié)是 "+"
錯(cuò)誤響應(yīng)(error reply)的第一個(gè)字節(jié)是 "-"
整數(shù)響應(yīng)(integer reply)的第一個(gè)字節(jié)是 ":"
主體響應(yīng)(bulk reply)的第一個(gè)字節(jié)是 "$"
批量主體響應(yīng)(multi bulk reply)的第一個(gè)字節(jié)是 "*"
例如對(duì)ping命令來說琅束,如果能夠ping通扭屁,返回的是"+PONG",這是一個(gè)狀態(tài)響應(yīng)
狀態(tài)響應(yīng)
對(duì)于狀態(tài)響應(yīng)涩禀,一般的處理就是相客戶端返回"+"之后的字符料滥,例如ping命令返回"PONG",set命令返回"OK"錯(cuò)誤響應(yīng)
錯(cuò)誤響應(yīng)的處理與狀態(tài)響應(yīng)類似艾船,因?yàn)閺哪撤N意義上講葵腹,錯(cuò)誤也是一種狀態(tài),只是一種特殊的狀態(tài)而已屿岂,所以錯(cuò)誤響應(yīng)的處理就是返回"-"之后的字符整數(shù)響應(yīng)
整數(shù)響應(yīng)是處理例如INCR践宴,TTL等命令的,這些命令直接返回一個(gè)整數(shù)爷怀,一般的處理就是返回":"之后的整數(shù)數(shù)字主體響應(yīng)
主體響應(yīng)是用來返回字符串阻肩,是最常見的響應(yīng)形式,例如GET命令等所有獲取字符串的命令运授,都是通過主體響應(yīng)或者批量主體響協(xié)議應(yīng)來獲取的
主體響應(yīng)的第一行"$"后面的數(shù)字表示返回字符串的長(zhǎng)度烤惊,下一行返回字符串文本。如果該字符串為空吁朦,那么第一行將返回"$-1"批量主體響應(yīng)
批量主體響應(yīng)是server端批量返回字符串的協(xié)議柒室,非常類似于請(qǐng)求協(xié)議,第一行"*"之后的數(shù)字表示本次返回的字符串一共多少個(gè)喇完,然后以主體響應(yīng)協(xié)議來返回字符串
好了伦泥,到這里我們就大致了解了redis的通訊協(xié)議剥啤。雖然我們是在分析別人寫的代碼锦溪,但紙上得來終覺淺,絕知此事要躬行府怯,在分析源碼的時(shí)候親手敲一些代碼是非常有益的刻诊。所以我用golang寫了一個(gè)小程序來模擬redis的通訊協(xié)議,由于響應(yīng)協(xié)議相對(duì)負(fù)責(zé)牺丙,我們暫時(shí)來模擬狀態(tài)響應(yīng)和主體響應(yīng)兩個(gè)協(xié)議
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
}
// 將參數(shù)轉(zhuǎn)化為redis請(qǐng)求協(xié)議
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!"}
}
}
// 處理狀態(tài)響應(yīng)
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
}
// 處理主體響應(yīng)
func dealBulkReply(reply []byte) (interface{}, error) {
statusByte := reply[1:]
// 獲取響應(yīng)文本第一行標(biāo)示的響應(yīng)字符串長(zhǎng)度
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的返回結(jié)果是 ", res)
}
運(yùn)行代碼:
// 測(cè)試PING命令
$go run proto.go PING
redis的返回結(jié)果是 PONG
// 測(cè)試SET命令
$go run proto.go SET key value
redis的返回結(jié)果是 OK
// 測(cè)試GET命令(GET一個(gè)存在的鍵)
$go run proto.go GET key
redis的返回結(jié)果是 value
// 測(cè)試GET命令(GET一個(gè)不存在的鍵)
$go run proto.go GET not_exist_key
redis的返回結(jié)果是 nil
一切ok则涯!
PS:這段測(cè)試代碼很潦草,很多異常情況沒有考慮冲簿,主要是為了測(cè)試對(duì)redis的理解