前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[译]使用 Go 语言读写Redis协议

[译]使用 Go 语言读写Redis协议

作者头像
李海彬
发布2019-05-08 10:42:04
5020
发布2019-05-08 10:42:04
举报
文章被收录于专栏:Golang语言社区Golang语言社区

原文: Reading and Writing Redis Protocol in Go 翻译整理: smallnest, 译文连接: 使用 Go 语言读写Redis协议。

这篇文章使用两个简单的Reader和Writer实现了redis客户端的读写协议,通过这两个实现可以容易地学习Redis协议是如何工作的。

如果你想寻找一个全功能的、产品级的Redis client, 推荐你看看 Gary Burd的 redigo

开始之前,建议你先阅读一下 Redis协议的介绍。

官方的协议可以在其网站上找到: protocol。 Redis的协议叫做 RESP (REdis Serialization Protocol),客户端和服务器端通过基于文本的协议进行通讯。

所有的服务器和客户端之间的通讯都使用以下5中基本类型:

  • 简单字符串: 服务器用来返回简单的结果,比如"OK"或者"PONG"
  • bulk string: 大部分单值命令的返回结果,比如 GET, LPOP, and HGET
  • 整数: 查询长度的命令的返回结果
  • 数组: 可以包含其它RESP对象,设置数组,用来发送命令给服务器,也用来返回多个值的命令
  • Error: 服务器返回错误信息

RESP的第一个字节表示数据的类型:

  • 简单字符串: 第一个字节是 "+", 比如 "+OK\r\n"
  • bulk string: 第一个字节是 "$", 比如 "$6\r\nfoobar\r\n"
  • 整数: 第一个字节是 ":", 比如 ":1000\r\n"
  • 数组: 第一个字节是 "", 比如 "2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
  • Error: 第一个字节是 "-", 比如 "-Error message\r\n"

基本了解Redis的协议之后,我们就可以实现它的读写器了。

RESPWriter

假定我们要实现的client很简单,只会发送 bulk string给服务器,下面的代码是它的实现:

代码语言:javascript
复制
 1package redis
 2import (
 3  "bufio"
 4  "io"
 5  "strconv"     // for converting integers to strings
 6)
 7var (
 8  arrayPrefixSlice      = []byte{'*'}
 9  bulkStringPrefixSlice = []byte{'$'}
10  lineEndingSlice       = []byte{'\r', '\n'}
11)
12type RESPWriter struct {
13  *bufio.Writer
14}
15func NewRESPWriter(writer io.Writer) *RESPWriter {
16  return &RESPWriter{
17    Writer: bufio.NewWriter(writer),
18  }
19}
20func (w *RESPWriter) WriteCommand(args ...string) (err error) {
21  // 首先写入数组的标志和数组的数量
22  w.Write(arrayPrefixSlice)
23  w.WriteString(strconv.Itoa(len(args)))
24  w.Write(lineEndingSlice)
25  // 写入批量字符串
26  for _, arg := range args {
27    w.Write(bulkStringPrefixSlice)
28    w.WriteString(strconv.Itoa(len(arg)))
29    w.Write(lineEndingSlice)
30    w.WriteString(arg)
31    w.Write(lineEndingSlice)
32  }
33  return w.Flush()
34}

注意我们并没有直接写入net.Conn,而是写入一个io.Writer对象中,这样方便我们写测试代码,测试代码中不必以来net包。

例如,我们可以使用bytes.Buffer来测试我们发送的命令:

代码语言:javascript
复制
1var buf bytes.Buffer
2writer := NewRESPWriter(&buf)
3writer.WriteCommand("GET", "foo")
4buf.Bytes() // *2\r\n$3\r\nGET\r\n$3\r\nfoo\r\n

RESPReader

客户端使用 RESPWriter 来写命令,而RESPReader用来读取返回结果, 它尝试从net.Conn中一直读取数据,直到读取到一个完整的响应。

需要引入的包:

代码语言:javascript
复制
1package redis
2import (
3  "bufio"
4  "bytes"
5  "errors"
6  "io"
7  "strconv"
8)

定义常量和错误:

代码语言:javascript
复制
 1const (
 2  SIMPLE_STRING = '+'
 3  BULK_STRING   = '$'
 4  INTEGER       = ':'
 5  ARRAY         = '*'
 6  ERROR         = '-'
 7)
 8var (
 9  ErrInvalidSyntax = errors.New("resp: invalid syntax")
10)

