iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

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

[Day 09] RESP(下):Redis 的通訊協定--指令

  • 分享至 

  • xImage
  •  

在昨天的文章中有提到,本質上 redis-cli 在發送指令的時候就是發送一組「由 BulkString 組成的 Array」,在未來的文章中我們會用「Redis 指令」來代稱這個結構。

註:事實上,Redis 嚴格要求 RESP 必須是 BulkString 組成的 Array,如果在 Array 中混入不屬於 BulkString 的資料會回報錯誤。

resp.(*Command) 的實作

定義一個 resp.(*Command) 來統一處理 Redis 指令:

// file: ./pkg/resp/cmd.go

package resp

import (
	"errors"
	"fmt"
)

var (
	ErrProtocol = errors.New("Protocol error")
)

type Command struct {
	raw  Value
	cmd  BulkString
	args []BulkString
}

func ReadCommand(rd *Reader) (*Command, error) {
	v, err := rd.Read()
	if err != nil {
		return nil, err
	}

	// 判定傳送過來的 RESP Request 必定是一個 Array
	arr, ok := v.(Array)
	// 判定 Array 中的首項(指令),必定是 BulkString
	cmd, ok := arr.data[0].(BulkString)
	if !ok {
		return nil, fmt.Errorf("%w: command expected bulk string, got %T(%+v)", ErrProtocol, arr.data[0], arr.data[0])
	}

	args := make([]BulkString, 0, len(arr.data)-1)
	for i := range arr.data[1:] {
		// 判定 Array 中的其它項(參數),必定是 BulkString
		arg, ok := arr.data[i+1].(BulkString)
		if !ok {
			return nil, fmt.Errorf("%w: argument [%d] expected bulk string, got %T(%+v)", ErrProtocol, i, arr.data[i+1], arr.data[i+1])
		}
		args = append(args, arg)
	}

	return &Command{
		raw:  v,
		cmd:  cmd,
		args: args,
	}, nil
}

server.(*simpleSrv) 中套用 resp.ReadCommand()

// file: ./internal/server/server.go
func (s *simpleSrv) serve(conn net.Conn) {
	defer conn.Close()

	rd := resp.NewReader(conn)
	cmd, err := resp.ReadCommand(rd)
	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
	}
}

Server 與 Handler

在一些小型的服務中,Handler 可以是 Server 的一部份:Server 藉由 Handler 來處理請求。然而,當服務不斷擴張之後就會漸漸發現 Server 與 Handler 有著不同的職責。

  • Server:監聽一個連接埠、並且維持一個連線
  • Handler:讀取連線中的資料,然後經過某些處理之後再將回應寫回連線

server.(Handler) 介面與實作

建立 server.(Handler) 介面:

// file: ./internal/server/handler.go
// file: internal/server/handler.go
package server

type Handler interface {
	ServeRESP(context.Context, *resp.Reader) (resp.Value, error)
}

func NewHandler() Handler {
	return &simpleHandler{}
}

type simpleHandler struct {}

func (h *simpleHandler) ServeRESP(ctx context.Context, rd *resp.Reader) (resp.Value, error) {
	cmd, err := resp.ReadCommand(rd)
	if err != nil {
		return nil, err
	}

	slog.Info("Read RESP command:", slog.Any("command", cmd))

	return resp.SimpleString("OK"), nil
}

應該有些人會感到疑惑,為什麼上面定義的 server.Handler.ServeRESP() 的參數是 context.Context*resp.Reader,而不單純只用 *resp.Reader 作為參數。

雖然我們目前還沒有用上 context.Context,但是它在許多場景下會發揮意想不到的作用,因此這邊在設計時就加入,而我們會在後續的內容中補充為什麼需要它。

事實上,標準庫中的 HTTP 一開始並沒有設計 context.Context 的支援,但為了不破壞向前相容的承諾,所以在 1.7 版才在標準庫中加入相關函式進行支援。

並且更新 server.(*simpleSrv)

// file: internal/server/server.go
func NewServer() Server {
	return &simpleSrv{
		handler: NewHandler(),
	}
}

func (s *simpleSrv) serve(conn net.Conn) {
	defer conn.Close()

	rd := resp.NewReader(conn)

	ret, err := s.handler.ServeRESP(context.Background(), rd)
	if err != nil {
		return
	}
	if ret == nil { // avoid panic
		ret = resp.SimpleString("OK")
	}

	if _, err := conn.Write(ret.Marshal()); err != nil {
		slog.Error("failed to write to conn:", slog.Any("error", err))
		return
	}
}

server.NewServer() 中調用了 server.NewHandler(),這意謂著 server.(Server) 會依賴於 server.(Handler),這並不是個好習慣:

  • 當彼此的依賴變得複雜之後,會讓管理上變得棘手
  • 未來如果想要抽換不同的 Handler 實作(這在測試時很常見),需要重新設計 NewHandler 的邏輯

這時,我們可以使用 相依注入(依賴注入) 這個設計模式,使 NewServer() 接受一個 server.(Handler) 作為參數,如此一來就可以在調用時決定使用哪一個實作。

// file: ./internal/server/server.go
func NewServer(handler Handler) Server {
	return &simpleSrv{
		handler: handler,
	}
}

當然,也不要忘記修改 main.NewApp()

// file: ./cmd/olivine/app.go
func NewApp() *App {
	return &App{
		srv: server.NewServer(server.NewHandler()),
	}
}

註:目前程式碼的規模還不算太大,我們還不需要使用相依注入的相關函式庫或工具,不過我們會在未來介紹相關內容。


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

尚未有邦友留言

立即登入留言