一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

腳本之家,腳本語言編程技術及教程分享平臺!
分類導航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服務器之家 - 腳本之家 - Golang - 使用golang寫一個redis-cli的方法示例

使用golang寫一個redis-cli的方法示例

2020-05-20 10:36liangwt Golang

這篇文章主要介紹了使用golang寫一個redis-cli的方法示例,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

0. redis通信協議

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

redis 協議設計的非常易讀,也易于實現,關于具體的redis通信協議請參考:通信協議(protocol)。后面我們在實現這個協議的過程中也會簡單重復介紹一下具體實現

1. 建立tcp連接

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
 
import (
 "flag"
 "log"
 "net"
)
 
var host string
var port string
 
func 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
}

后續我們發送和接受數據便都可以使用conn.Read()和conn.Write()來進行了

2. 發送請求

發送請求第一個第一個字節是"*",中間是包含命令本身的參數個數,后面跟著"\r\n" 。之后使用"$"加參數字節數量并使用"\r\n"結尾,然后緊跟參數內容同時也使用"\r\n"結尾。如執行 SET key liangwt 客戶端發送的請求為"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$7\r\nliangwt\r\n"

注意:

  1. 命令本身也作為協議的其中一個參數來發送
  2. \r\n 對應byte的十進制為 13 10

我們可以使用telnet測試下

?
1
2
3
4
5
6
7
8
9
10
11
12
wentao@bj:~/github.com/liangwt/redis-cli$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
*3
$3
SET
$3
key
$7
liangwt
+OK

先暫時忽略服務端的回復,通過telnet我們可以看出請求協議非常簡單,所以對于請求協議的實現不做過多的介紹了,直接放代碼(如下使用基于字符串拼接,只是為了更直觀的演示,效率并不高,實際代碼中我們使用bytes.Buffer來實現)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func MultiBulkMarshal(args ...string) string {
 var s string
 s = "*"
 s += strconv.Itoa(len(args))
 s += "\r\n"
 
 // 命令所有參數
 for _, v := range args {
 s += "$"
 s += strconv.Itoa(len(v))
 s += "\r\n"
 s += v
 s += "\r\n"
 }
 
 return s
}

在實現了對命令和參數進行編碼之后,我們便可以通過conn.Write()把數據推送到服務端

?
1
2
3
4
5
6
7
8
9
func main() {
  // ....
 req := MultiBulkMarshal("SET", "key", "liangwt")
 _, err = conn.Write([]byte(req))
 if err != nil {
 log.Fatal(err)
 }
 // to be continue
}

3. 獲取回復

我們首先實現通過tcp獲取服務端返回值,就是上面提到過的conn.Read()。

?
1
2
3
4
5
6
7
8
9
func main() {
  // ....
 p := make([]byte, 1024)
 _, err = conn.Read(p)
 if err != nil {
 log.Fatal(err)
 }
 // to be continue
}

4. 解析回復

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

  • 狀態回復
  • 錯誤回復
  • 整數回復
  • 批量回復
  • 多條批量回復

我們把前四種單獨看作一組,因為他們都是單一類型的返回值

我們把最后的多條批量回復看成單獨的一組,因為它是包含前面幾種類型的混合類型。而且你可以發現它和我們的請求協議是一樣的

也正是基于以上的考慮我們創建兩個函數來分別解析單一類型和混合類型,這樣在解析混合類型中的某一類型時就只需要調用單一類型解析的函數即可

在解析具體協議前我們先實現一個是讀取到\r\n為止的函數

?
1
2
3
4
5
6
7
8
9
10
11
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")
}

第一種狀態回復:

狀態回復是一段以 "+" 開始, "\r\n" 結尾的單行字符串。如 SET 命令成功的返回值:"+OK\r\n"

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}

注:我們在返回實際回復內容的同時也返回了整個回復的長度,方便后面解析多條批量回復時定位下一次的解析位置

第二種錯誤回復:

錯誤回復的第一個字節是 "-", "\r\n" 結尾的單行字符串。如執行 SET key缺少參數時返回值:"-ERR wrong number of arguments for 'set' command\r\n"

錯誤回復和狀態回復非常相似,解析方式也是一樣到。所以我們只需添加一個case即可

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

第三種整數回復:

整數回復的第一個字節是":",中間是字符串表示的整數,"\r\n" 結尾的單行字符串。如執行LLEN mylist命令時返回 ":10\r\n"

整數回復也和上面兩種是一樣的,只不過返回的是字符串表示的十進制整數

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

第四種批量回復:

批量回復的第一個字節為 "$",接下來是字符串表示的整數,它表示實際回復的長度,之后跟著一個 "\r\n",再后面跟著的是實際回復數據,最末尾是另一個 "\r\n"。如GET key 命令的返回值:"$7\r\nliangwt\r\n"

所以批量回復解析的實現:

  • 讀取第一行得到實際回復的長度
  • 把字符串類型的長度轉換成對應十進制整數
  • 從第二行開始位置往下讀對應長度

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 三個字符
 result = p[len(n)+3 : len(n)+3+l]
 length = len(n) + 5 + l
 }
 return result, length, err
}

思考:

為什么redis要使用提前告知字節數,然后往下讀取指定長度的方式,而不是直接讀取第二行到\r\n為止?

答案很明顯:此方式可以讓redis讀取返回值時不受具體的返回內容影響,在按行讀取的情況下,無論使用任何分割符都有可能導致redis在解析具體內容時把內容中的分割符當作時結尾,導致解析錯誤。

思考一下這種情況:我們SET key "liang\r\nwt" ,那么當我們GET key時,服務端返回值為"$9\r\nliang\r\nwt\r\n" 完全規避了value中的\r\n影響

第五種多條批量回復:

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

所以多條批量回復解析的實現:

  • 解析第一行數據獲得字符串類型的回復數量
  • 把字符串類型的長度轉換成對應十進制整數
  • 按照單條回復依次逐個解析,一共解析成上面得到的數量

在這里我們用到了單條解析時返回的字節長度length,通過這個長度我們可以很方便的知道下次單條解析的開始位置為上一次位置+length

在解析多條批量回復時需要注意兩點:

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

第二,多條批量回復也可以是無內容的(null multi bulk reply)。例如執行BLPOP key 1 服務端返回值"*-1\r\n"。此時客戶端返回的應該是nil

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
 }
 // 多條批量回復也可以是空白的(empty)
 if l == 0 {
 return [][]byte{}, nil
 }
 
 // 無內容的多條批量回復(null multi bulk reply)也是存在的,
 // 客戶端庫應該返回一個 null 對象, 而不是一個空數組。
 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. 命令行模式

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
 }
}

我們運行以上代碼就可以實現

?
1
2
3
4
5
localhost:6379>set key liang
set key liang
localhost:6379>get key
get key
localhost:6379>

結合上我們的redis發送請求和解析請求即可完成整個redis-cli

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func main() {
  // ....
 for {
 fmt.Printf("%s:%d>", host, port)
 
 // 獲取輸入命令和參數
 bio := bufio.NewReader(os.Stdin)
 input, err := bio.ReadString('\n')
 if err != nil {
  log.Fatal(err)
 }
 fields := strings.Fields(input)
 
 // 編碼發送請求
 req := MultiBulkMarshal(fields...)
 
 // 發送請求
 _, err = conn.Write([]byte(req))
 if err != nil {
  log.Fatal(err)
 }
 
 // 讀取返回內容
 p := make([]byte, 1024)
 _, err = conn.Read(p)
 if err != nil {
  log.Fatal(err)
 }
 
 // 解析返回內容
 if p[0] == '*' {
  result, err := MultiUnMarsh(p)
 } else {
  result, _, err := SingleUnMarshal(p)
 }
 
  }
  // ....
}

