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.