用 Go 來瞭解一下 Redis 通訊協議
原文地址:煎魚的迷之傳送門
Go、PHP、Java... 都有那麼多包來支撐你使用 Redis,那你是否有想過
有了服務端,有了用戶端,他們倆是怎樣通訊,又是基於什麼通訊協議做出互動的呢?
介紹
基於我們的目的,本文主要講解和實踐 Redis 的通訊協議
Redis 的用戶端和服務端是通過 TCP 串連來進行資料互動, 伺服器預設的連接埠號碼為 6379
用戶端和伺服器發送的命令或資料一律以 \r\n
(CRLF)結尾(這是一條約定)
協議
在 Redis 中分為請求和回複,而請求協議又分為新版和舊版,新版統一請求協議在 Redis 1.2 版本中引入,最終在 Redis 2.0 版本成為 Redis 伺服器通訊的標準方式
本文是基於新版協議來實現功能,不建議使用舊版(1.2 挺老舊了)。如下是新協議的各種範例:
請求協議
1、 格式樣本
*<參數數量> CR LF$<參數 1 的位元組數量> CR LF<參數 1 的資料> CR LF...$<參數 N 的位元組數量> CR LF<參數 N 的資料> CR LF
在該協議下所有發送至 Redis 伺服器的參數都是二進位安全(binary safe)的
2、列印樣本
*3$3SET$5mykey$7myvalue
3、實際協議值
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
這就是 Redis 的請求協議規範,按照範例1編寫用戶端邏輯,最終發送的是範例3,相信你已經有大致的概念了,Redis 的協議非常的簡潔易懂,這也是好上手的原因之一,你可以想想協議這麼定義的好處在哪?
回複
Redis 會根據你請求協議的不同(執行的命令結果也不同),返回多種不同類型的回複。在這個回複“協議”中,可以通過檢查第一個位元組,確定這個回複是什麼類型,如下:
- 狀態回複(status reply)的第一個位元組是 "+"
- 錯誤回複(error reply)的第一個位元組是 "-"
- 整數回複(integer reply)的第一個位元組是 ":"
- 批量回複(bulk reply)的第一個位元組是 "$"
- 多條批量回複(multi bulk reply)的第一個位元組是 "*"
有了回複的頭部標識,結尾的 CRLF,你可以大致猜想出回複“協議”是怎麼樣的,但是實踐才能得出真理,斎知道怕是你很快就忘記了
實踐
與 Redis 伺服器互動
package mainimport ( "log" "net" "os" "github.com/EDDYCJY/redis-protocol-example/protocol")const ( Address = "127.0.0.1:6379" Network = "tcp")func Conn(network, address string) (net.Conn, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } return conn, nil}func main() { // 讀取入參 args := os.Args[1:] if len(args) <= 0 { log.Fatalf("Os.Args <= 0") } // 擷取請求協議 reqCommand := protocol.GetRequest(args) // 串連 Redis 伺服器 redisConn, err := Conn(Network, Address) if err != nil { log.Fatalf("Conn err: %v", err) } defer redisConn.Close() // 寫入請求內容 _, err = redisConn.Write(reqCommand) if err != nil { log.Fatalf("Conn Write err: %v", err) } // 讀取回複 command := make([]byte, 1024) n, err := redisConn.Read(command) if err != nil { log.Fatalf("Conn Read err: %v", err) } // 處理回複 reply, err := protocol.GetReply(command[:n]) if err != nil { log.Fatalf("protocol.GetReply err: %v", err) } // 處理後的回複內容 log.Printf("Reply: %v", reply) // 原始的回複內容 log.Printf("Command: %v", string(command[:n]))}
在這裡我們完成了整個 Redis 用戶端和服務端互動的流程,分別如下:
1、讀取命令列參數:擷取執行的 Redis 命令
2、擷取請求協議參數
3、串連 Redis 伺服器,擷取串連控制代碼
4、將請求協議參數寫入串連:發送請求的命令列參數
5、從串連中讀取返回的資料:讀取先前請求的回複資料
6、根據回複“協議”內容,處理回複的資料集
7、輸出處理後的回複內容及原始回複內容
請求
func GetRequest(args []string) []byte { req := []string{ "*" + strconv.Itoa(len(args)), } for _, arg := range args { req = append(req, "$"+strconv.Itoa(len(arg))) req = append(req, arg) } str := strings.Join(req, "\r\n") return []byte(str + "\r\n")}
通過對 Redis 的請求協議的分析,可得出它的規律,先加上標誌位,計算參數總數量,再迴圈合并各個參數的位元組數量、值就可以了
回複
func GetReply(reply []byte) (interface{}, error) { replyType := reply[0] switch replyType { case StatusReply: return doStatusReply(reply[1:]) case ErrorReply: return doErrorReply(reply[1:]) case IntegerReply: return doIntegerReply(reply[1:]) case BulkReply: return doBulkReply(reply[1:]) case MultiBulkReply: return doMultiBulkReply(reply[1:]) default: return nil, nil }}func doStatusReply(reply []byte) (string, error) { if len(reply) == 3 && reply[1] == 'O' && reply[2] == 'K' { return OkReply, nil } if len(reply) == 5 && reply[1] == 'P' && reply[2] == 'O' && reply[3] == 'N' && reply[4] == 'G' { return PongReply, nil } return string(reply), nil}func doErrorReply(reply []byte) (string, error) { return string(reply), nil}func doIntegerReply(reply []byte) (int, error) { pos := getFlagPos('\r', reply) result, err := strconv.Atoi(string(reply[:pos])) if err != nil { return 0, err } return result, nil}...
在這裡我們對所有回複類型進行了分發,不同的回複標誌位對應不同的處理方式,在這裡需求注意幾項問題,如下:
1、當請求的值不存在,會將特殊值 -1 用作回複
2、伺服器發送的所有字串都由 CRLF 結尾
3、多條批量回複是可基於批量回複的,要注意理解
4、無內容的多條批量回複是存在的
最重要的是,對不同回複的規則的把控,能夠讓你更好的理解 Redis 的請求、回複的互動過程
小結
寫這篇文章的起因,是因為常常在使用 Redis 時,只是用,你不知道它是基於什麼樣的通訊協議來通訊,這樣的感覺是十分難受的
通過本文的講解,我相信你已經大致瞭解 Redis 用戶端是怎麼樣和服務端互動,也清楚了其所用的通訊原理,希望能夠對你有所協助!
最後,如果想詳細查看代碼,右拐項目地址:https://github.com/EDDYCJY/re...
如果對你有所協助,歡迎點個 Star
參考