go-redis源碼分析(一):redis協議

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.