iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫系列 第 3

Day 03:RESP 協定介紹與 Parser 基礎架構設計

  • 分享至 

  • xImage
  •  

要自己刻 Redis,第一步當然是要讓 Server 聽得懂 Client 在講什麼。
Redis 客戶端與伺服器之間進行通訊時,使用的是一種名為 RESP (Redis Serialization Protocol) 的序列化協定。

今天先把 RESP 的規格看懂,順便幫 Go 專案搭出 parser 需要的基本資料結構。


什麼是 RESP?

RESP 是 Redis 自己使用的應用層協定。它的好處是格式夠簡單,大部分內容也還看得懂;對手寫 parser 來說,這點很重要,不然第一週大概就卡死了。

在 RESP2 版本中,通訊的所有內容都由 5 種基本資料類型 組成,每種類型都以一個特定的 prefix byte 開始,並以 \r\n (CRLF) 結尾。

說實在,一開始看 RESP 的文件覺得那些前綴符號像是亂碼,多看幾次才覺得其實滿巧妙的。

RESP 5 大基本資料結構

資料類型 prefix byte 說明 範例與解析
Simple Strings + 簡單的單行字串,不可包含換行符,通常用於伺服器狀態回應(如 OK +OK\r\n
Errors - 單行的錯誤訊息,客戶端會將其解析為異常(Exception) -ERR unknown command\r\n
Integers : 十進位整數,長度不限但通常在 64 位元整數範圍內 :1000\r\n
Bulk Strings $ 二進位安全的字串。先標註長度,再傳送內容。支援換行與二進位資料。 $6\r\nfoobar\r\n (長度為 6 的字串 foobar)
Arrays * 複雜的多元素容器。先標註元素個數,後續接著各元素的 RESP 表示。 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n (包含兩個 Bulk String 的陣列)

此外,RESP 還支援特殊的空值表示:

  • Null Bulk String$-1\r\n(表示 Key 不存在等情況)
  • Null Array*-1\r\n

在 Go 中定義 RESP 資料結構

我在 code/resp/resp.go 中,將 RESP 的資料類型抽象為一個 Type(本質上是一個 byte),並使用一個整合的 Value 結構來代表任何一種可能被解析出來的 RESP 資料。

1. 類型定義與常數

package resp

// Type 代表 RESP 的資料類型
type Type byte

const (
	TypeSimpleString Type = '+'
	TypeError        Type = '-'
	TypeInteger      Type = ':'
	TypeBulkString   Type = '$'
	TypeArray        Type = '*'
)

2. 統一的 Value 結構

由於 Go 語言是靜態強型別語言,為了方便在一個變數中儲存不同類型的 RESP 資料,設計了一個包含所有型態屬性的結構,並透過輔助函數(Constructor Functions)來確保型別安全性:

type Value struct {
	Type  Type      // 資料的 RESP 型態
	Str   string    // 用於 SimpleString 和 Error
	Num   int       // 用於 Integer
	Bulk  []byte    // 用於 Bulk String
	Array []Value   // 用於 Array(遞迴巢狀定義)
}

// 輔助建構函數範例
func NewSimpleString(s string) Value {
	return Value{Type: TypeSimpleString, Str: s}
}

func NewBulkString(b []byte) Value {
	return Value{Type: TypeBulkString, Bulk: b}
}

這樣的設計滿乾淨的,Array 裡面再放 []Value,剛好可以處理 RESP Array 裡面還包另一個 Array 的情況。


搭建 Parser 基礎骨架

我將解析器的實作邏輯放置於 code/resp/parser.go

處理 TCP stream時不能每讀一點就直接碰底層連線,所以我先選 Go 標準庫的 bufio.Reader。它可以做緩衝讀取,也方便我後面按字元或按行解析。

1. Reader 結構與初始化

type Reader struct {
	rd *bufio.Reader
}

func NewReader(rd io.Reader) *Reader {
	return &Reader{rd: bufio.NewReader(rd)}
}

2. 實作基礎的 readLine

因為 RESP 協定中的所有行都以 \r\n (CRLF) 結尾,我們需要一個高效率的 readLine 函數來讀取一整行,並將結尾的 \r\n 剔除:

func (r *Reader) readLine() ([]byte, error) {
	// ReadBytes 會一直讀取到出現 '\n' 為止
	line, err := r.rd.ReadBytes('\n')
	if err != nil {
		return nil, err
	}
	n := len(line)
	// 驗證行長度至少為 2 (即只有 \r\n),且倒數第二個字元必須是 \r
	if n < 2 || line[n-2] != '\r' {
		return nil, errors.New("無效的行結尾格式,必須為 CRLF")
	}
	// 剔除結尾的 \r\n,只回傳中間的內容
	return line[:n-2], nil
}

3. 主解析方法分發

當開始解析一個完整的 RESP Value 時,首先讀取第一個位元組(Prefix Byte),並依據其值分發給對應的解析子函數:

func (r *Reader) ReadValue() (Value, error) {
	typ, err := r.rd.ReadByte()
	if err != nil {
		return Value{}, err
	}

	switch Type(typ) {
	case TypeSimpleString:
		return r.readSimpleString()
	case TypeError:
		return r.readError()
	case TypeInteger:
		return r.readIntegerType()
	case TypeBulkString:
		return r.readBulkString()
	case TypeArray:
		return r.readArray()
	default:
		return Value{}, errors.New("未知或不支援的 RESP 類型: " + string(typ))
	}
}

跑起來看看

我們來寫一小段 Go 程式,確認一下今天建的 Value 結構運作起來是什麼感覺。你可以把這段寫在 main.go 裡跑跑看:

package main

import (
	"fmt"
	"redis-clone/resp"
)

func main() {
	v1 := resp.NewSimpleString("OK")
	fmt.Printf("Type: %q, Value: %s\n", v1.Type, v1.Str)

	v2 := resp.NewBulkString("hello world")
	fmt.Printf("Type: %q, Value: %s\n", v2.Type, v2.Bulk)
}

執行下去:

$ go run main.go
# 預期輸出:
# Type: "+", Value: OK
# Type: "$", Value: hello world

看到這些型別常數乖乖被賦值,我們這層資料結構就算打好地基了!

總結

今天先把 RESP 的資料模型和 parser 骨架搭好。真正的解析邏輯還沒全部寫完,但 ValueReader 這兩個核心已經有雛形了。

明天先拿幾個最簡單的型別開刀,希望不要一開始就被 CRLF 搞到頭痛,明天見!


上一篇
Day 02:實作高併發 TCP Socket 伺服器與 Graceful Shutdown
下一篇
Day 04:實作 RESP Parser - 解析 Simple Strings, Errors 與 Integers
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言