所謂 協(xié)議?,本質(zhì)是一種約定,需要使用者雙方來準(zhǔn)守,常見于 C/S 通信模式?中,比如在瀏覽器中最常用的 HTTP 應(yīng)用層通信協(xié)議。


(資料圖)

通信兩端需要某種約定,才能保持正常通信。一端通過約定的格式發(fā)送數(shù)據(jù),另一端通過約定的格式解析數(shù)據(jù),這種約定,取了一個好聽的名字 ---- 協(xié)議。

典型的 HTTP 協(xié)議,最本質(zhì)的原理也是如此。redis 作為一款高性能內(nèi)存組件,要盡可能將精力花在數(shù)據(jù)的組織形式上,因此,沒有采用開源的一些復(fù)雜協(xié)議,比如 HTTP,而是簡單的自定義一套應(yīng)用層通信協(xié)議。

Redis 客戶端 - 服務(wù)端通信協(xié)議稱之為 RESP 協(xié)議,全稱叫 Redis Serialization Protocol,即 redis 序列化協(xié)議。人類易讀,相當(dāng)精巧!

RESP 協(xié)議特點人類易讀簡單實現(xiàn)快速解析

RESP 是一種二進(jìn)制安全協(xié)議,因為編碼后的每一個字符串都有前綴來表明其長度,通過長度就能知道數(shù)據(jù)邊界,從而避免越界訪問的問題。

值得注意的是,RESP 協(xié)議只用于 客戶端 - 服務(wù)端? 之間的交流,redis cluster? 各節(jié)點之間采用不同的二進(jìn)制協(xié)議(采用 Gossip 協(xié)議)進(jìn)行交流。

網(wǎng)絡(luò)通信

我們知道,在傳統(tǒng)計算機(jī)網(wǎng)絡(luò)模型中,傳輸層?(TCP / UDP)的上一層便是應(yīng)用層。應(yīng)用層協(xié)議一般專注于數(shù)據(jù)的編解碼等約定,比如經(jīng)典的 HTTP 協(xié)議。

RESP 協(xié)議本質(zhì)和 HTTP 是一個級別,都屬于應(yīng)用層協(xié)議。

在 redis 中,傳輸層協(xié)議使用的是 TCP,服務(wù)端從 TCP socket 緩沖區(qū)中讀取數(shù)據(jù),然后經(jīng)過 RESP 協(xié)議解碼得到我們的指令。

而寫入數(shù)據(jù)則是相反,服務(wù)器先將響應(yīng)數(shù)據(jù)使用 RESP 編碼,然后將編碼后的數(shù)據(jù)寫入 TCP Socket 緩沖區(qū)發(fā)送給客戶端。

協(xié)議格式

在 RESP 協(xié)議中,第一個字節(jié)決定了具體數(shù)據(jù)類型:

簡單字符串?:Simple Strings,第一個字節(jié)響應(yīng)+錯誤?:Errors,第一個字節(jié)響應(yīng)-整型?:Integers,第一個字節(jié)響應(yīng):批量字符串?:Bulk Strings,第一個字節(jié)響應(yīng)$數(shù)組?:Arrays,第一個字節(jié)響應(yīng)*

我們來看看一具體的例子,我們一條正常指令 PSETEX test_redisson_batch_key8 120000 test_redisson_batch_key=>value:8,經(jīng) RESP 協(xié)議編碼后長這樣:

*4$6PSETEX$24test_redisson_batch_key8$6120000$32test_redisson_batch_key=>value:8

值得注意的是,在 RESP 協(xié)議中的每一部分都是以 \R\N 結(jié)尾。

簡單字符串:

Simple Strings?。以 + 為前綴的響應(yīng)數(shù)據(jù),例如:

"+OK\r\n"

以上是字符串 OK,被編碼后的格式,總共 5 字節(jié)。

這是一種非二進(jìn)制安全的編碼方式,因為, 我們無法確切的知道字符串的長度,只能以 \r\n? 來判斷,所以編碼的字符串中,不能包含 \r? 或者 \n 字符。

當(dāng)然,如果你想要二進(jìn)制安全字符串,可以選擇 Bulk Strings 方式,我們后面會介紹。

錯誤

Errors?。RESP 提供了錯誤類型,和簡單字符串非常類似,不過是以 - 開頭,基本格式如下:

"-Error message\r\n"

與簡單字符串真正不同的之處在于客戶端的處理上,對 - 開頭的響應(yīng),客戶端直接以異常情況處理。

我們來看一個是實際例子,當(dāng)我們的指令或者參數(shù)錯誤,redis 服務(wù)端會直接返回異常,如下:

-ERR unknown command "helloworld"-WRONGTYPE Operation against a key holding the wrong kind of value

- 后面的第一個單詞,直到第一個空格或換行符,表示返回的錯誤類型。這只是 Redis 使用的一個慣例,并不是 RESP 錯誤格式的一部分。

