在昨天的文章中有提到,本質上 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
}
}
在一些小型的服務中,Handler 可以是 Server 的一部份: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),這並不是個好習慣:
這時,我們可以使用 相依注入(依賴注入) 這個設計模式,使 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()),
}
}
註:目前程式碼的規模還不算太大,我們還不需要使用相依注入的相關函式庫或工具,不過我們會在未來介紹相關內容。