這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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 6379Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is '^]'.// 以下是發送的內容*1$4PING// 這是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$3SET$3key$5value
用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 mainimport ( "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 serverfunc 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 PINGredis的返回結果是 PONG// 測試SET命令$go run proto.go SET key valueredis的返回結果是 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的理解
文章參考
http://doc.redisfans.com/topic/protocol.html