ERR? 是通用錯誤,而 WRONGTYPE 是一個更具體的錯誤,表示客戶端嘗試執(zhí)行錯誤的數(shù)據(jù)類型,通常作為一個錯誤的前綴,它允許客戶端在不檢查確切錯誤消息的情況下理解服務(wù)器返回的錯誤類型。

我們在客戶端實現(xiàn)的時候,可以針對不同的錯誤返回不同類型的異常,或者提供一種捕獲錯誤的通用方法,比如,直接將錯誤名稱作為字符串提供給調(diào)用者。

然而,這樣的特性不應(yīng)該被認(rèn)為是至關(guān)重要的,因為它很少有用,而且有限的客戶端實現(xiàn)可能只是返回一個通用的錯誤條件,比如 false。

整型

RESP Integers?。表示響應(yīng)的是整數(shù),以 :? 開頭,比如 :0\r\n? 和 :1000\r\n。

redis 中很多命令的響應(yīng)都是整數(shù),比如 ==INCR==, ==LLEN==, 及 ==LASTSAVE==。另外,響應(yīng)值是一個 64 位的整數(shù)。

當(dāng)然,整形也可以表示 true? 或者 false? 語義,比如 EXISTS? 或者 SISMEMBER 返回 1 表示 true,0 表示 false。

還有其他命令,比如 SADD?, SREM?, 和 SETNX 返回 1 表示實際執(zhí)行,反之為 0。

以下命令會響應(yīng)結(jié)果為整數(shù):

SETNX, DEL, EXISTS, INCR, INCRBY, DECR, DECRBY, DBSIZE, LASTSAVE, RENAMENX, MOVE, LLEN, SADD, SREM, SISMEMBER, SCARD.批量字符串

RESP Bulk Strings。批量回復(fù),是一個大小在 512 Mb 的二進(jìn)制安全字符串,被編碼成:

以$? 開頭,緊跟一個整數(shù)代表回復(fù)字符串的大小,以\r\n 結(jié)束隨后是 實際的字符串?dāng)?shù)據(jù)最后以 "\r\n" 結(jié)尾

比如 hello 被編碼為:

"$5\r\nhello\r\n"

一個空字符串被編碼為:

"$0\r\n\r\n"

另外,對于一些不存在的 value 可以返回 -1 表示 null,也被稱為 NULL 批量回復(fù)。

客戶端庫進(jìn)行實現(xiàn)時,可以將此 -1 處理成空對象,比如 Ruby 將返回 nil?,而 C 則返回 NULL

數(shù)組

RESP Arrays?。數(shù)組,對于響應(yīng)的集合元素,比如 LRANGE 命令,返回的是元素列表,也就是數(shù)組形式。

編碼格式:

以*? 開頭表示,緊接著是一個整數(shù),表示數(shù)組元素個數(shù),并以\r\n 結(jié)尾。數(shù)組的每個元素的都是 RESP 提供的類型。

例如,空數(shù)組:

"*0\r\n"

包含 "hello" 和 "world" 的響應(yīng)數(shù)組(也叫多批量字符串,每一個元素是批量字符串):

"*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n"

3個整數(shù)的數(shù)組是這樣的:

"*3\r\n:1\r\n:2\r\n:3\r\n"

另外,數(shù)組也可以混合類型的。

比如以下5個元素中,有4個是整形,一個是 批量字符串:

*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$5\r\nhello\r\n

... ( ? 以上結(jié)果為了更加清晰的展示,進(jìn)行了手動換行。

當(dāng)然,也同樣支持空數(shù)組(一般情況下,更習(xí)慣使用 Null Bulk String,但由于歷史原因,兩種方式都存在)

例如,當(dāng)使用 BLPOP 命令 timeout 時,將返回空數(shù)組:

"*-1\r\n"

當(dāng) redis 返回 NULL 數(shù)組時,客戶端實現(xiàn)庫最好也返回一個空對象,有助于區(qū)別到底是 empty 數(shù)組還是產(chǎn)生了其他問題

內(nèi)置數(shù)組,如下:

*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Hello\r\n-World\r\n

... ( ?? 同樣,為了展示更加清晰,進(jìn)行了手動換行

該響應(yīng)結(jié)果表示,外層數(shù)組包含兩個元素,每個元素都是數(shù)組。第一個子數(shù)組包含 3 個整型數(shù)字,第二個子數(shù)組包含 1 個簡單字符串和一個錯誤。

數(shù)組中的空元素

Null elements in Arrays?。數(shù)組出現(xiàn) NULL? 元素,這種場景也是很常見的,比如我們使用 MGET 批量獲取 key,當(dāng)其中一些 key 不存在時,返回的就是 NULL 元素。

例如響應(yīng)結(jié)果:

*3\r\n$5\r\nhello\r\n$-1\r\n$5\r\nworld\r\n

如上響應(yīng)編碼,客戶端庫解析之后應(yīng)該是這樣:

["hello",nil,"world"]多命令和管道

Multiple commands and pipelining。多命令和管道,redis 中提供了一次發(fā)送多條指令的操作,比如 ==MGET==、==MSET==、==pipline==,服務(wù)端接收并處理后一次性響應(yīng)。

這種形式就是上面提到的 數(shù)組?,數(shù)組里面可以是 批量字符串、整數(shù)?,甚至是 NULL 都可以。

我們先使用 telnet 看看原生響應(yīng)結(jié)果:

[root@VM-20-17-centos ~]# telnet 127.0.0.1 6379MGET key1 key2 key3*3$6value1$6value2$-1

我們再使用 redis-cli 看看被客戶端解碼后的結(jié)果:

127.0.0.1:6379> MGET key1 key2 key31) "value1"2) "value2"3) (nil)內(nèi)聯(lián)命令

