iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

從零開始打造 Redis:以 Go 建立 Production Ready 應用程式系列 第 7

[Day 07] RESP2(上):Redis 的通訊協定--資料型態與值

  • 分享至 

  • xImage
  •  

我們必須要講相同的語言,才有辦法與其它人溝通。同樣地,如果我們想要與 Redis 溝通,就必須使用 Redis serialization protocol(簡稱 RESP),這是一個由 Redis 設計的通訊協定。

目前主流有兩個版本:RESP2 與 RESP3。RESP3 是 RESP2 的超集:換句話說,新版的 Redis 是兼容 RESP2 與 RESP3 的。

Redis 的官方文件 Redis serialization protocol specification 有闡述 RESP 是如何、為什麼這麼設計,非常值得一讀。

RESP Value

RESP 基於「值(Value)」來表達資料,值可以有不同的資料型態--在 RESP2 中支援 5 種不同的資料型態,而在 RESP3 中擴充到一共 15 種。

資料型態與魔術字元

RESP 利用 magic byte(魔術字元) 來辨識資料型態,在資料的前方都會有一個 byte 標記,代表資料型態:

  • Simple String 簡單字串+
  • Simple Error 簡單錯誤-
  • Integer 整數:
  • Bulk String 長字串$
  • Array 陣列*

據此,我們定義出相應的常數:

// 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.bulkValue.array 的記憶體空間就會被浪費掉。更何況,我還需要根據 Value.typ 才能判定哪個欄位是有效的。

因此,我認為直接定義一個 resp.(Value) 的介面比較合適:

// file: ./pkg/resp/value.go
type Value interface {
	Marshal() []byte
}

Marshal() 的靈感源於 json.Marshal(),代表的是將會被寫入回應的 []byte

Simple String

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 工具,例如 redis-cli 可以拿來傳送並接收 RESP 的請求及回應,用來判斷目前的服務是否與 Redis 相容。

$ redis-cli -p 16379 ping
OK

上一篇
[Day 06] Server:伺服器提供者
下一篇
[Day 08] RESP(中):Redis 的通訊協定--BulkString 與 Array
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言