前言
Redis客戶端使用被稱為RESP(Redis序列化協(xié)議)的協(xié)議與Redis服務(wù)器進(jìn)行通訊。雖然該協(xié)議是專門為Redis設(shè)計(jì)的,但它同樣可以被用于其他客戶端/服務(wù)器的軟件項(xiàng)目。
RESP是以下幾點(diǎn)的折中方案:
- 實(shí)現(xiàn)起來(lái)簡(jiǎn)單
- 解析速度快
- 可讀的
RESP可以序列化諸如整型、字符串和數(shù)組等不同的數(shù)據(jù)類型,還有一個(gè)特定的錯(cuò)誤類型。請(qǐng)求以字符串?dāng)?shù)組的形式由客戶端發(fā)送到Redis服務(wù)器,字符串?dāng)?shù)組表示需要執(zhí)行的命令。Redis用特定于命令的數(shù)據(jù)類型回復(fù)。
RESP是二進(jìn)制安全的,不需要處理從一個(gè)進(jìn)程傳輸?shù)搅硪粋€(gè)進(jìn)程的批量數(shù)據(jù),因?yàn)樗褂瞄L(zhǎng)度前綴來(lái)傳輸批量數(shù)據(jù)。
注意: 這里描述的協(xié)議僅用于客戶端/服務(wù)器通信,Redis集群使用不同的二進(jìn)制協(xié)議在節(jié)點(diǎn)之間交換信息。
網(wǎng)絡(luò)層
客戶端通過創(chuàng)建端口號(hào)為6379的TCP來(lái)連接Redis服務(wù)器。
雖然RESP在技術(shù)上是非TCP特定的,但該協(xié)議僅用于Redis上下文的(或者等效的面向流的連接,如Unix套接字)TCP連接。
請(qǐng)求-應(yīng)答模型
Redis接收由不同參數(shù)組成的命令。一旦命令被接收,將會(huì)被執(zhí)行并且發(fā)送一個(gè)回復(fù)給客戶端。
這可能是最簡(jiǎn)單的模型,然而,有兩個(gè)例外:
- Redis支持管道操作,所以客戶端可能一次發(fā)送多個(gè)命令并且等待回復(fù)
- 當(dāng)Redis客戶端訂閱了一個(gè)Pub/Sub頻道,協(xié)議語(yǔ)義改變?yōu)橐环N推送協(xié)議??蛻舳瞬辉傩枰l(fā)送命令因?yàn)橹灰?wù)器收到新的消息,將會(huì)自動(dòng)發(fā)送新的消息到客戶端(對(duì)于客戶端訂閱的頻道)。
除了這兩種例外,Redis協(xié)議是一種簡(jiǎn)單的請(qǐng)求-應(yīng)答協(xié)議。
RESP協(xié)議描述
RedisRESP協(xié)議在v1.2版本中介紹,但是到v2.0才變?yōu)榕c服務(wù)器通信的標(biāo)準(zhǔn)。
RESP協(xié)議支持以下數(shù)據(jù)類型: Simple Strings(簡(jiǎn)單字符串),Errors(錯(cuò)誤),Integers(整型),Bulk Strings(批量字符串)以及Arrays(數(shù)組)。
Redis通過以下方式將RESP用作請(qǐng)求-應(yīng)答協(xié)議:
- 客戶端以Bulk String(批量字符串)組成的RESP數(shù)組發(fā)送命令到服務(wù)器。
- 服務(wù)器根據(jù)命令以RESP數(shù)據(jù)類型之一回復(fù)客戶端
在RESP中,第一個(gè)字節(jié)決定了數(shù)據(jù)類型:
-
+表示Simple Strings(簡(jiǎn)單字符串) -
-表示Errors(錯(cuò)誤) -
:表示Integers(整型) -
$表示Bulk Strings(批量字符串) -
*表示Arrays(數(shù)組)
在RESP中,協(xié)議不同部分總是以\r\n(CRLF)結(jié)尾。
RESP使用特殊的組合表示空的Bulk Strings或者空的Arrays:$-1\r\n表示空的Bulk Strings,*-1\r\n表示空的Arrays,需要注意的是:$0\r\n與*0\r\n分別表示有回復(fù),但長(zhǎng)度為0。
RESP Simple Strings(簡(jiǎn)單字符串)
Simple Strings(簡(jiǎn)單字符串)的編碼方式為:一個(gè)+號(hào)在最前面,后面跟著一個(gè)不能包含CR或者LF字符的字符串(即不允許換行符),并且最后以CRLF(\r\n)結(jié)尾。
Simple Strings(簡(jiǎn)單字符串)以最小的開銷傳輸非二進(jìn)制安全的字符串。例如:很多Redis命令執(zhí)行成功后的回復(fù)只是OK,RESP簡(jiǎn)單字符串將以5個(gè)字節(jié)編碼:+OK\r\n
如果想要傳輸二進(jìn)制安全的字符串,請(qǐng)使用Bulk Strings替代。
當(dāng)Redis以簡(jiǎn)單字符串回復(fù)時(shí),客戶端庫(kù)應(yīng)該返回+號(hào)后面第一個(gè)字符后面的所有字符串(不包括CRLF字節(jié))。
RESP Errors(錯(cuò)誤)
Redis有特定的錯(cuò)誤類型,與Simple Strings相似,不同的是第一個(gè)字符是減號(hào)-而不是加號(hào)+,二者真正不同的是,客戶端將錯(cuò)誤視為異常,而構(gòu)成Error類型的字符串就是錯(cuò)誤消息本身。
錯(cuò)誤類型的基本格式為:
-Error message\r\n
只有當(dāng)發(fā)生錯(cuò)誤時(shí)才會(huì)回復(fù)錯(cuò)誤,比如你想要在錯(cuò)誤的數(shù)據(jù)類型上執(zhí)行命令,或者命令根本不存在??蛻舳耸盏紼rror回復(fù)時(shí)應(yīng)該拋出異常。
下面是錯(cuò)誤回復(fù)的例子:
-ERR unknown command 'helloworld'
-WRONGTYPE Operation against a key holding the wrong kind of value
-號(hào)到后面第一個(gè)空格或者新行的第一個(gè)單詞表示返回的錯(cuò)誤類型,這只是Redis使用的約定,而不是RESP錯(cuò)誤格式的一部分。
比如,ERR是一般錯(cuò)誤,但是WRONGTYPE是一個(gè)更具體的錯(cuò)誤,暗示客戶端嘗試執(zhí)行應(yīng)對(duì)錯(cuò)誤類型的操作。這被稱為錯(cuò)誤前綴,是一種允許客戶端了解服務(wù)器返回的錯(cuò)誤類型而無(wú)需檢查確切錯(cuò)誤消息的方法。
客戶端實(shí)現(xiàn)可能會(huì)針對(duì)不同的錯(cuò)誤返回不同類型的異常,或者通過直接將錯(cuò)誤名稱作為字符串提供給調(diào)用者來(lái)提供捕獲錯(cuò)誤的通用方法。
但是不應(yīng)將此類功能視為至關(guān)重要,因?yàn)樗苌儆杏茫⑶矣邢薜目蛻舳藢?shí)現(xiàn)可能會(huì)簡(jiǎn)單地返回通用錯(cuò)誤條件,例如false
RESP Integers(整型)
這種類型只是一個(gè)以CRLF結(jié)尾的字符串,表示一個(gè)整數(shù),前綴為:,比如::0\r\n和:1000\r\n。
有很多返回整型的Redis命令,比如: INCR、LLEN以及LASTSAVE。返回的整型數(shù)據(jù)范圍為有符號(hào)的64位整數(shù)。
整型回復(fù)同樣可以用來(lái)表示true或者false,比如EXISTS或者SISMEMBER將會(huì)返回1表示true,0表示false。
其他命令比如SADD、SREM、SETNX如果被執(zhí)行了將會(huì)返回1,否則返回0。
其他返回整型的命令:SETNX、DEL、 EXISTS、INCR、INCRBY、DECR、DECRBY、DBSIZE、LASTSAVE、RENAMENX、MOVE、LLEN、SADD、SREM、SISMEMBER、SCARD。
RESP Bulk Strings(批量字符串)
Bulk Strings被用來(lái)表示單個(gè)的最大長(zhǎng)度512MB的二進(jìn)制安全字符串。
Bulk Strings編碼方式為:
-
$字符開頭,后面跟著字符串值的字節(jié)長(zhǎng)度(長(zhǎng)度前綴),以CRLF結(jié)尾。 - 實(shí)際的字符串?dāng)?shù)據(jù)。
- 最終的CRLF。
所以,字符串hello被編碼為:$5\r\nhello\r\n
一個(gè)空字符串被編碼為:$0\r\n\r\n
RESP Bulk Strings也可用特殊格式表示不存在(NULL),在這種格式中,長(zhǎng)度為-1,沒有數(shù)據(jù):$-1\r\n,這被稱作NULL Bulk String,當(dāng)服務(wù)器回復(fù)NULL Bulk String時(shí),客戶端庫(kù)的API不應(yīng)該返回空的字符串,而是返回nil對(duì)象。
RESP Arrays(數(shù)組)
客戶端使用RESP Arrays發(fā)送命令到服務(wù)器。同樣,某些返回元素集合給客戶端的命令使用RESP數(shù)組作為回復(fù),比如:LRANGE命令。RESP Arrays以下面的格式發(fā)送:
-
*開頭,后面跟著數(shù)組元素的數(shù)量,數(shù)量以十進(jìn)制表示,然后跟著CRLF。 - Array每個(gè)元素附件的RESP類型。
所以,空數(shù)組編碼為:*0\r\n
包含"hello"和"world"兩個(gè)元素的RESP數(shù)組被編碼為:*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
如你所見,*<count>CRLF前綴后面,組成數(shù)組的其他數(shù)據(jù)類型只是一個(gè)接一個(gè)的連接起來(lái),比如一個(gè)由3個(gè)整型構(gòu)成的Array編碼結(jié)果為:*3\r\n:1\r\n:2\r\n:3\r\n
Array可以包含不同的數(shù)據(jù)類型,比如一個(gè)有4個(gè)整型和一個(gè)批量字符串組成的Array編碼為:(為了直觀,以換行的形式展現(xiàn))
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
hello\r\n
第一行*5\r\n為了表示后面還有5個(gè)回復(fù),然后再讀取后面的5個(gè)數(shù)組元素。
值為NULL的數(shù)組也存在(通常使用NULL Bulk String,由于歷史原因,NULL存在兩種格式)。比如BLPOP超時(shí)時(shí)將會(huì)返回一個(gè)長(zhǎng)度為-1的NULL Array:*-1\r\n
在RESP中同樣存在嵌套的數(shù)組,比如兩個(gè)嵌套的數(shù)組編碼結(jié)果為:
*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Hello\r\n
-World\r\n
上面的編碼結(jié)果包含兩個(gè)元素的數(shù)組,第一個(gè)元素由(1,2,3)構(gòu)成的子數(shù)組,第二個(gè)元素由一個(gè)Bulk String(+Hello)和一個(gè)Error(-World)組成的數(shù)組。
Array中的Null元素
一個(gè)Array的單個(gè)元素可能為NULL。這在Redis回復(fù)中用來(lái)表示這些元素丟失而不是空字符串。當(dāng)SORT命令使用GET pattern子命令并且key缺失時(shí),將會(huì)發(fā)生這種情況。一個(gè)包含NULL元素的數(shù)組回復(fù)為:
*3\r\n
$5\r\n
hello\r\n
$-1\r\n
$5\r\n
world\r\n
上面的編碼解析結(jié)果為:["hello", nil, "world"]
發(fā)送命令到Redis服務(wù)器
可以根據(jù)上面幾部分的介紹來(lái)編寫Redis客戶端,同時(shí)進(jìn)一步了解客戶端和服務(wù)器之間的交互是如何工作的。
- 客戶端發(fā)送只由Bulk Strings組成的RESP Array到Redis服務(wù)器。
- Redis以各種有效的RESP數(shù)據(jù)類型回復(fù)客戶端
所以,一種典型的交互場(chǎng)景可能如下:
為了獲取存儲(chǔ)在mylist中的列表的長(zhǎng)度,客戶端發(fā)送命令LLEN mylist到服務(wù)器,然后服務(wù)器回復(fù)客戶端一個(gè)整型回復(fù):
Client: *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n
Sserver: :48293\r\n
用Golang實(shí)現(xiàn)命令編碼與回復(fù)解析
import (
"bufio"
"bytes"
"errors"
"fmt"
"net"
"strconv"
)
// Reply load parsed reply from redis server
type Reply struct {
array []*Reply // nested array
value []byte // SimpleString & Integer & BulkString
err error // Error
}
type Client struct {
c net.Conn // tcp connection
writer *bufio.Writer
reader *bufio.Reader
}
func (c *Client) Send(cmd string, args ...interface{}) error {
const crlf = "\r\n"
var buf bytes.Buffer
buf.WriteByte('*') // Array標(biāo)志
buf.WriteString(strconv.FormatInt(int64(1+len(args)), 10)) // 寫入數(shù)組長(zhǎng)度
buf.WriteString(crlf) // 寫入分隔符
buf.WriteByte('$') // 寫入命令部分
buf.WriteString(strconv.FormatInt(int64(len(cmd)), 10)) // 寫入命令長(zhǎng)度
buf.WriteString(crlf) // 寫入分隔符
buf.WriteString(cmd) // 寫入命令
buf.WriteString(crlf) // 寫入分隔符
// 寫入各個(gè)參數(shù)
for _, arg := range args {
a := fmt.Sprint(arg)
buf.WriteByte('$')
buf.WriteString(strconv.FormatInt(int64(len(a)), 10))
buf.WriteString(crlf)
buf.WriteString(a)
buf.WriteString(crlf)
}
if _, err := c.writer.Write(buf.Bytes()); err != nil {
return err
}
return c.writer.Flush()
}
func (c *Client) Response() (interface{}, error) {
line, err := c.ReadLine()
if err != nil {
return nil, err
}
if c.IsNilReply(line) {
return nil, nil
}
switch line[0] {
case '+', ':':
return &Reply{value: line[1:]}, nil
case '-':
return &Reply{err: errors.New(string(line[1:]))}, nil
case '$':
bulk, err := c.ReadBulkString(line)
if err != nil {
return nil, err
}
return string(bulk), nil
case '*':
return c.ReadArray(line)
default:
return nil, fmt.Errorf("invalid redis reply type")
}
}
func (c *Client) ReadLine() ([]byte, error) {
line, err := c.reader.ReadSlice('\n')
if err != nil {
if err != bufio.ErrBufferFull {
return nil, err
}
full := make([]byte, len(line))
copy(full, line)
line, err = c.reader.ReadBytes('\n')
if err != nil {
return nil, err
}
full = append(full, line...)
line = full
}
if len(line) <= 2 || line[len(line)-2] != '\r' || line[len(line)-1] != '\n' {
return nil, fmt.Errorf("read invalid reply: %q", line)
}
return line[:len(line)-2], nil // 去掉結(jié)尾的'\r\n'
}
func (c *Client) DataLen(data []byte) (int, error) {
return strconv.Atoi(string(data))
}
func (c *Client) ReadBulkString(head []byte) ([]byte, error) {
length, err := c.DataLen(head)
if err != nil {
return nil, err
}
buf := make([]byte, length+2)
if _, err = c.reader.Read(buf); err != nil {
return nil, err
}
return buf[:length], nil
}
func (c *Client) ReadArray(head []byte) (interface{}, error) {
length, err := c.DataLen(head)
if err != nil {
return nil, err
}
// 處理空數(shù)組
if length <= 0 {
return &Reply{}, nil
}
var array = make([]interface{}, length)
for i := 0; i < length; i++ {
array[i], err = c.Response()
if err != nil {
return nil, err
}
}
return array, nil
}
func (c *Client) IsNilReply(b []byte) bool {
if len(b) == 3 && (b[0] == '$' || b[0] == '*') && b[1] == '-' && b[2] == '1' {
return true
}
return false
}