Inline commands?。是這樣的,一般情況下我們和 redis 服務(wù)端通信都需要一個客戶端(比如redis-cli),因為雙方都遵循 RESP 協(xié)議,數(shù)據(jù)可以正常編碼和解析。

考慮這樣一種情況,當(dāng)你沒有任何客戶端工具可用時,是否也能正常和服務(wù)端通信呢?比如 telnet。

也是可以的?,redis 正式通過 內(nèi)聯(lián)指令 支持的,咱們來看看例子:

例1,通過 RESP 協(xié)議發(fā)送指令(由于沒有客戶端,這里我們手動編碼):

[root@VM-20-17-centos ~]# telnet 127.0.0.1 6379Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is "^]".*3 $3set$4 key1$5 world+OK

我們正常的指令是 set key1 word?,經(jīng)過 RESP 編碼之后 *3\r\n$3\r\nset\r\n$4\r\nkey1\r\r$5\r\nworld,redis 服務(wù)端解碼之后便可得到正常指令。

例2,通過內(nèi)聯(lián)操作發(fā)送指令:

[root@VM-20-17-centos ~]# telnet 127.0.0.1 6379Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is "^]".exists key1:1get key1$11set key1 hello +OKget key1$5hello

這里我們直接發(fā)送 內(nèi)聯(lián)指令? 比如 EXISTS key1、GET key1、SET key1 hello 等,無需 RESP 協(xié)議編碼,服務(wù)端仍可正常處理。

值得注意的是,因為沒有了統(tǒng)一請求協(xié)議中的 *? 項來聲明參數(shù)的數(shù)量,所以在 telnet 會話輸入命令的時候,必須使用空格來分割各個參數(shù),服務(wù)器在接收到數(shù)據(jù)之后,會按空格對用戶的輸入進(jìn)行解析,并獲取其中的命令參數(shù)。

高性能 Redis 協(xié)議解析器

High performance parser for the Redis protocol,即,高性能 Redis 協(xié)議分析器。

RESP 是一款人類易讀、簡單實現(xiàn)的通信協(xié)議,它可以類似于二進(jìn)制協(xié)議的性能實現(xiàn)。

RESP 使用前綴長度來傳輸批量數(shù)據(jù),因此不需要像 JSON 那樣,為了查找某個特殊字符而掃描整個數(shù)據(jù),也無須對發(fā)送至服務(wù)器的數(shù)據(jù)進(jìn)行轉(zhuǎn)義。

程序可以在對協(xié)議文本中的各個字符進(jìn)行處理的同時, 查找 CR 字符, 并計算出批量回復(fù)或多條批量回復(fù)的長度, 就像這樣:

#include int main(void) { unsigned char *p = "$123\r\n"; int len = 0; p++; while(*p != "\r") { len = (len*10)+(*p - "0"); p++; } /* Now p points at "\r", and the len is in bulk_len. */ printf("%d\n", len); return 0;}

得到了批量回復(fù)或多條批量回復(fù)的長度之后, 程序只需調(diào)用一次 read 函數(shù), 就可以將回復(fù)的正文數(shù)據(jù)全部讀入到內(nèi)存中, 而無須對這些數(shù)據(jù)做任何的處理。

在回復(fù)最末尾的 CR 和 LF 不作處理,丟棄它們。

Redis 協(xié)議的實現(xiàn)性能可以和二進(jìn)制協(xié)議的實現(xiàn)性能相媲美,并且由于 Redis 協(xié)議的簡單性,大部分高級語言都可以輕易地實現(xiàn)這個協(xié)議,這使得客戶端軟件的 bug 數(shù)量大大減少。

總結(jié)

協(xié)議,本質(zhì)是雙方對數(shù)據(jù)處理的一種約定,redis 提供了簡單易實現(xiàn)的 RESP 協(xié)議,你也看到了,確實相當(dāng)簡單,按照這種協(xié)議約定,你也能很快寫出一個 redis 客戶端。

協(xié)議工作的一般流程是:

客戶端:原始命令 -> RESP 編碼服務(wù)端:RESP 解碼 -> 原始命令

redis 服務(wù)端除了支持 RESP? 協(xié)議,還支持內(nèi)聯(lián)指令,也就是我們原始的命令,這樣一來就不需要編碼解碼的過程了。

標(biāo)簽: RESP 網(wǎng)絡(luò)通信 網(wǎng)絡(luò)協(xié)議