在每次 server.(*simpleSrv).serve() 執行完之後都會呼叫 defer conn.Close(),所以每次都必須重新連線才能執行 Redis 指令。
從以下的 redis-cli 指令就可以很明顯地感覺到異狀:
$ redis-cli -p 16379
127.0.0.1:16379> ping
Error: Server closed the connection
not connected> ping
OK
127.0.0.1:16379> ping
Error: Server closed the connection
TCP 連線需要經過握手,這個過程對於系統資源是有極大消耗的,因此現代的 Server-Client 架構中都會盡量重複使用一個 TCP 連線(例如 HTTP 有 Keep-Alive),而在 Redis 中將這個稱為 Redis pipelining
要加入這個特性也很容易:只要把 server.(*simpleSrv).serve() 變成一個無限迴圈即可,並且在遇到錯誤時中止。
註:如果試圖從已經關閉的
net.Conn讀取資料或寫入資料會產生錯誤,因此可以利用這個特性跳出迴圈。
// file: internal/server/server.go
func (s *simpleSrv) serve(conn net.Conn) {
defer conn.Close()
rd := resp.NewReader(conn)
for {
ret, err := s.handler.ServeRESP(context.Background(), rd)
if err != nil {
return
}
if _, err := conn.Write(ret.Marshal()); err != nil {
slog.Error("failed to write to conn:", slog.Any("error", err))
return
}
}
}
然而,引入連線複用的機制之後卻也衍生了另一個問題:如果客戶端發生錯誤(例如輸入錯誤的指令或參數),連線也一樣會被關閉:這並不是我們預期的。
因此,我們可以將 server.(Handler).ServeRESP() 所回傳的 err 分為兩類:
註:其實這個思想跟 HTTP 4xx 與 5xx 的狀態碼是類似的,只不過 HTTP 的複雜度高得多。
定義 ErrClient 與 ErrServer,並且在讀取 Redis 指令回傳 resp.ErrProtocol 的時候,視為客戶端錯誤、其餘則是伺服端錯誤:
// file: ./internal/server/handler.go
var (
ErrClient = errors.New("client error")
ErrServer = errors.New("server error")
)
// ...
func (h *simpleHandler) ServeRESP(ctx context.Context, rd *resp.Reader) (resp.Value, error) {
cmd, err := resp.ReadCommand(rd)
if err != nil {
if errors.Is(err, resp.ErrProtocol) {
return resp.NewSimpleError(err), fmt.Errorf("%w: %w", ErrClient, err)
} else {
return nil, fmt.Errorf("%w: %w", ErrServer, err)
}
}
slog.Info("Read RESP command:", slog.Any("command", cmd))
return h.exec(ctx, cmd)
}