第 3 章
Day 3、4:RESP2 Value 與 Reader
RESP:Redis Serialization Protocol
溝通需要共同的語言。要和 Redis 溝通,就要使用為它設計的 RESP。
RESP 有 RESP2 與 RESP3 兩個版本。這一章先實作較簡單的 RESP2。
注意:RESP3 大致上是 RESP2 的超集,自 Redis 6.0 起受到支援。
Redis 官方提供 Redis serialization protocol specification,說明 RESP 的設計原因與格式。
RESP Value
Magic Byte
RESP 透過 magic byte 辨識資料型別。RESP2 有五種型別:
- Simple String:
+ - Simple Error:
- - Integer:
: - Bulk String:
$ - Array:
*
在 pkg/resp/value.go 定義常數:
https://github.com/jr-dragon/olivine/blob/
// file: pkg/resp/value.go
package resp
const(
MAGIC_SIMPLE_STRING = '+'
MAGIC_SIMPLE_ERROR = '-'
MAGIC_INTEGER = ':'
MAGIC_BULK_STRING = '$'
MAGIC_ARRAY = '*'
)
注意:
pkg下的 package 用來提供其他開發者引用。可參考 golang-standards/project-layout。這不是 Go 官方標準,但能作為中大型專案組織方式的參考。
Value interface
Build Redis from scratch 使用下列 Value{}:
type Value struct {
typ string
str string
num int
bulk string
array []Value
}
這很簡單,但我偏好為所有 RESP 型別定義共同的 Value interface:
type Value interface {
Marshal() []byte
}
Marshal() 的命名受到 json.Marshal() 啟發,回傳要寫入連線、作為回應的內容。
Simple String
先建立實作 Value interface 的 SimpleString 型別。
type SimpleString []byte
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
internal/server/server.go 原本以 conn.Write([]byte("ACK: " + strings.ToUpper(msg))) 寫入回應。
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
}
}
- if _, err := conn.Write([]byte("ACK: " + strings.ToUpper(msg))); err != nil {
+ if _, err := conn.Write(resp.SimpleString("OK").Marshal()); err != nil {
func (s *simpleSrv) serve(conn net.Conn) {
defer conn.Close()
rd := bufio.NewReader(conn)
_, err := rd.ReadString('\n')
if err != nil {
slog.Error("failed to read from conn:", slog.Any("error", err))
return
}
if _, err := conn.Write(resp.SimpleString("OK").Marshal()); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}
使用 redis-cli 手動測試
Redis 提供許多工具,其中 redis-cli 能以 RESP 傳送請求、處理回應,適合檢查服務是否能與 Redis client 溝通。
$ redis-cli -p 16879 ping
OK
Redis Command:Array 與 BulkString
請求中的 Redis Command 是由 Bulk Strings 組成的 RESP Array。
Array{ BulkString{"PING"} }:PING指令。Array{ BulkString{"GET"}, BulkString{"bar"} }:帶有"bar"參數的GET指令。
BulkString Value
RESP 區分下列兩種 Bulk String 表示方式:
RESP3 才定義獨立的 Null 型別,因此這裡以 Null Bulk String 表示空值。
// file: pkg/resp/value.go
const (
MAGIC_SIMPLE_STRING = '+'
MAGIC_SIMPLE_ERROR = '-'
MAGIC_INTEGER = ':'
MAGIC_BULK_STRING = '$'
MAGIC_ARRAY = '*'
+ SENTINEL = "\r\n"
)
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()
}
Array Value
和 Bulk String 類似,RESP2 也定義了 Null Array。
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()
}
從 net.Conn 讀取
目前使用 *bufio.Reader 處理連線,但它不是專為 RESP 設計的。
考慮以下 reader:
rd := bufio.NewReader(conn)
for {
msg, _, err := rd.ReadLine()
if err != nil {
fmt.Printf("err = %+v\n", err)
break
} else {
fmt.Printf("msg = %s\n", msg)
}
}
傳入 *1\r\n$4\r\nPING\r\n 這個 Redis Command 時,必須分多次解析 msg,並不方便。
示範:Go Playground。
因此,我在 pkg/resp/io.go 建立 *resp.Reader:
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)
}
}
readLine() 與 readInt()
Redis Command 是 Bulk Strings 的 Array,在 Redis 內部也稱為 multibulk。這裡以 {MAGIC}{LENGTH}\r\n{DATA}\r\n 概括 Array 與 Bulk String 的格式。
在實作 readArray()、readBulkString() 前,先建立 readLine() 與 readInt():
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)
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()
Bulk String 是 binary-safe 字串,讀取時根據 header 指定的長度取得資料。
注意:C-style 字串以 null byte
\0結尾,因此無法直接表示任意位置都可能包含 null byte 的資料。
func (r *Reader) readBulkString() (BulkString, error) {
sz, err := r.readInt()
if err != nil {
return BulkString{}, err
}
if sz < 0 { // Ref: https://redis.io/docs/latest/develop/reference/protocol-spec/#null-bulk-strings
return BulkString{null: true}, nil
}
buf := make([]byte, sz)
if _, err := io.ReadFull(r.rd, buf); err != nil {
return BulkString{}, err
}
// Drop tailing "\r\n" from reader.
r.rd.ReadByte()
r.rd.ReadByte()
return BulkString{data: buf}, nil
}
readArray()
RESP Array 可以視為 RESP values 的集合,逐一遞迴讀取其中的值。
func (r *Reader) readArray() (Array, error) {
sz, err := r.readInt()
if err != nil {
return Array{}, err
}
if sz < 0 { // Ref: https://redis.io/docs/latest/develop/reference/protocol-spec/#null-arrays
return Array{null: true}, nil
}
arr := Array{}
arr.data = make([]Value, 0, sz)
for range sz {
v, err := r.Read()
if err != nil {
return arr, err
}
arr.data = append(arr.data, v)
}
return arr, nil
}
處理 Redis Commands
internal/server/server.go 原本使用 msg, err := rd.ReadString('\n'),現在改用 *resp.Reader。
rd := resp.NewReader(conn)
- msg, err := rd.ReadBytes('\n')
+ cmd, err := rd.Read()
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
}
}