国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 編程 > Golang > 正文

使用golang寫(xiě)一個(gè)redis-cli的方法示例

2020-04-01 18:51:56
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

0. redis通信協(xié)議

redis的客戶端(redis-cli)和服務(wù)端(redis-server)的通信是建立在tcp連接之上, 兩者之間數(shù)據(jù)傳輸?shù)木幋a解碼方式就是所謂的redis通信協(xié)議。所以,只要我們的redis-cli實(shí)現(xiàn)了這個(gè)協(xié)議的解析和編碼,那么我們就可以完成所有的redis操作。

redis 協(xié)議設(shè)計(jì)的非常易讀,也易于實(shí)現(xiàn),關(guān)于具體的redis通信協(xié)議請(qǐng)參考:通信協(xié)議(protocol)。后面我們?cè)趯?shí)現(xiàn)這個(gè)協(xié)議的過(guò)程中也會(huì)簡(jiǎn)單重復(fù)介紹一下具體實(shí)現(xiàn)

1. 建立tcp連接

redis客戶端和服務(wù)端的通信是建立tcp連接之上,所以第一步自然是先建立連接

package mainimport ( "flag" "log" "net")var host stringvar port stringfunc init() { flag.StringVar(&host, "h", "localhost", "hsot") flag.StringVar(&port, "p", "6379", "port")}func main() { flag.Parse() tcpAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port} conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { log.Println(err)  }  defer conn.Close() // to be continue}

后續(xù)我們發(fā)送和接受數(shù)據(jù)便都可以使用conn.Read()和conn.Write()來(lái)進(jìn)行了

2. 發(fā)送請(qǐng)求

發(fā)送請(qǐng)求第一個(gè)第一個(gè)字節(jié)是"*",中間是包含命令本身的參數(shù)個(gè)數(shù),后面跟著"/r/n" 。之后使用"$"加參數(shù)字節(jié)數(shù)量并使用"/r/n"結(jié)尾,然后緊跟參數(shù)內(nèi)容同時(shí)也使用"/r/n"結(jié)尾。如執(zhí)行 SET key liangwt 客戶端發(fā)送的請(qǐng)求為"*3/r/n$3/r/nSET/r/n$3/r/nkey/r/n$7/r/nliangwt/r/n"

注意:

  1. 命令本身也作為協(xié)議的其中一個(gè)參數(shù)來(lái)發(fā)送
  2. /r/n 對(duì)應(yīng)byte的十進(jìn)制為 13 10

我們可以使用telnet測(cè)試下

wentao@bj:~/github.com/liangwt/redis-cli$ telnet 127.0.0.1 6379Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is '^]'.*3$3SET$3key$7liangwt+OK

先暫時(shí)忽略服務(wù)端的回復(fù),通過(guò)telnet我們可以看出請(qǐng)求協(xié)議非常簡(jiǎn)單,所以對(duì)于請(qǐng)求協(xié)議的實(shí)現(xiàn)不做過(guò)多的介紹了,直接放代碼(如下使用基于字符串拼接,只是為了更直觀的演示,效率并不高,實(shí)際代碼中我們使用bytes.Buffer來(lái)實(shí)現(xiàn))

