我們必須要講相同的語言,才有辦法與其它人溝通。同樣地,如果我們想要與 Redis 溝通,就必須使用 Redis serialization protocol(簡稱 RESP),這是一個由 Redis 設計的通訊協定。
目前主流有兩個版本:RESP2 與 RESP3。RESP3 是 RESP2 的超集:換句話說,新版的 Redis 是兼容 RESP2 與 RESP3 的。
Redis 的官方文件 Redis serialization protocol specification 有闡述 RESP 是如何、為什麼這麼設計,非常值得一讀。
RESP 基於「值(Value)」來表達資料,值可以有不同的資料型態--在 RESP2 中支援 5 種不同的資料型態,而在 RESP3 中擴充到一共 15 種。
RESP 利用 magic byte(魔術字元) 來辨識資料型態,在資料的前方都會有一個 byte 標記,代表資料型態:
+
-
:
$
*
據此,我們定義出相應的常數:
// file: ./pkg/resp/value.go
package resp
const(
MAGIC_SIMPLE_STRING = '+'
MAGIC_SIMPLE_ERROR = '-'
MAGIC_INTEGER = ':'
MAGIC_BULK_STRING = '$'
MAGIC_ARRAY = '*'
)
註:我把
resp放在./pkg底下,這代表的是一個可以被公開存取的套件,這同時也是 golang-standard/project-layout 的建議。
resp.(Value)在閱讀 Build Redis from Scratch: RESP Reader 的時候,我很疑惑為什麼它要定義這樣的結構體:
type Value struct {
typ string
str string
num int
bulk string
array []Value
}
誠然,它完成了它的使命:儲存資料,但是顯而易見的浪費。如果我儲存 Value.str,那 Value.num, Value.bulk 或 Value.array 的記憶體空間就會被浪費掉。更何況,我還需要根據 Value.typ 才能判定哪個欄位是有效的。
因此,我認為直接定義一個 resp.(Value) 的介面比較合適:
// file: ./pkg/resp/value.go
type Value interface {
Marshal() []byte
}
Marshal() 的靈感源於 json.Marshal(),代表的是將會被寫入回應的 []byte。
Simple String 是 Redis 中最基礎的資料型態,它的組成很簡單:
+
{data}
\r\n
嚴格來說,Simple String 的 {data} 本質上就是一個 []byte,因此我們可以做出如下定義:
// file: ./pkg/resp/value.go
type SimpleString []byte
為了要讓它實現 resp.(Value) 介面,在此定義一個 resp.(SimpleString).Marshal():
const SENTINEL = "\r\n"
func (v SimpleString) Marshal() []byte {
/*
* Simple String Format:
* |---------------------|---------|----------|
* | MAGIC_SIMPLE_STRING | Content | SENTINEL |
* |---------------------|---------|----------|
* | '+'| "hello" | "\r\n" |
* |------------------------------------------|
*/
var buf bytes.Buffer
buf.WriteRune(MAGIC_SIMPLE_STRING)
buf.Write(v)
buf.WriteString(SENTINEL)
return buf.Bytes()
}
net.Conn寫入目前,我們使用以下操作將回應寫入連線中:
func (s *simpleSrv) serve(conn net.Conn) {
defer conn.Close()
rd := bufio.NewReader(conn)
msg, err := rd.ReadString('\n')
if err != nil {
slog.Error("failed to read from conn:", slog.Any("error", err))
return
}
time.Sleep(time.Second * 3)
if _, err := conn.Write([]byte("ACK: " + strings.ToUpper(msg))); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}
只要將 []byte("ACK: " + strings.ToUpper(msg)) 改為 resp.SimpleString("OK").Marshal() 即可用 RESP Simple String 回傳。
Redis 提供了一系列方便的 CLI 工具,例如 redis-cli 可以拿來傳送並接收 RESP 的請求及回應,用來判斷目前的服務是否與 Redis 相容。
$ redis-cli -p 16379 ping
OK