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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容