前言
Redis客戶端使用被稱為RESP(Redis序列化協(xié)議)
的協(xié)議與Redis服務(wù)器進行通訊凰棉。雖然該協(xié)議是專門為Redis設(shè)計的讳侨,但它同樣可以被用于其他客戶端/服務(wù)器的軟件項目。
RESP
是以下幾點的折中方案:
- 實現(xiàn)起來簡單
- 解析速度快
- 可讀的
RESP
可以序列化諸如整型、字符串和數(shù)組等不同的數(shù)據(jù)類型伶丐,還有一個特定的錯誤類型悼做。請求以字符串?dāng)?shù)組的形式由客戶端發(fā)送到Redis服務(wù)器,字符串?dāng)?shù)組表示需要執(zhí)行的命令哗魂。Redis用特定于命令的數(shù)據(jù)類型回復(fù)肛走。
RESP
是二進制安全的,不需要處理從一個進程傳輸?shù)搅硪粋€進程的批量數(shù)據(jù)录别,因為它使用長度前綴來傳輸批量數(shù)據(jù)朽色。
注意: 這里描述的協(xié)議僅用于客戶端/服務(wù)器通信,Redis集群使用不同的二進制協(xié)議在節(jié)點之間交換信息组题。
網(wǎng)絡(luò)層
客戶端通過創(chuàng)建端口號為6379的TCP來連接Redis服務(wù)器葫男。
雖然RESP
在技術(shù)上是非TCP特定的,但該協(xié)議僅用于Redis上下文的(或者等效的面向流的連接崔列,如Unix套接字)TCP連接梢褐。
請求-應(yīng)答模型
Redis接收由不同參數(shù)組成的命令。一旦命令被接收赵讯,將會被執(zhí)行并且發(fā)送一個回復(fù)給客戶端盈咳。
這可能是最簡單的模型,然而瘦癌,有兩個例外:
- Redis支持管道操作猪贪,所以客戶端可能一次發(fā)送多個命令并且等待回復(fù)
- 當(dāng)Redis客戶端訂閱了一個Pub/Sub頻道跷敬,協(xié)議語義改變?yōu)橐环N推送協(xié)議讯私。客戶端不再需要發(fā)送命令因為只要服務(wù)器收到新的消息西傀,將會自動發(fā)送新的消息到客戶端(對于客戶端訂閱的頻道)斤寇。
除了這兩種例外,Redis協(xié)議是一種簡單的請求-應(yīng)答協(xié)議拥褂。
RESP協(xié)議描述
RedisRESP
協(xié)議在v1.2版本中介紹娘锁,但是到v2.0才變?yōu)榕c服務(wù)器通信的標準。
RESP
協(xié)議支持以下數(shù)據(jù)類型: Simple Strings(簡單字符串)饺鹃,Errors(錯誤)莫秆,Integers(整型),Bulk Strings(批量字符串)以及Arrays(數(shù)組)悔详。
Redis通過以下方式將RESP
用作請求-應(yīng)答協(xié)議:
- 客戶端以Bulk String(批量字符串)組成的RESP數(shù)組發(fā)送命令到服務(wù)器镊屎。
- 服務(wù)器根據(jù)命令以RESP數(shù)據(jù)類型之一回復(fù)客戶端
在RESP
中,第一個字節(jié)決定了數(shù)據(jù)類型:
-
+
表示Simple Strings(簡單字符串) -
-
表示Errors(錯誤) -
:
表示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ù)用狱,但長度為0运怖。
RESP Simple Strings(簡單字符串)
Simple Strings(簡單字符串)的編碼方式為:一個+
號在最前面,后面跟著一個不能包含CR或者LF字符的字符串(即不允許換行符)夏伊,并且最后以CRLF(\r\n
)結(jié)尾摇展。
Simple Strings(簡單字符串)以最小的開銷傳輸非二進制安全的字符串。例如:很多Redis命令執(zhí)行成功后的回復(fù)只是OK
署海,RESP
簡單字符串將以5個字節(jié)編碼:+OK\r\n
如果想要傳輸二進制安全的字符串吗购,請使用Bulk Strings替代。
當(dāng)Redis以簡單字符串回復(fù)時砸狞,客戶端庫應(yīng)該返回+
號后面第一個字符后面的所有字符串(不包括CRLF字節(jié))捻勉。
RESP Errors(錯誤)
Redis有特定的錯誤類型,與Simple Strings相似刀森,不同的是第一個字符是減號-
而不是加號+
踱启,二者真正不同的是,客戶端將錯誤視為異常研底,而構(gòu)成Error類型的字符串就是錯誤消息本身埠偿。
錯誤類型的基本格式為:
-Error message\r\n
只有當(dāng)發(fā)生錯誤時才會回復(fù)錯誤,比如你想要在錯誤的數(shù)據(jù)類型上執(zhí)行命令榜晦,或者命令根本不存在冠蒋。客戶端收到Error回復(fù)時應(yīng)該拋出異常乾胶。
下面是錯誤回復(fù)的例子:
-ERR unknown command 'helloworld'
-WRONGTYPE Operation against a key holding the wrong kind of value
-
號到后面第一個空格或者新行的第一個單詞表示返回的錯誤類型抖剿,這只是Redis使用的約定,而不是RESP
錯誤格式的一部分识窿。
比如斩郎,ERR
是一般錯誤,但是WRONGTYPE
是一個更具體的錯誤喻频,暗示客戶端嘗試執(zhí)行應(yīng)對錯誤類型的操作缩宜。這被稱為錯誤前綴,是一種允許客戶端了解服務(wù)器返回的錯誤類型而無需檢查確切錯誤消息的方法甥温。
客戶端實現(xiàn)可能會針對不同的錯誤返回不同類型的異常锻煌,或者通過直接將錯誤名稱作為字符串提供給調(diào)用者來提供捕獲錯誤的通用方法。
但是不應(yīng)將此類功能視為至關(guān)重要姻蚓,因為它很少有用宋梧,并且有限的客戶端實現(xiàn)可能會簡單地返回通用錯誤條件,例如false
RESP Integers(整型)
這種類型只是一個以CRLF結(jié)尾的字符串史简,表示一個整數(shù)乃秀,前綴為:
肛著,比如::0\r\n
和:1000\r\n
。
有很多返回整型的Redis命令跺讯,比如: INCR
枢贿、LLEN
以及LASTSAVE
。返回的整型數(shù)據(jù)范圍為有符號的64位整數(shù)刀脏。
整型回復(fù)同樣可以用來表示true或者false局荚,比如EXISTS
或者SISMEMBER
將會返回1表示true,0表示false愈污。
其他命令比如SADD
耀态、SREM
、SETNX
如果被執(zhí)行了將會返回1暂雹,否則返回0首装。
其他返回整型的命令:SETNX
、DEL
杭跪、 EXISTS
仙逻、INCR
、INCRBY
涧尿、DECR
系奉、DECRBY
、DBSIZE
姑廉、LASTSAVE
缺亮、RENAMENX
、MOVE
桥言、LLEN
萌踱、SADD
、SREM
限书、SISMEMBER
虫蝶、SCARD
章咧。
RESP Bulk Strings(批量字符串)
Bulk Strings被用來表示單個的最大長度512MB的二進制安全字符串倦西。
Bulk Strings編碼方式為:
-
$
字符開頭,后面跟著字符串值的字節(jié)長度(長度前綴)赁严,以CRLF結(jié)尾扰柠。 - 實際的字符串?dāng)?shù)據(jù)。
- 最終的CRLF疼约。
所以卤档,字符串hello
被編碼為:$5\r\nhello\r\n
一個空字符串被編碼為:$0\r\n\r\n
RESP Bulk Strings也可用特殊格式表示不存在(NULL),在這種格式中程剥,長度為-1劝枣,沒有數(shù)據(jù):$-1\r\n
,這被稱作NULL Bulk String,當(dāng)服務(wù)器回復(fù)NULL Bulk String時舔腾,客戶端庫的API不應(yīng)該返回空的字符串溪胶,而是返回nil對象。
RESP Arrays(數(shù)組)
客戶端使用RESP Arrays發(fā)送命令到服務(wù)器稳诚。同樣哗脖,某些返回元素集合給客戶端的命令使用RESP數(shù)組作為回復(fù),比如:LRANGE
命令扳还。RESP Arrays以下面的格式發(fā)送:
-
*
開頭才避,后面跟著數(shù)組元素的數(shù)量,數(shù)量以十進制表示氨距,然后跟著CRLF桑逝。 - Array每個元素附件的RESP類型。
所以俏让,空數(shù)組編碼為:*0\r\n
包含"hello"和"world"兩個元素的RESP數(shù)組被編碼為:*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
如你所見肢娘,*<count>CRLF
前綴后面,組成數(shù)組的其他數(shù)據(jù)類型只是一個接一個的連接起來舆驶,比如一個由3個整型構(gòu)成的Array編碼結(jié)果為:*3\r\n:1\r\n:2\r\n:3\r\n
Array可以包含不同的數(shù)據(jù)類型橱健,比如一個有4個整型和一個批量字符串組成的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個回復(fù)沙廉,然后再讀取后面的5個數(shù)組元素拘荡。
值為NULL的數(shù)組也存在(通常使用NULL Bulk String,由于歷史原因撬陵,NULL存在兩種格式)珊皿。比如BLPOP
超時時將會返回一個長度為-1的NULL Array:*-1\r\n
在RESP中同樣存在嵌套的數(shù)組,比如兩個嵌套的數(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é)果包含兩個元素的數(shù)組巨税,第一個元素由(1蟋定,2,3)構(gòu)成的子數(shù)組草添,第二個元素由一個Bulk String(+Hello)和一個Error(-World)組成的數(shù)組驶兜。
Array中的Null元素
一個Array的單個元素可能為NULL。這在Redis回復(fù)中用來表示這些元素丟失而不是空字符串远寸。當(dāng)SORT
命令使用GET pattern
子命令并且key缺失時抄淑,將會發(fā)生這種情況。一個包含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ù)上面幾部分的介紹來編寫Redis客戶端驰后,同時進一步了解客戶端和服務(wù)器之間的交互是如何工作的肆资。
- 客戶端發(fā)送只由Bulk Strings組成的RESP Array到Redis服務(wù)器。
- Redis以各種有效的RESP數(shù)據(jù)類型回復(fù)客戶端
所以灶芝,一種典型的交互場景可能如下:
為了獲取存儲在mylist
中的列表的長度郑原,客戶端發(fā)送命令LLEN mylist
到服務(wù)器唉韭,然后服務(wù)器回復(fù)客戶端一個整型回復(fù):
Client: *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n
Sserver: :48293\r\n
用Golang實現(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標志
buf.WriteString(strconv.FormatInt(int64(1+len(args)), 10)) // 寫入數(shù)組長度
buf.WriteString(crlf) // 寫入分隔符
buf.WriteByte('$') // 寫入命令部分
buf.WriteString(strconv.FormatInt(int64(len(cmd)), 10)) // 寫入命令長度
buf.WriteString(crlf) // 寫入分隔符
buf.WriteString(cmd) // 寫入命令
buf.WriteString(crlf) // 寫入分隔符
// 寫入各個參數(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
}