func MultiBulkMarshal(args ...string) string { var s string s = "*" s += strconv.Itoa(len(args)) s += "/r/n" // 命令所有參數(shù) for _, v := range args { s += "$" s += strconv.Itoa(len(v)) s += "/r/n" s += v s += "/r/n" } return s}

在實(shí)現(xiàn)了對(duì)命令和參數(shù)進(jìn)行編碼之后,我們便可以通過(guò)conn.Write()把數(shù)據(jù)推送到服務(wù)端

func main() {  // .... req := MultiBulkMarshal("SET", "key", "liangwt") _, err = conn.Write([]byte(req)) if err != nil { log.Fatal(err) } // to be continue}

3. 獲取回復(fù)

我們首先實(shí)現(xiàn)通過(guò)tcp獲取服務(wù)端返回值,就是上面提到過(guò)的conn.Read()。

func main() {  // .... p := make([]byte, 1024) _, err = conn.Read(p) if err != nil { log.Fatal(err) } // to be continue}

4. 解析回復(fù)

我們拿到p之后我們就可以解析返回值了,redis服務(wù)端的回復(fù)是分為幾種情況的

  • 狀態(tài)回復(fù)
  • 錯(cuò)誤回復(fù)
  • 整數(shù)回復(fù)
  • 批量回復(fù)
  • 多條批量回復(fù)

我們把前四種單獨(dú)看作一組,因?yàn)樗麄兌际菃我活愋偷姆祷刂?/p>

我們把最后的多條批量回復(fù)看成單獨(dú)的一組,因?yàn)樗前懊鎺追N類型的混合類型。而且你可以發(fā)現(xiàn)它和我們的請(qǐng)求協(xié)議是一樣的

也正是基于以上的考慮我們創(chuàng)建兩個(gè)函數(shù)來(lái)分別解析單一類型和混合類型,這樣在解析混合類型中的某一類型時(shí)就只需要調(diào)用單一類型解析的函數(shù)即可

在解析具體協(xié)議前我們先實(shí)現(xiàn)一個(gè)是讀取到/r/n為止的函數(shù)

func ReadLine(p []byte) ([]byte, error) { for i := 0; i < len(p); i++ { if p[i] == '/r' {  if p[i+1] != '/n' {  return []byte{}, errors.New("format error")  }  return p[0:i], nil } } return []byte{}, errors.New("format error")}

第一種狀態(tài)回復(fù):

狀態(tài)回復(fù)是一段以 "+" 開(kāi)始, "/r/n" 結(jié)尾的單行字符串。如 SET 命令成功的返回值:"+OK/r/n"

所以我們判斷第一個(gè)字符是否等于 '+' 如果相等,則讀取到/r/n

func SingleUnMarshal(p []byte) ([]byte, int, error) { var ( result []byte err  error length int ) switch p[0] { case '+': result, err = ReadLine(p[1:]) length = len(result) + 3 } return result, length, err}

注:我們?cè)诜祷貙?shí)際回復(fù)內(nèi)容的同時(shí)也返回了整個(gè)回復(fù)的長(zhǎng)度,方便后面解析多條批量回復(fù)時(shí)定位下一次的解析位置

第二種錯(cuò)誤回復(fù):

錯(cuò)誤回復(fù)的第一個(gè)字節(jié)是 "-", "/r/n" 結(jié)尾的單行字符串。如執(zhí)行 SET key缺少參數(shù)時(shí)返回值:"-ERR wrong number of arguments for 'set' command/r/n"

錯(cuò)誤回復(fù)和狀態(tài)回復(fù)非常相似,解析方式也是一樣到。所以我們只需添加一個(gè)case即可

func SingleUnMarshal(p []byte) ([]byte, int, error) { var ( result []byte err  error length int ) switch p[0] { case '+', '-': result, err = ReadLine(p[1:]) length = len(result) + 3 } return result, length, err}

第三種整數(shù)回復(fù):

整數(shù)回復(fù)的第一個(gè)字節(jié)是":",中間是字符串表示的整數(shù),"/r/n" 結(jié)尾的單行字符串。如執(zhí)行LLEN mylist命令時(shí)返回 ":10/r/n"

整數(shù)回復(fù)也和上面兩種是一樣的,只不過(guò)返回的是字符串表示的十進(jìn)制整數(shù)

func SingleUnMarshal(p []byte) ([]byte, int, error) { var ( result []byte err  error length int ) switch p[0] { case '+', '-', ':': result, err = ReadLine(p[1:]) length = len(result) + 3 } return result, length, err}

第四種批量回復(fù):

