用 Go 来了解一下 Redis 通讯协议

    有了服务端,有了客户端,他们俩是怎样通讯,又是基于什么通讯协议做出交互的呢?

    基于我们的目的,本文主要讲解和实践 Redis 的通讯协议

    Redis 的客户端和服务端是通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379

    客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾(这是一条约定)

    在 Redis 中分为请求回复,而请求协议又分为新版和旧版,新版统一请求协议在 Redis 1.2 版本中引入,最终在 Redis 2.0 版本成为 Redis 服务器通信的标准方式

    本文是基于新版协议来实现功能,不建议使用旧版(1.2 挺老旧了)。如下是新协议的各种范例:

    1、 格式示例

    在该协议下所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的

    2、打印示例

    1. $3
    2. SET
    3. $5
    4. mykey
    5. $7
    6. myvalue

    3、实际协议值

    Redis 会根据你请求协议的不同(执行的命令结果也不同),返回多种不同类型的回复。在这个回复“协议”中,可以通过检查第一个字节,确定这个回复是什么类型,如下:

    • 状态回复(status reply)的第一个字节是 “+”
    • 错误回复(error reply)的第一个字节是 “-“
    • 整数回复(integer reply)的第一个字节是 “:”
    • 批量回复(bulk reply)的第一个字节是 “$”
    • 多条批量回复(multi bulk reply)的第一个字节是 “*”

    有了回复的头部标识,结尾的 CRLF,你可以大致猜想出回复“协议”是怎么样的,但是实践才能得出真理,斎知道怕是你很快就忘记了 ?

    1. package main
    2. import (
    3. "log"
    4. "net"
    5. "os"
    6. "github.com/EDDYCJY/redis-protocol-example/protocol"
    7. )
    8. const (
    9. Address = "127.0.0.1:6379"
    10. Network = "tcp"
    11. )
    12. func Conn(network, address string) (net.Conn, error) {
    13. conn, err := net.Dial(network, address)
    14. if err != nil {
    15. return nil, err
    16. }
    17. }
    18. func main() {
    19. // 读取入参
    20. args := os.Args[1:]
    21. if len(args) <= 0 {
    22. log.Fatalf("Os.Args <= 0")
    23. }
    24. // 获取请求协议
    25. // 连接 Redis 服务器
    26. redisConn, err := Conn(Network, Address)
    27. if err != nil {
    28. log.Fatalf("Conn err: %v", err)
    29. }
    30. defer redisConn.Close()
    31. // 写入请求内容
    32. _, err = redisConn.Write(reqCommand)
    33. if err != nil {
    34. log.Fatalf("Conn Write err: %v", err)
    35. }
    36. // 读取回复
    37. command := make([]byte, 1024)
    38. n, err := redisConn.Read(command)
    39. if err != nil {
    40. log.Fatalf("Conn Read err: %v", err)
    41. }
    42. // 处理回复
    43. reply, err := protocol.GetReply(command[:n])
    44. if err != nil {
    45. log.Fatalf("protocol.GetReply err: %v", err)
    46. }
    47. // 处理后的回复内容
    48. log.Printf("Reply: %v", reply)
    49. // 原始的回复内容
    50. log.Printf("Command: %v", string(command[:n]))
    51. }

    在这里我们完成了整个 Redis 客户端和服务端交互的流程,分别如下:

    1、读取命令行参数:获取执行的 Redis 命令

    2、获取请求协议参数

    3、连接 Redis 服务器,获取连接句柄

    4、将请求协议参数写入连接:发送请求的命令行参数

    5、从连接中读取返回的数据:读取先前请求的回复数据

    6、根据回复“协议”内容,处理回复的数据集

    7、输出处理后的回复内容及原始回复内容

    1. replyType := reply[0]
    2. switch replyType {
    3. case StatusReply:
    4. return doStatusReply(reply[1:])
    5. case ErrorReply:
    6. case IntegerReply:
    7. return doIntegerReply(reply[1:])
    8. case BulkReply:
    9. return doBulkReply(reply[1:])
    10. case MultiBulkReply:
    11. return doMultiBulkReply(reply[1:])
    12. default:
    13. return nil, nil
    14. }
    15. }
    16. func doStatusReply(reply []byte) (string, error) {
    17. if len(reply) == 3 && reply[1] == 'O' && reply[2] == 'K' {
    18. return OkReply, nil
    19. }
    20. if len(reply) == 5 && reply[1] == 'P' && reply[2] == 'O' && reply[3] == 'N' && reply[4] == 'G' {
    21. return PongReply, nil
    22. }
    23. return string(reply), nil
    24. }
    25. func doErrorReply(reply []byte) (string, error) {
    26. return string(reply), nil
    27. }
    28. func doIntegerReply(reply []byte) (int, error) {
    29. pos := getFlagPos('\r', reply)
    30. result, err := strconv.Atoi(string(reply[:pos]))
    31. if err != nil {
    32. return 0, err
    33. }
    34. return result, nil
    35. }

    在这里我们对所有回复类型进行了分发,不同的回复标志位对应不同的处理方式,在这里需求注意几项问题,如下:

    1、当请求的值不存在,会将特殊值 -1 用作回复

    2、服务器发送的所有字符串都由 CRLF 结尾

    3、多条批量回复是可基于批量回复的,要注意理解

    4、无内容的多条批量回复是存在的

    最重要的是,对不同回复的规则的把控,能够让你更好的理解 Redis 的请求、回复的交互过程 ?

    写这篇文章的起因,是因为常常在使用 Redis 时,只是用,你不知道它是基于什么样的通讯协议来通讯,这样的感觉是十分难受的

    通过本文的讲解,我相信你已经大致了解 Redis 客户端是怎么样和服务端交互,也清楚了其所用的通讯原理,希望能够对你有所帮助!

    最后,如果想详细查看代码,右拐项目地址:

    如果对你有所帮助,欢迎点个 Star ?