6. 總結

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

更詳細的redis-cli實現可以參考我的github:A Simaple redis cli - Rclient

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。

原文鏈接:https://my.oschina.net/liangwt/blog/2231557

延伸 · 閱讀

精彩推薦
  • Golanggo語言制作端口掃描器

    go語言制作端口掃描器

    本文給大家分享的是使用go語言編寫的TCP端口掃描器,可以選擇IP范圍,掃描的端口,以及多線程,有需要的小伙伴可以參考下。 ...

    腳本之家3642020-04-25
  • Golanggo日志系統logrus顯示文件和行號的操作

    go日志系統logrus顯示文件和行號的操作

    這篇文章主要介紹了go日志系統logrus顯示文件和行號的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    SmallQinYan12302021-02-02
  • GolangGolang中Bit數組的實現方式

    Golang中Bit數組的實現方式

    這篇文章主要介紹了Golang中Bit數組的實現方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    天易獨尊11682021-06-09
  • Golanggolang 通過ssh代理連接mysql的操作

    golang 通過ssh代理連接mysql的操作

    這篇文章主要介紹了golang 通過ssh代理連接mysql的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    a165861639710342021-03-08
  • Golanggolang如何使用struct的tag屬性的詳細介紹

    golang如何使用struct的tag屬性的詳細介紹

    這篇文章主要介紹了golang如何使用struct的tag屬性的詳細介紹,從例子說起,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看...

    Go語言中文網11352020-05-21
  • GolangGolang通脈之數據類型詳情

    Golang通脈之數據類型詳情

    這篇文章主要介紹了Golang通脈之數據類型,在編程語言中標識符就是定義的具有某種意義的詞,比如變量名、常量名、函數名等等,Go語言中標識符允許由...

    4272021-11-24
  • Golanggolang的httpserver優雅重啟方法詳解

    golang的httpserver優雅重啟方法詳解

    這篇文章主要給大家介紹了關于golang的httpserver優雅重啟的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,...

    helight2992020-05-14
  • Golanggolang json.Marshal 特殊html字符被轉義的解決方法

    golang json.Marshal 特殊html字符被轉義的解決方法

    今天小編就為大家分享一篇golang json.Marshal 特殊html字符被轉義的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧 ...

    李浩的life12792020-05-27
主站蜘蛛池模板: 久久亚洲午夜牛牛影视 | 亚洲第一区在线观看 | 韩国三级年轻的小婊孑 | sao虎在线精品永久 s0e一923春菜花在线播放 | 国产精品久久久久久久久99热 | 国产精品成人网红女主播 | 羞羞漫画免费漫画页面在线看漫画秋蝉 | 欧美色图日韩色图 | 免费一级国产大片 | 99在线观看视频免费精品9 | 蜜桃视频一区二区三区四区 | 国产午夜精品不卡视频 | 国产亚洲自愉自愉 | 波多野结衣xxxxx在线播放 | 日韩综合第一页 | 亚洲精品午夜在线观看 | 恩爱夫妇交换小说 | 边吃胸边膜下刺激免费男对女 | 好大用力深一点视频 | 日本阿v在线播放 | 毛片大全免费看 | 亚洲男人的天堂网站 | 国产成人+亚洲欧洲 | 日本网络视频www色高清免费 | 午夜免费啪视频观看视频 | 国产麻豆精品入口在线观看 | 好吊色永久免费视频大全 | 四虎2021地址入口 | 网红思瑞一区二区三区 | 91精品国产免费久久国语蜜臀 | 国产精品视频2021 | 日韩精品成人免费观看 | 欧美精品黑人巨大在线播放 | 欧美一级免费看 | 九九久久国产 | 日本破处| 人禽l交免费视频观看+视频 | gay台湾无套男同志可播放 | 性xxxx中国老妇506070 | 午夜宅男宅女看在线观看 | 美女脱一净二净不带胸罩 |