前面我在 resp 模組寫了 RESP 解析器,db 模組也建好了記憶體 KV 引擎,但這兩個現在還是各過各的。
今天要在 command 模組裡補上Command Dispatcher,讓 TCP Server 收到的 RESP 指令真的能打到底層 DB。
Command Dispatcher有點像整個系統的入口守門員。它要負責幾件事:
CommandFunc)。AUTH 驗證。我在 code/command/dispatcher.go 裡實作了這個機制。
我先定義統一的 CommandFunc 簽名,然後在 Dispatcher 裡維護命令對應表和寫指令標記:
type CommandFunc func(dbEngine *db.DB, client *Client, args [][]byte) resp.Value
type Dispatcher struct {
mu sync.RWMutex
handlers map[string]CommandFunc
writeCmds map[string]bool // 標記寫操作命令
dbEngine *db.DB
requirePass string
aofWriteCallback func(val resp.Value) // AOF/Replication 回呼
}
我們使用 Register 方法動態註冊命令,並明確標註其是否為寫操作命令。這對於後續的持久化(AOF)和複製(Replication)至關重要:
func (d *Dispatcher) Register(name string, handler CommandFunc, isWrite bool) {
d.mu.Lock()
defer d.mu.Unlock()
upperName := strings.ToUpper(name)
d.handlers[upperName] = handler
if isWrite {
d.writeCmds[upperName] = true
}
}
Dispatch這是整個資料庫命令執行的入口。這邊我一開始忘記把 cmdName 轉成大寫,結果用 redis-cli 敲小寫的 set 直接炸開報 unknown command,這才趕快補上 strings.ToUpper。
它將接收到的 RESP 值轉換成命令,並執行安全校驗:
func (d *Dispatcher) Dispatch(client *Client, val resp.Value) resp.Value {
// 1. 驗證格式必須是 Array,且第一個字元為 BulkString (命令名)
if val.Type != resp.TypeArray || len(val.Array) == 0 {
return resp.NewError("ERR Protocol error: invalid bulk/array format")
}
cmdVal := val.Array[0]
cmdName := strings.ToUpper(string(cmdVal.Bulk))
// 2. 提取參數 (其餘元素)
args := make([][]byte, 0, len(val.Array)-1)
for i := 1; i < len(val.Array); i++ {
args = append(args, val.Array[i].Bulk)
}
d.mu.RLock()
handler, exists := d.handlers[cmdName]
isWrite := d.writeCmds[cmdName]
d.mu.RUnlock()
if !exists {
return resp.NewError("ERR unknown command '" + cmdName + "'")
}
// 3. 安全與密碼校驗 (AUTH)
if cmdName == "AUTH" {
// ... 密碼驗證邏輯 ...
}
if d.requirePass != "" && !client.Authenticated {
return resp.NewError("NOAUTH Authentication required.")
}
// 4. 執行命令
reply := handler(d.dbEngine, client, args)
// 5. 若是寫指令且成功執行,觸發 AOF 與主從同步
if isWrite && reply.Type != resp.TypeError {
// 觸發 AofWriteCallback 廣播給所有從節點與寫入 AOF 檔案
}
return reply
}
在 code/server/server.go 的 handleConnection 中,我把原本單純的 Echo 邏輯換成了真正的命令執行:
// 每次 Accept 後,建立一個 Client 上下文狀態
clientCtx := command.NewClient(conn, s.password != "")
reader := resp.NewReader(conn)
for {
val, err := reader.ReadValue()
if err != nil {
break
}
// 呼叫dispatcher 執行並回傳
reply := s.dispatcher.Dispatch(clientCtx, val)
if reply.Type != 0 {
_, err = conn.Write(reply.Marshal())
if err != nil {
break
}
}
}
先把 Server 跑起來:
go run ./code/main.go
這次我們開兩個終端機來測試 Pub/Sub 訂閱功能。
終端機 1 (Subscriber):
# SUBSCRIBE news
printf "*2\r\n\$9\r\nSUBSCRIBE\r\n\$4\r\nnews\r\n" | nc localhost 6379
# 預期回覆:
# *3
# $9
# subscribe
# $4
# news
# :1
# (然後卡住等待訊息)
終端機 2 (Publisher):
# PUBLISH news "hello"
printf "*3\r\n\$7\r\nPUBLISH\r\n\$4\r\nnews\r\n\$5\r\nhello\r\n" | nc localhost 6379
# 預期回覆::1 (代表 1 個訂閱者收到)
此時回到終端機 1,應該會看到推送過來的訊息:
*3
$7
message
$4
news
$5
hello
今天總算把 Command Dispatcher 接起來,RESP 請求可以從網路層一路分發到 handler,再把結果寫回 client。這個 dispatcher 第一版其實我少傳了一個 context,結果 client 斷線 goroutine 沒清掉,後來才加上連線管理。
明天要補 SET、GET、DEL 這些基礎命令的處理邏輯,先把最常用的打通!