Go 手写一个简易的 Redis 客户端(RESP协议)

4次阅读

RESP协议解析需先用Peek(1)识别前导符号:+/-/:/$/*/对应简单字符串、错误、整数、批量字符串、数组,$-1和-1表示NULL,$N后读N字节+rn,N递归解析N项,须处理半包粘包及io.ErrUnexpectedEOF。

Go 手写一个简易的 Redis 客户端(RESP协议)

RESP 协议解析的核心是识别前导符号

redis 使用 RESP(redis Serialization Protocol)传输数据,不是 jsON 也不是纯文本。客户端必须能根据首字节区分类型:+(简单字符串)、-(错误)、:(整数)、$(批量字符串)、*(数组)。忽略这个规则,直接 bufio.ReadString('n') 读取会卡死或解析错乱。

实操建议:

  • bufio.Reader.Peek(1) 先看第一个字节,再决定后续读法
  • $-1 表示 null bulk string,*-1 表示 null Array,必须支持,否则 GET missing_key 会解析失败
  • 批量字符串长度后跟 rn,内容本身不包含 rn,但可能含任意二进制字节——别用 strings.Split 处理

写入命令必须严格遵循 RESP 编码格式

go 标准库没有内置 RESP 编码器,手写时容易漏掉 rn 或错用换行。比如 SET key value 对应的 wire format 是:

*3rn$3rnSETrn$3rnkeyrn$5rnvaluern

常见错误:

  • n 替代 rn → Redis 返回 Protocol Error: expected 'rn'.
  • 字符串长度没转成十进制 ASCII(如写 $3 而非 $0x3)→ 实际必须是 $3,不是十六进制
  • 数组项数量写错:比如传了 4 个参数却写 *3 → Redis 静默截断或报错

连接与读写需处理半包和粘包

网络 IO 不保证一次 Read() 拿到完整 RESP 帧。例如一个 *2rnrnGETrnrnfoorn 可能被分两次到达。不能假设 reader.ReadString('n') 就能拿到完整行——因为 $ 类型的内容里可能有换行(虽然 Redis 命令值一般不含,但协议允许)。

正确做法:

  • $N 类型,先读出 N,再调用 io.ReadFull(reader, buf[:N]) 确保读满
  • *N 类型,递归解析 N 个子项,每项都按自身前导符处理
  • net.Conn.SetReadDeadline() 防止阻塞,尤其在解析 $ 后等待内容时

支持基本命令只需实现 5 种响应类型解析

不需要一开始就兼容所有 Redis 类型。先跑通 SET/GET/PING 就够验证链路。对应 RESP 解析分支只有:

  • + → 读到 rn 前为止,作为 string(如 +OKrn
  • - → 同上,但存为 error(如 -ERR unknown command 'xxx'rn
  • : → 读整数,如 :1000rnint64(1000)
  • $N → 若 N == -1 返回 nil;否则读 N 字节 + rn
  • *N → 若 N == -1 返回 nil;否则递归解析 N 个项,合并为 []Interface{}

真正容易被忽略的是错误传播:比如 $ 后读不满 N 字节,io.ReadFull 返回 io.ErrUnexpectedEOF,这必须原样返回,不能吞掉或转成空字符串——否则上层无法区分“键不存在”和“网络中断”。

text=ZqhQzanResources