RESPWriter一样, RESPReader并不关心它读取到的对象的细节,它所有的工作就是从连接中读取一个完整的RESP对象。 它需要传入一个io.Reade用来读取,并且在内部使用bufio.Reader包装了一下。RESPReader的定义很简单:

代码语言:javascript
复制
1type RESPReader struct {
2  *bufio.Reader
3}
4func NewReader(reader io.Reader) *RESPReader {
5  return &RESPReader{
6    Reader: bufio.NewReaderSize(reader, 32*1024),
7  }
8}

这个缓存大小只是在开发过程中拍脑袋定的,在实际使用中,你可以需要它是可配置的,并且在测试中进行调优。32KB在开发测试中应该没有问题。

RESPReader只有一个暴露的方法:ReadObject(),它返回这个RESP Object的字节slice。它会返回读取中的错误,以及解析命令的时候的错误。

RESP的第一个字节意味这我们只需读取第一个字节就可以直到如何处理接下来的数据,但是我们总是需要读取一行数据,所以我们还是先读取第一行再处理:

代码语言:javascript
复制
 1func (r *RESPReader) ReadObject() ([]byte, error) {
 2  line, err := r.readLine()
 3  if err != nil {
 4    return nil, err
 5  }
 6  switch line[0] {
 7  case SIMPLE_STRING, INTEGER, ERROR:
 8    return line, nil
 9  case BULK_STRING:
10    return r.readBulkString(line)
11  case ARRAY:
12    return r.readArray(line) default:
13    return nil, ErrInvalidSyntax
14  }
15}

如果我们读取的这一行是简单字符串、整数或者是Error,我们只需返回这完整的一行就可以了,因为这一行包含了完整的RESP对象。 在readLine方法中,我们一直读取直到读取到\n,并且检查它前一个字符是否是\r, 如果是返回这一行数据 (注意line结尾中包含\r\n):

代码语言:javascript
复制
 1func (r *RESPReader) readLine() (line []byte, err error) {
 2  line, err = r.ReadBytes('\n')
 3  if err != nil {
 4    return nil, err
 5  }
 6  if len(line) > 1 && line[len(line)-2] == '\r' {
 7    return line, nil
 8  } else {
 9    // Line was too short or \n wasn't preceded by \r.
10    return nil, ErrInvalidSyntax
11  }
12}

接下来我们在看看readBulkString的实现。我们需要解析长度值,以便我们接下来决定要读取多少字节。这样我们可以读取count字节的数据,并且再读取\r\n:

代码语言:javascript
复制
 1func (r *RESPReader) readBulkString(line []byte) ([]byte, error) {
 2  count, err := r.getCount(line)
 3  if err != nil {
 4    return nil, err
 5  }
 6  if count == -1 {
 7    return line, nil
 8  }
 9  buf := make([]byte, len(line)+count+2)
10  copy(buf, line)
11  _, err = io.ReadFull(r, buf[len(line):])
12  if err != nil {
13    return nil, err
14  }
15  return buf, nil
16}

其中getCount单独抽取出来了,因为解析数组的时候也需要它:

代码语言:javascript
复制
1func (r *RESPReader) getCount(line []byte) (int, error) {
2  end := bytes.IndexByte(line, '\r')
3  return strconv.Atoi(string(line[1:end]))
4}

为了处理数组,我们首先需要解析数组的数量,然后循环地调用ReadObject,将读取到的字节slice放入到结果buffer中:

代码语言:javascript
复制
 1func (r *RESPReader) readArray(line []byte) ([]byte, error) {
 2  // Get number of array elements.
 3  count, err := r.getCount(line)
 4  if err != nil {
 5    return nil, err
 6  }
 7  // Read `count` number of RESP objects in the array.
 8  for i := 0; i < count; i++ {
 9    buf, err := r.ReadObject()
10    if err != nil {
11      return nil, err
12    }
13    line = append(line, buf...)
14  }
15  return line, nil
16}

最后总结

上面的百行代码就实现了完整的读写Redis RESP对象,但是在应用到产品环境之前,还有一些东西需要补上:

  • 需要从RESP对象中读取实际的值。当前RESPReader只是返回整个的RESP字节slice,它并没有返回字符串或者整数,当然实现起来也很容易
  • RESPReader需要更好的错误处理

代码也没进行优化,对于内存分配和复制的优化也没有做,你可以看看成熟的产品级的redis库的实现。

如果想在服务器端读写Redis命令,可以考虑tidwall/redcon库。


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • RESPWriter
  • RESPReader
  • 最后总结
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档