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

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的理解

文章參考
http://doc.redisfans.com/topic/protocol.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末粟判,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子峦剔,更是在濱河造成了極大的恐慌档礁,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吝沫,死亡現(xiàn)場(chǎng)離奇詭異呻澜,居然都是意外死亡递礼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門羹幸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脊髓,“玉大人,你說我怎么就攤上這事栅受〗酰” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵窘疮,是天一觀的道長(zhǎng)袋哼。 經(jīng)常有香客問我,道長(zhǎng)闸衫,這世上最難降的妖魔是什么涛贯? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮蔚出,結(jié)果婚禮上弟翘,老公的妹妹穿的比我還像新娘。我一直安慰自己骄酗,他們只是感情好稀余,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趋翻,像睡著了一般睛琳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上踏烙,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天师骗,我揣著相機(jī)與錄音,去河邊找鬼讨惩。 笑死辟癌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荐捻。 我是一名探鬼主播黍少,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼处面!你這毒婦竟也來了厂置?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤魂角,失蹤者是張志新(化名)和其女友劉穎昵济,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡砸紊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年传于,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片醉顽。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沼溜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出游添,到底是詐尸還是另有隱情系草,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布唆涝,位于F島的核電站找都,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏廊酣。R本人自食惡果不足惜能耻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亡驰。 院中可真熱鬧晓猛,春花似錦、人聲如沸凡辱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)透乾。三九已至洪燥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乳乌,已是汗流浹背捧韵。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钦扭,地道東北人纫版。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓床绪,卻偏偏與公主長(zhǎng)得像客情,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子癞己,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容