Go Redigo 源码分析(一) 实现Protocol协议请求redis

栏目: 数据库 · 发布时间: 5年前

内容简介:Redis是我们日常开发中使用的最常见的一种Nosql,是一个key-value存储系统,但是redis不止支持key-value,还自持很多存储类型包括字符串、链表、集合、有序集合和哈希。在go使用redis中有很多的开源库可以使用,我经常使用的是redigo这个库,它封装很多对redis的api、网络链接和连接池。分析Redigo之前我觉得需要知道如果不用redigo,我们该如何访问redis。之后才能更加简单方便的理解Redigo是做了一些什么事。

概述

Redis是我们日常开发中使用的最常见的一种Nosql,是一个key-value存储系统,但是 redis 不止支持key-value,还自持很多存储类型包括字符串、链表、集合、有序集合和哈希。

go 使用redis中有很多的开源库可以使用,我经常使用的是redigo这个库,它封装很多对redis的api、网络链接和连接池。

分析Redigo之前我觉得需要知道如果不用redigo,我们该如何访问redis。之后才能更加简单方便的理解Redigo是做了一些什么事。

Protocol协议

官方对protocol协议的定义 链接

网络层:

客户端和服务端用通过TCP链接来交互

请求

*<参数数量> CR LF

$<参数 1 的字节数量> CR LF

<参数 1 的数据> CR LF

...

$<参数 N 的字节数量> CR LF

<参数 N 的数据> CR LF

举个例子 get aaa = *2rn$3\r\nget\r\n$3rn$aaarn

每个参数结尾用rn $之后是参数的字节数

这样组成的一串命令通过tcp发送到redis服务端之后就是redis的返回了

返回

Redis的返回有5中情况:

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

下面按照5中情况各自举一个例子

状态回复:

请求: set aaa aaa

回复: +OKrn

错误回复:

请求: set aaa

回复: -ERR wrong number of arguments for 'set' commandrn

整数回复:

请求:llen list

回复::5rn

批量回复

请求: get aaa

回复: $3rnaaarn

多条批量回复

请求: lrange list 0 -1

回复: *3rn$3\r\naaa\r\n$3rndddrn$3rncccrn

实现

那么我们如何用go来实现不用redis框架,自己请求redis服务。其实也很简单,go提供很方便的net包让我们很容易的使用tcp

先看解析回复方法,封装了一个reply对象:

package client

import (
    "bufio"
    "errors"
    "fmt"
    "net"
    "strconv"
)

type Reply struct {
    Conn        *net.TCPConn
    SingleReply []byte
    MultiReply  [][]byte
    Source      []byte
    IsMulti     bool
    Err         error
}

// 组成请求命令
func MultiCommandMarshal(args ...string) string {
    var s string
    s = "*"
    s += strconv.Itoa(len(args))
    s += "\r\n"

    // 命令所有参数
    for _, v := range args {
        s += "$"
        s += strconv.Itoa(len(v))
        s += "\r\n"
        s += v
        s += "\r\n"
    }

    return s
}

// 预读取第一个字节判断是多行还是单行返回 分开处理
func (reply *Reply) Reply() {
    rd := bufio.NewReader(reply.Conn)
    b, err := rd.Peek(1)

    if err != nil {
        fmt.Println("conn error")
    }
    fmt.Println("prefix =", string(b))
    if b[0] == byte('*') {
        reply.IsMulti = true
        reply.MultiReply, reply.Err = multiResponse(rd)
    } else {
        reply.IsMulti = false
        reply.SingleReply, err = singleResponse(rd)
        if err != nil {
            reply.Err = err
            return
        }
    }
}

