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

蔡健雅的红色高跟鞋 · · 2592 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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协议

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2592 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传