Go Redigo Source Analysis Implements Protocol Protocol Request redis

Posted by ahmed17 on Fri, 24 May 2019 18:46:39 +0200

Summary

Redis is the most common Nosql used in our daily development and is a key-value storage system, but redis supports not only key-value, but also many storage types including strings, chain tables, collections, ordered collections, and hashes.
There are many open source libraries available in go using redis, and I often use redigo, which encapsulates a number of api, network links, and connection pools for redis.
Before analyzing Redigo, I thought I needed to know how to access redis without using redigo.Then it's easier to understand what Redigo is doing.

Protocol Protocol

Official Definition of protocol Agreement : link

Network layer:

Clients and servers interact through TCP links

request

*<Number of Parameters> CR LF
$<Number of bytes for parameter 1> CR LF
<Data for Parameter 1> CR LF
...
$<Number of bytes for parameter N> CR LF
<Data for parameter N> CR LF
Example get AAA = *2r n$3\rn getr\n$3r n$aaa R n
Each parameter ends with rn $followed by the number of bytes of the parameter
This consists of a sequence of commands that are sent to the redis server via tcp and then returned by redis

Return

Redis returns five cases:

  • The first byte of status reply is'+'
  • The first byte of an error reply is'-'
  • The first byte of an integer reply is':'
  • The first byte of a bulk reply is'$'
  • The first byte of a multiple bulk reply is'*'

Here is an example of each of the five scenarios

Status Reply:
Request: set aaa aaa
Reply: +OKrn
Error response:
Request: set aaa
Reply: -ERR wrong number of arguments for'set'commandrn
Integer Reply:
Request: llen list
Reply:: 5rn
Bulk Reply
Request: get aaa
Reply: $3rnaaarn
Multiple batch replies
Request: lrange list 0-1
Reply: *3r n$3\r\naaa\r\n$3rndddrn$3rncccrn

Realization

So how do we use go to implement a redis service without using the redis framework?In fact, it is very simple. go provides a convenient net package to make it easy to use tcp
First, look at the parse reply method, which encapsulates a reply object:

package client

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

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

// Compose Request Command
func MultiCommandMarshal(args ...string) string {
    var s string
    s = "*"
    s += strconv.Itoa(len(args))
    s += "\r\n"

    // Command All Parameters
    for _, v := range args {
        s += "$"
        s += strconv.Itoa(len(v))
        s += "\r\n"
        s += v
        s += "\r\n"
    }

    return s
}

// Pre-read the first byte to determine whether multiline or single-line returns are processed separately
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
        }
    }
}

// Multiple rows return reading one row at a time and calling singleResponse to get a single row of data
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
}

// Get Single Line Data+ -: Logically Same $Processed Separately
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
}

Then see how to call

package main

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

var host string
var port string

func init() {
    // Parameter acquisition settings have default values
    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))
    }

}

summary

In the code above, we see different logical parsing depending on the type of reply.
In fact, the essence of all redis processing frameworks is to encapsulate the code above to make it easier for us to use.Of course, there are other features that use Lua scripting, publishing subscriptions, and so on.
I think to understand the redis library, first understand the Protocol, then look at the source code or you will see a lot of logic and encapsulation that you don't understand.So we first studied the Protocol protocol and implemented it by ourselves.

Topics: Go Redis network