分享
  1. 首页
  2. 文章

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

大二小的宝 · · 1254 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

概述

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 = *2rn3ドル\r\nget\r\n3ドルrn$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
回复: 3ドルrnaaarn
多条批量回复
请求: lrange list 0 -1
回复: *3rn3ドル\r\naaa\r\n3ドルrndddrn3ドルrncccrn

实现

那么我们如何用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\n1ドル\r\n3\r\n1ドル\r\n2\r\n1ドル\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协议并自己实现了一下。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:大二小的宝

查看原文:Go Redigo 源码分析(一) 实现Protocol协议请求redis

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

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

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