批量回復(fù)的第一個(gè)字節(jié)為 "$",接下來(lái)是字符串表示的整數(shù),它表示實(shí)際回復(fù)的長(zhǎng)度,之后跟著一個(gè) "/r/n",再后面跟著的是實(shí)際回復(fù)數(shù)據(jù),最末尾是另一個(gè) "/r/n"。如GET key 命令的返回值:"$7/r/nliangwt/r/n"

所以批量回復(fù)解析的實(shí)現(xiàn):

  • 讀取第一行得到實(shí)際回復(fù)的長(zhǎng)度
  • 把字符串類型的長(zhǎng)度轉(zhuǎn)換成對(duì)應(yīng)十進(jìn)制整數(shù)
  • 從第二行開(kāi)始位置往下讀對(duì)應(yīng)長(zhǎng)度

但是對(duì)于某些不存在的key,批量回復(fù)會(huì)將特殊值 -1 用作回復(fù)的長(zhǎng)度值, 此時(shí)我們不需要繼續(xù)往下讀取實(shí)際回復(fù)。例如GET NOT_EXIST_KEY 返回值:"$-1", 所以我們需要對(duì)此特殊情況判斷,讓函數(shù)返回一個(gè)空對(duì)象(nil)而不是空值("")

func SingleUnMarshal(p []byte) ([]byte, int, error) { // .... case '$': n, err := ReadLine(p[1:]) if err != nil {  return []byte{}, 0, err } l, err := strconv.Atoi(string(n)) if err != nil {  return []byte{}, 0, err } if l == -1 {  return nil, 0, nil } // +3 的原因 $ /r /n 三個(gè)字符 result = p[len(n)+3 : len(n)+3+l] length = len(n) + 5 + l } return result, length, err}

思考:

為什么redis要使用提前告知字節(jié)數(shù),然后往下讀取指定長(zhǎng)度的方式,而不是直接讀取第二行到/r/n為止?

答案很明顯:此方式可以讓redis讀取返回值時(shí)不受具體的返回內(nèi)容影響,在按行讀取的情況下,無(wú)論使用任何分割符都有可能導(dǎo)致redis在解析具體內(nèi)容時(shí)把內(nèi)容中的分割符當(dāng)作時(shí)結(jié)尾,導(dǎo)致解析錯(cuò)誤。

思考一下這種情況:我們SET key "liang/r/nwt" ,那么當(dāng)我們GET key時(shí),服務(wù)端返回值為"$9/r/nliang/r/nwt/r/n" 完全規(guī)避了value中的/r/n影響

第五種多條批量回復(fù):

多條批量回復(fù)是由多個(gè)回復(fù)組成的數(shù)組,它的第一個(gè)字節(jié)為"*", 后跟一個(gè)字符串表示的整數(shù)值, 這個(gè)值記錄了多條批量回復(fù)所包含的回復(fù)數(shù)量, 再后面是一個(gè)"/r/n"。如LRANGE mylist 0 -1的返回值:"*3/r/n$1/r/n3/r/n$1/r/n2/r/n$1/r/n1"。

所以多條批量回復(fù)解析的實(shí)現(xiàn):

  • 解析第一行數(shù)據(jù)獲得字符串類型的回復(fù)數(shù)量
  • 把字符串類型的長(zhǎng)度轉(zhuǎn)換成對(duì)應(yīng)十進(jìn)制整數(shù)
  • 按照單條回復(fù)依次逐個(gè)解析,一共解析成上面得到的數(shù)量

在這里我們用到了單條解析時(shí)返回的字節(jié)長(zhǎng)度length,通過(guò)這個(gè)長(zhǎng)度我們可以很方便的知道下次單條解析的開(kāi)始位置為上一次位置+length

在解析多條批量回復(fù)時(shí)需要注意兩點(diǎn):

第一,多條批量回復(fù)也可以是空白的(empty)。例如執(zhí)行LRANGE NOT_EXIST_KEY 0 -1 服務(wù)端返回值"*0/r/n"。此時(shí)客戶端返回的應(yīng)該空數(shù)組[][]byte

第二,多條批量回復(fù)也可以是無(wú)內(nèi)容的(null multi bulk reply)。例如執(zhí)行BLPOP key 1 服務(wù)端返回值"*-1/r/n"。此時(shí)客戶端返回的應(yīng)該是nil

func MultiUnMarsh(p []byte) ([][]byte, error) { if p[0] != '*' { return [][]byte{}, errors.New("format error") } n, err := ReadLine(p[1:]) if err != nil { return [][]byte{}, err } l, err := strconv.Atoi(string(n)) if err != nil { return [][]byte{}, err } // 多條批量回復(fù)也可以是空白的(empty) if l == 0 { return [][]byte{}, nil } // 無(wú)內(nèi)容的多條批量回復(fù)(null multi bulk reply)也是存在的, // 客戶端庫(kù)應(yīng)該返回一個(gè) null 對(duì)象, 而不是一個(gè)空數(shù)組。 if l == -1 { return nil, nil } result := make([][]byte, l) t := len(n) + 3 for i := 0; i < l; i++ { ret, length, err := SingleUnMarshal(p[t:]) if err != nil {  return [][]byte{}, errors.New("format error") } result[i] = ret t += length } return result, nil}

5. 命令行模式

一個(gè)可用的redis-cli自然是一個(gè)交互式的,用戶輸入指令然后輸出返回值。在go中我們可以使用以下代碼即可獲得一個(gè)類似的交互式命令行

func main() { // .... for { fmt.Printf("%s:%d>", host, port) bio := bufio.NewReader(os.Stdin) input, _, err := bio.ReadLine() if err != nil {  log.Fatal(err) } fmt.Printf("%s/n", input) }}

我們運(yùn)行以上代碼就可以實(shí)現(xiàn)

localhost:6379>set key liangset key lianglocalhost:6379>get keyget keylocalhost:6379>

結(jié)合上我們的redis發(fā)送請(qǐng)求和解析請(qǐng)求即可完成整個(gè)redis-cli

func main() {  // .... for { fmt.Printf("%s:%d>", host, port) // 獲取輸入命令和參數(shù) bio := bufio.NewReader(os.Stdin) input, err := bio.ReadString('/n') if err != nil {  log.Fatal(err) } fields := strings.Fields(input) // 編碼發(fā)送請(qǐng)求 req := MultiBulkMarshal(fields...) // 發(fā)送請(qǐng)求 _, err = conn.Write([]byte(req)) if err != nil {  log.Fatal(err) } // 讀取返回內(nèi)容 p := make([]byte, 1024) _, err = conn.Read(p) if err != nil {  log.Fatal(err) } // 解析返回內(nèi)容 if p[0] == '*' {  result, err := MultiUnMarsh(p) } else {  result, _, err := SingleUnMarshal(p) }  }  // ....}

6. 總結(jié)

到目前為止我們的cli程序已經(jīng)全部完成,但其實(shí)還有很多不完美地方。但核心的redis協(xié)議解析已經(jīng)完成,使用這個(gè)解析我們能完成任何的cli與服務(wù)器之間的交互

更詳細(xì)的redis-cli實(shí)現(xiàn)可以參考我的github:A Simaple redis cli - Rclient 

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持VEVB武林網(wǎng)。


發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 科技| 隆子县| 连平县| 蒙自县| 临潭县| 阿鲁科尔沁旗| 盈江县| 苍溪县| 南充市| 大城县| 唐河县| 庆安县| 遵化市| 栾川县| 潞西市| 广西| 黑龙江省| 太保市| 平谷区| 武强县| 大埔区| 双流县| 文登市| 宁化县| 新竹县| 余干县| 若羌县| 胶南市| 天峨县| 三都| 酒泉市| 辽宁省| 永济市| 友谊县| 将乐县| 库伦旗| 田阳县| 沙田区| 抚松县| 聂拉木县| 辽宁省|