根據 RESP 的規格,存在兩種 BulkString:
Redis 直到 RESP3 才定義了 NULL 資料型態,因此在 RESP2 時期都是使用 NULL Bulk String 來借指 NULL。
BulkString 由以下四個部份組成:
-
\r\n
綜上所述,我們可以定義:
// file: ./pkg/resp/value.go
type BulkString struct {
null bool
data []byte
}
func (v BulkString) Marshal() []byte {
/*
* Null Bulk String Format:
* |-------------------|--------|----------|
* | MAGIC_BULK_STRING | LENGTH | SENTINEL |
* |-------------------|--------|----------|
* | '-' | "-1" | "\r\n" |
* |-------------------|--------|----------|
*/
if v.null {
return []byte("$-1\r\n")
}
/*
* Non Null Bulk String Format:
* |-------------------|--------|---------|----------|
* | MAGIC_BULK_STRING | Length | Content | SENTINEL |
* |-------------------|--------|---------|----------|
* | '$' | "5" | "hello" | "\r\n" |
* |-------------------|--------|---------|----------|
*/
var buf bytes.Buffer
buf.WriteRune(MAGIC_BULK_STRING)
buf.WriteString(strconv.Itoa(len(v.data)))
buf.WriteString(SENTINEL)
buf.Write(v.data)
buf.WriteString(SENTINEL)
return buf.Bytes()
}
RESP Array 可以視為零或多個 Value 組成的值,Array 裡面可以容納所有不同的 RESP 資料型態,包括 Array 本身(可以以此構造巢狀結構)。
舉例來說:
Array{ SimpleString{"Hello"}, SimpleString{"World"} }:由兩個 SimpleString 組合而成的 ArrayArray{ BulkString{"Hello"}, SimpleString{"World"} }:由一個 BulkSTring 與一個 SimpleString 組合而成的 ArrayArray 的組成如下:
*
Marshal() 之後的值,視陣列長度決定有幾組\r\n
綜上所述,我們可以定義:
// file: ./pkg/resp/value.go
type Array struct {
null bool
data []Value
}
func (v Array) Marshal() []byte {
/*
* Null Array Format:
* |-------------|--------|----------|
* | MAGIC_ARRAY | LENGTH | SENTINEL |
* |-------------|--------|----------|
* | '*' | "-1" | "\r\n" |
* |-------------|--------|----------|
*/
if v.null {
return []byte("*-1\r\n")
}
/*
* Array Format:
* |-------------|--------|----------|-----------------------|
* | MAGIC_ARRAY | LENGTH | SENTINEL | Marshaled Element [0] |
* |-------------|--------|----------|-----------------------|
* | '*' | "1" | "\r\n" | "$4\r\nPING\r\n" |
* |-------------|--------|----------|-----------------------|
*/
var buf bytes.Buffer
buf.WriteRune(MAGIC_ARRAY)
buf.WriteString(strconv.Itoa(len(v.data)))
buf.WriteString(SENTINEL)
for _, v := range v.data {
buf.Write(v.Marshal())
}
return buf.Bytes()
}
resp.(Value)當 redis-cli 傳送指令時,可以把它想像成傳送了幾個由 BulkString 組成的 Array:
bar 參數net.Conn 讀取 resp.(Array)先前在 Day 06 實作的 server.(*simpleSrv).serve() 如下:
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
}
if _, err := conn.Write([]byte("ACK: " + strings.ToUpper(msg))); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}
bufio.(*Reader).ReadString('\n') 會從 net.Conn 讀取資料,直到遇到 \n 或 EOF 為止。這個做法在先前實作 ACK 服務時是完全可以接受的,但是在解析 Redis 指令的時候就會遇到一些問題:
\n 組成,例如 *1\r\n$4\r\nPING\r\n 就需要反覆解析 3 次才能得知意圖。\n 字元,這些字元也一樣會被視為結束符並輸出到 msg 中,這是非預期行為,將會導致一些邏輯上的問題。resp.(*Reader)因此,比較好的做法應該是為了 RESP 實作一個 resp.(*Reader),其本質上僅僅是一個 bufio.(*Reader) 的封裝:
// file: ./pkg/resp/io.go
type Reader struct {
rd *bufio.Reader
}
func NewReader(rd io.Reader) *Reader {
return &Reader{rd: bufio.NewReader(rd)}
}
func (r *Reader) Read() (Value, error) {
t, err := r.rd.ReadByte()
if err != nil {
return nil, err
}
switch t {
case MAGIC_ARRAY:
return r.readArray()
case MAGIC_BULK_STRING:
return r.readBulkString()
default:
return nil, fmt.Errorf("%w: %c", ErrUnknownType, t)
}
}
藉由 resp.(*Reader).Read(),我們預期可以從 net.Conn 中取得 RESP Value 或出錯的話會回傳 error。
註:你或許已經注意到了,在
Read()中只處理了MAGIC_ARRAY與MAGIC_BULK_STRING兩種情況,這是因為解析 Redis 指令的功能暫時還不需要其它的資料型態。
readLine() 及 readInt()誠如先前所說,Redis 指令本質上是「由 BulkString 組成的 Array」(在 Redis 內部將其稱為 "multibulk"),而無論是 Array 還是 BulkString,都可以被寫為以下格式 {MAGIC}{LENGTH}\r\n{DATA}\r\n。
因此,我們可以在 resp.(*Reader) 中先建立 readLine() 與 readInt(),以便後續建立 readArray() 與 readBulkString()。
// file: ./pkg/resp/io.go
func (r *Reader) readLine() ([]byte, error) {
var buf bytes.Buffer
for {
data, err := r.rd.ReadBytes('\n')
if err != nil {
return nil, err
}
buf.Write(data)
// 在這裡多判斷倒數第二個字元是不是 \r,藉此確定是不是 SENTINEL
if data[len(data)-2] == '\r' {
return buf.Bytes()[:buf.Len()-2], nil
}
}
}
func (r *Reader) readInt() (int, error) {
line, err := r.readLine()
if err != nil {
return 0, err
}
return strconv.Atoi(string(line))
}
readBulkString()BulkString 被設計為是一個 Binary Safe 字串:無論字串中出現什麼內容,都不影響該字串在已被規定的長度下的表現。(換句話說,我們不能夠將 BulkString 交給 readLine() 去解析,因為要是 BulkString 中出現 \r\n 就會發生預期外的結果)
補充:C 語言的字串其本質上是一個字元陣列,並且以
\0(Null Byte)作為結束符,這就不是一個 binary safe 的字串,而歷史上也有許多資訊安全漏洞是因為這個設計而導致。
// file: ./pkg/resp/io.go
func (r *Reader) readBulkString() (BulkString, error) {
sz, err := r.readInt()
if err != nil {
return BulkString{}, err
}
// 當讀出來的 sz < 0 時,表示它是一個 Null BulkString
if sz < 0 {
return BulkString{null: true}, nil
}
// 根據 sz 大小,初始化一個 buf 空間
buf := make([]byte, sz)
// 利用 io.ReadFull 一次性讀完整個 buf 的空間
if _, err := io.ReadFull(r.rd, buf); err != nil {
return BulkString{}, err
}
// 已經獲得所需的資料,把最後一個 \r\n 丟棄即可
r.rd.ReadByte()
r.rd.ReadByte()
return BulkString{data: buf}, nil
}
readArray()RESP Array 可以被想像成「許多 RESP 值的集合」,因此我們可遞迴地讀取:
// file: ./pkg/resp/io.go
func (r *Reader) readArray() (Array, error) {
sz, err := r.readInt()
if err != nil {
return Array{}, err
}
// 當讀出來的 sz < 0 時,表示它是一個 Null Array
if sz < 0 {
return Array{null: true}, nil
}
arr := Array{}
arr.data = make([]Value, 0, sz)
for range sz {
// 遞迴性地呼叫 r.Read(),用於解析每一個 Array 中的值
v, err := r.Read()
if err != nil {
return arr, err
}
arr.data = append(arr.data, v)
}
return arr, nil
}
resp.(Array)修改 server.(*simpleSrv).serve() 的實作方式,改為採用 resp.(*Reader):
// file: ./internal/server/server.go
func (s *simpleSrv) serve(conn net.Conn) {
defer conn.Close()
rd := resp.NewReader(conn)
cmd, err := rd.Read()
if err != nil {
slog.Error("failed to read from conn:", slog.Any("error", err))
return
}
slog.Info("Read RESP command:", slog.Any("command", cmd))
if _, err := conn.Write(resp.SimpleString("OK").Marshal()); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}