// 多行返回 每次读取一行然后调用singleResponse 获取单行数据
func multiResponse(rd *bufio.Reader) ([][]byte, error) {
    prefix, err := rd.ReadByte()
    var result [][]byte
    if err != nil {
        return result, err
    }
    if prefix != byte('*') {
        return result, errors.New("not multi response")
    }
    //*3\r\n$1\r\n3\r\n$1\r\n2\r\n$1\r\n
    l, _, err := rd.ReadLine()
    if err != nil {
        return result, err
    }
    n, err := strconv.Atoi(string(l))
    if err != nil {
        return result, err
    }
    for i := 0; i < n; i++ {
        s, err := singleResponse(rd)
        fmt.Println("i =", i, "result = ", string(s))
        if err != nil {
            return result, err
        }
        result = append(result, s)
    }

    return result, nil
}

// 获取单行数据 + - : 逻辑相同 $单独处理
func singleResponse(rd *bufio.Reader) ([]byte, error) {
    var (
        result []byte
        err    error
    )
    prefix, err := rd.ReadByte()
    if err != nil {
        return []byte{}, err
    }
    switch prefix {
    case byte('+'), byte('-'), byte(':'):
        result, _, err = rd.ReadLine()
    case byte('$'):
        // $7\r\nliangwt\r\n
        n, _, err := rd.ReadLine()
        if err != nil {
            return []byte{}, err
        }
        l, err := strconv.Atoi(string(n))
        if err != nil {
            return []byte{}, err
        }
        p := make([]byte, l+2)
        rd.Read(p)
        result = p[0 : len(p)-2]

    }

    return result, err
}

然后看下如何调用

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "net"
    "os"
    "strconv"
    "strings"
    "test/redis/rediscli/client"
)

var host string
var port string

func init() {
    // 参数获取 设置有默认值
    flag.StringVar(&host, "h", "localhost", "hsot")
    flag.StringVar(&port, "p", "6379", "port")
}

func main() {
    flag.Parse()

    porti, err := strconv.Atoi(port)
    if err != nil {
        panic("port is error")
    }
    
    tcpAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: porti}
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        log.Println(err)
    }
    defer conn.Close()

    for {
        fmt.Printf("%s:%d>", host, porti)
        bio := bufio.NewReader(os.Stdin)
        input, _, err := bio.ReadLine()
        if err != nil {
            fmt.Println(err)
        }
        s := strings.Split(string(input), " ")
        req := client.MultiCommandMarshal(s...)
        conn.Write([]byte(req))
        reply := client.Reply{}
        reply.Conn = conn
        reply.Reply()

        if reply.Err != nil {
            fmt.Println("err:", reply.Err)
        }
        var res []byte
        if reply.IsMulti {

        } else {
            res = reply.SingleReply
        }
        fmt.Println("result:", string(res), "\nerr:", err)
        //fmt.Println(string(p))
    }

}

总结

上面的代码我们看到根据不同的回复类型,用不同的逻辑解析。

其实所有的redis 处理框架的本质就是封装上面的代码,让我们使用更加方便。当然还有一些其他的功能 使用 Lua 脚本、发布订阅等等功能。

我觉得要理解redis库 首先要理解Protocol,然后再去看源码 否则你会看到很多你看不懂的逻辑和封装。所以先研究了下Protocol协议并自己实现了一下。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

鳥哥的Linux私房菜(第四版)

鳥哥的Linux私房菜(第四版)

鳥哥 / 碁峰資訊股份有限公司 / 2016-1-25 / TWD 980.00

本書前三版均蟬聯電腦專業書籍Linux暢銷排行榜Top1,為地表最暢銷的Linux中文書籍! 您是有意學習Linux的小菜鳥,卻不知如何下手?您是遨遊Linux的老鳥,想要一本資料豐富的工具書?本書絕對是最佳選擇! ※鳥哥傾囊相授,內容由淺入深 書中包含了鳥哥從完全不懂Linux到現在的所有歷程,鳥哥將這幾年來的所知所學傾囊相授,以最淺顯易懂的文字帶領您進入Linux的世界。 ......一起来看看 《鳥哥的Linux私房菜(第四版)》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具