美文网首页我爱编程
go-redis源码分析(一):redis协议

go-redis源码分析(一):redis协议

作者: cuihang | 来源:发表于2017-03-09 04:09 被阅读0次

    redis.v5是一款基于golang的redis操作库,封装了对redis的各种操作

    源码地址是
    https://github.com/go-redis/redis

    Redis客户端的工作本质上是基于tcp协议向redis server传输符合redis协议的命令请求,并根据redis协议解析server端的返回值
    我们可以通过telnet工具来模拟这一过程,例如ping命令我们可以这样发送请求

    $ telnet 127.0.0.1 6379
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    
    // 以下是发送的内容
    *1
    $4
    PING
    
    // 这是redis server返回内容
    +PONG
    

    所以要想理解redis客户端,首先要熟悉redis协议
    redis的协议由请求协议响应协议两部分组成,都是非常简单的通讯协议,易于程序解析,也方便人类进行阅读
    需要注意一点的是早期版本的redis协议和如今的不太一样,所以特别提醒的是本文是基于redis 3.2.6版本。

    请求协议:

    * <参数数量> CR LF
    $ <参数 1 的字节数量> CR LF
    <参数 1 的数据> CR LF
    ... 
    $ <参数 N 的字节数量> CR LF
    <参数 N 的数据> CR LF
    

    我们以开头的 telnet模拟发送 ping 命令 作为例子
    其中第一行星号后面表示本次传输的命令个数。1表示本次请求只有一个参数,同样的道理对于get命令而言,参数是两个(get key),所以对于get参数而言应该写成2
    紧接着后面开始一个一个传递请求参数,每一个参数用两行表示,其中上一行$n表示参数的字符数,下一行是参数的字符串
    例如上面的例子,$4表示这个命令有4个字符,下一行的ping就是该命令的字符串表示

    同样的道理,set命令可以这样写

    *3
    $3
    SET
    $3
    key
    $5
    value
    

    用byte数组可以这样写

    "*3\r\n$3\r\nset\r\n$3key\r\n$5value\r\n"
    

    返回值是

    +OK
    

    说明命令被成功解析并执行

    响应协议:

    说完了请求协议,我们再来看看响应协议,与拥有统一格式的请求协议相比,响应协议稍微复杂一些,原因也很简单,因为不同命令的响应结果是不同的,所以我们分别来看

    首先redis返回文本的第一个字节标示了本次响应的类型,其中响应类型一共如下:

    状态响应(status reply)的第一个字节是 "+"
    错误响应(error reply)的第一个字节是 "-"
    整数响应(integer reply)的第一个字节是 ":"
    主体响应(bulk reply)的第一个字节是 "$"
    批量主体响应(multi bulk reply)的第一个字节是 "*"
    

    例如对ping命令来说,如果能够ping通,返回的是"+PONG",这是一个状态响应

    • 状态响应
      对于状态响应,一般的处理就是相客户端返回"+"之后的字符,例如ping命令返回"PONG",set命令返回"OK"

    • 错误响应
      错误响应的处理与状态响应类似,因为从某种意义上讲,错误也是一种状态,只是一种特殊的状态而已,所以错误响应的处理就是返回"-"之后的字符

    • 整数响应
      整数响应是处理例如INCR,TTL等命令的,这些命令直接返回一个整数,一般的处理就是返回":"之后的整数数字

    • 主体响应
      主体响应是用来返回字符串,是最常见的响应形式,例如GET命令等所有获取字符串的命令,都是通过主体响应或者批量主体响协议应来获取的
      主体响应的第一行"$"后面的数字表示返回字符串的长度,下一行返回字符串文本。如果该字符串为空,那么第一行将返回"$-1"

    • 批量主体响应
      批量主体响应是server端批量返回字符串的协议,非常类似于请求协议,第一行"*"之后的数字表示本次返回的字符串一共多少个,然后以主体响应协议来返回字符串

    好了,到这里我们就大致了解了redis的通讯协议。虽然我们是在分析别人写的代码,但纸上得来终觉浅,绝知此事要躬行,在分析源码的时候亲手敲一些代码是非常有益的。所以我用golang写了一个小程序来模拟redis的通讯协议,由于响应协议相对负责,我们暂时来模拟状态响应和主体响应两个协议

    golang代码如下:

    package main
    
    import (
        "fmt"
        "os"
        "net"
        "strconv"
    )
    
    const (
        RedisServerAddress = "127.0.0.1:6379"
        RedisServerNetwork = "tcp"
    )
    
    type RedisError struct {
        msg string
    }
    
    func (this *RedisError) Error() string {
        return this.msg
    }
    
    // 连接到redis server
    func conn() (net.Conn, error) {
        conn, err := net.Dial(RedisServerNetwork, RedisServerAddress)
    
        if err != nil {
            fmt.Println(err.Error())
            os.Exit(1)
        }
    
        return conn, err
    }
    
    // 将参数转化为redis请求协议
    func getCmd(args []string) []byte {
    
        cmdString := "*" + strconv.Itoa(len(args)) + "\r\n"
        for _, v := range args {
            cmdString += "$" + strconv.Itoa(len(v)) + "\r\n" + v + "\r\n"
        }
    
        cmdByte := make([]byte, len(cmdString))
    
        copy(cmdByte[:], cmdString)
    
        return cmdByte
    }
    
    func dealReply(reply []byte) (interface{}, error) {
    
        responseType := reply[0]
    
        switch responseType {
        case '+':
            return dealStatusReply(reply)
        case '$':
            return dealBulkReply(reply)
        default:
            return nil, &RedisError{"proto wrong!"}
    
        }
    
    }
    
    // 处理状态响应
    func dealStatusReply(reply []byte) (interface{}, error) {
        statusByte := reply[1:]
    
        pos := 0
        for _, v := range statusByte {
            if v == '\r' {
                break
            }
            pos++
        }
        status := statusByte[:pos]
    
        return string(status), nil
    }
    
    // 处理主体响应
    func dealBulkReply(reply []byte) (interface{}, error) {
    
        statusByte := reply[1:]
    
        // 获取响应文本第一行标示的响应字符串长度
        pos := 0
    
        for _, v := range statusByte {
            if v == '\r' {
                break
            }
            pos++
        }
    
        strlen, err := strconv.Atoi(string(statusByte[:pos]))
        if err != nil {
            fmt.Println(err.Error())
            os.Exit(1)
        }
    
        if strlen == -1 {
        return "nil", nil
    }
        nextLinePost := 1
        for _, v := range statusByte {
            if v == '\n' {
                break
            }
            nextLinePost++
        }
    
        result := string(statusByte[nextLinePost:nextLinePost+strlen])
        return result, nil
    }
    
    func main() {
        args := os.Args[1:]
    
        if len(args) == 0 {
            fmt.Println("usage: go run proto.go + redis command\nfor example:\ngo run proto.go PING")
            os.Exit(0)
        }
    
        conn, _ := conn()
    
        cmd := getCmd(args)
    
        conn.Write(cmd)
    
        buf := make([]byte, 1024)
    
        n, _ := conn.Read(buf)
    
        res, _ := dealReply(buf[:n])
        fmt.Println("redis的返回结果是 ", res)
    
    }
    

    运行代码:

    // 测试PING命令
    $go run proto.go PING
    redis的返回结果是  PONG
    
    // 测试SET命令
    $go run proto.go SET key value
    redis的返回结果是  OK
    
    // 测试GET命令(GET一个存在的键)
    $go run proto.go GET key 
    redis的返回结果是  value
    
    // 测试GET命令(GET一个不存在的键)
    $go run proto.go GET not_exist_key 
    redis的返回结果是  nil
    

    一切ok!

    PS:这段测试代码很潦草,很多异常情况没有考虑,主要是为了测试对redis的理解

    文章参考
    http://doc.redisfans.com/topic/protocol.html

    相关文章

      网友评论

        本文标题:go-redis源码分析(一):redis协议

        本文链接:https://www.haomeiwen.com/subject/eraigttx.html