本身動手寫Redis客戶端- Redis協議(1)

 

網絡層

客戶端和服務器經過 TCP 鏈接來進行數據交互, 服務器默認的端口號爲 6379 。redis

客戶端和服務器發送的命令或數據一概以 \r\n (CRLF)結尾。數組

請求

Redis 服務器接受命令以及命令的參數。安全

服務器會在接到命令以後,對命令進行處理,並將命令的回覆傳送回客戶端。服務器

新版統一請求協議

新版統一請求協議在 Redis 1.2 版本中引入, 並最終在 Redis 2.0 版本成爲 Redis 服務器通訊的標準方式。網絡

你的 Redis 客戶端應該按照這個新版協議來進行實現。函數

在這個協議中, 全部發送至 Redis 服務器的參數都是二進制安全(binary safe)的。性能

如下是這個協議的通常形式:對象

*<參數數量> CR LF
$<參數 1 的字節數量> CR LF
<參數 1 的數據> CR LF
...
$<參數 N 的字節數量> CR LF
<參數 N 的數據> CR LF

譯註:命令自己也做爲協議的其中一個參數來發送。blog

舉個例子, 如下是一個命令協議的打印版本:內存

*3
$3
SET
$5
mykey
$7
myvalue

這個命令的實際協議值以下:

"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"

稍後咱們會看到, 這種格式除了用做命令請求協議以外, 也用在命令的回覆協議中: 這種只有一個參數的回覆格式被稱爲批量回復(Bulk Reply)。

統一協議請求本來是用在回覆協議中, 用於將列表的多個項返回給客戶端的, 這種回覆格式被稱爲多條批量回復(Multi Bulk Reply)。

一個多條批量回復以 *<argc>\r\n 爲前綴, 後跟多條不一樣的批量回復, 其中 argc 爲這些批量回復的數量。

回覆

Redis 命令會返回多種不一樣類型的回覆。

經過檢查服務器發回數據的第一個字節, 能夠肯定這個回覆是什麼類型:

狀態回覆(status reply)的第一個字節是 "+"

錯誤回覆(error reply)的第一個字節是 "-"

整數回覆(integer reply)的第一個字節是 ":"

批量回復(bulk reply)的第一個字節是 "$"

多條批量回復(multi bulk reply)的第一個字節是 "*"

狀態回覆

一個狀態回覆(或者單行回覆,single line reply)是一段以 "+" 開始、 "\r\n" 結尾的單行字符串。

如下是一個狀態回覆的例子:

+OK

客戶端庫應該返回 "+" 號以後的全部內容。 好比在在上面的這個例子中, 客戶端就應該返回字符串 "OK" 。

狀態回覆一般由那些不須要返回數據的命令返回,這種回覆不是二進制安全的,它也不能包含新行。

狀態回覆的額外開銷很是少,只須要三個字節(開頭的 "+" 和結尾的 CRLF)。

錯誤回覆

錯誤回覆和狀態回覆很是類似, 它們之間的惟一區別是, 錯誤回覆的第一個字節是 "-" , 而狀態回覆的第一個字節是 "+" 。

錯誤回覆只在某些地方出現問題時發送: 好比說, 當用戶對不正確的數據類型執行命令, 或者執行一個不存在的命令, 等等。

一個客戶端庫應該在收到錯誤回覆時產生一個異常。

如下是兩個錯誤回覆的例子:

-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

在 "-" 以後,直到遇到第一個空格或新行爲止,這中間的內容表示所返回錯誤的類型。

ERR 是一個通用錯誤,而 WRONGTYPE 則是一個更特定的錯誤。 一個客戶端實現能夠爲不一樣類型的錯誤產生不一樣類型的異常, 或者提供一種通用的方式, 讓調用者能夠經過提供字符串形式的錯誤名來捕捉(trap)不一樣的錯誤。

不過這些特性用得並很少, 因此並非特別重要, 一個受限的(limited)客戶端能夠經過簡單地返回一個邏輯假(false)來表示一個通用的錯誤條件。

整數回覆

整數回覆就是一個以 ":" 開頭, CRLF 結尾的字符串表示的整數。

好比說, ":0\r\n" 和 ":1000\r\n" 都是整數回覆。

返回整數回覆的其中兩個命令是 INCRLASTSAVE 。 被返回的整數沒有什麼特殊的含義, INCR 返回鍵的一個自增後的整數值, 而 LASTSAVE 則返回一個 UNIX 時間戳, 返回值的惟一限制是這些數必須可以用 64 位有符號整數表示。

整數回覆也被普遍地用於表示邏輯真和邏輯假: 好比 EXISTSSISMEMBER 都用返回值 1 表示真, 0 表示假。

其餘一些命令, 好比 SADDSREMSETNX , 只在操做真正被執行了的時候, 才返回 1 , 不然返回 0 。

如下命令都返回整數回覆: SETNXDELEXISTSINCRINCRBYDECRDECRBYDBSIZELASTSAVERENAMENXMOVELLENSADDSREMSISMEMBERSCARD

批量回復

服務器使用批量回復來返回二進制安全的字符串,字符串的最大長度爲 512 MB 。

客戶端:GET mykey
服務器:foobar

服務器發送的內容中:

第一字節爲 "$" 符號

接下來跟着的是表示實際回覆長度的數字值

以後跟着一個 CRLF

再後面跟着的是實際回覆數據

最末尾是另外一個 CRLF

對於前面的 GET 命令,服務器實際發送的內容爲:

"$6\r\nfoobar\r\n"

若是被請求的值不存在, 那麼批量回復會將特殊值 -1 用做回覆的長度值, 就像這樣:

客戶端:GET non-existing-key
服務器:$-1

這種回覆稱爲空批量回復(NULL Bulk Reply)。

當請求對象不存在時,客戶端應該返回空對象,而不是空字符串: 好比 Ruby 庫應該返回 nil , 而 C 庫應該返回 NULL (或者在回覆對象中設置一個特殊標誌), 諸如此類。

多條批量回復

LRANGE 這樣的命令須要返回多個值, 這一目標能夠經過多條批量回復來完成。

多條批量回復是由多個回覆組成的數組, 數組中的每一個元素均可以是任意類型的回覆, 包括多條批量回復自己。

多條批量回復的第一個字節爲 "*" , 後跟一個字符串表示的整數值, 這個值記錄了多條批量回復所包含的回覆數量, 再後面是一個 CRLF 。

客戶端: LRANGE mylist 0 3
服務器: *4
服務器: $3
服務器: foo
服務器: $3
服務器: bar
服務器: $5
服務器: Hello
服務器: $5
服務器: World

在上面的示例中,服務器發送的全部字符串都由 CRLF 結尾。

正如你所見到的那樣, 多條批量回復所使用的格式, 和客戶端發送命令時使用的統一請求協議的格式如出一轍。 它們之間的惟一區別是:

統一請求協議只發送批量回復。

而服務器應答命令時所發送的多條批量回復,則能夠包含任意類型的回覆。

如下例子展現了一個多條批量回復, 回覆中包含四個整數值, 以及一個二進制安全字符串:

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

在回覆的第一行, 服務器發送 *5\r\n , 表示這個多條批量回復包含 5 條回覆, 再後面跟着的則是 5 條回覆的正文。

多條批量回復也能夠是空白的(empty), 就像這樣:

客戶端: LRANGE nokey 0 1
服務器: *0\r\n

無內容的多條批量回復(null multi bulk reply)也是存在的, 好比當 BLPOP 命令的阻塞時間超過最大時限時, 它就返回一個無內容的多條批量回復, 這個回覆的計數值爲 -1 :

客戶端: BLPOP key 1
服務器: *-1\r\n

客戶端庫應該區別對待空白多條回覆和無內容多條回覆: 當 Redis 返回一個無內容多條回覆時, 客戶端庫應該返回一個 null 對象, 而不是一個空數組。

多條批量回復中的空元素

多條批量回復中的元素能夠將自身的長度設置爲 -1 , 從而表示該元素不存在, 而且也不是一個空白字符串(empty string)。

SORT 命令使用 GET pattern 選項對一個不存在的鍵進行操做時, 就會發生多條批量回復中帶有空白元素的狀況。

如下例子展現了一個包含空元素的多重批量回復:

服務器: *3
服務器: $3
服務器: foo
服務器: $-1
服務器: $3
服務器: bar

其中, 回覆中的第二個元素爲空。

對於這個回覆, 客戶端庫應該返回相似於這樣的回覆:

["foo", nil, "bar"]

多命令和流水線

客戶端能夠經過流水線, 在一次寫入操做中發送多個命令:

在發送新命令以前, 無須閱讀前一個命令的回覆。

多個命令的回覆會在最後一併返回。

內聯命令

當你須要和 Redis 服務器進行溝通, 但又找不到 redis-cli , 而手上只有 telnet 的時候, 你能夠經過 Redis 特別爲這種情形而設的內聯命令格式來發送命令。

如下是一個客戶端和服務器使用內聯命令來進行交互的例子:

客戶端: PING
服務器: +PONG

如下另外一個返回整數值的內聯命令的例子:

客戶端: EXISTS somekey
服務器: :0

由於沒有了統一請求協議中的 "*" 項來聲明參數的數量, 因此在 telnet 會話輸入命令的時候, 必須使用空格來分割各個參數, 服務器在接收到數據以後, 會按空格對用戶的輸入進行分析(parse), 並獲取其中的命令參數。

高性能 Redis 協議分析器

儘管 Redis 的協議很是利於人類閱讀, 定義也很簡單, 但這個協議的實現性能仍然能夠和二進制協議同樣快。

由於 Redis 協議將數據的長度放在數據正文以前, 因此程序無須像 JSON 那樣, 爲了尋找某個特殊字符而掃描整個 payload , 也無須對發送至服務器的 payload 進行轉義(quote)。

程序能夠在對協議文本中的各個字符進行處理的同時, 查找 CR 字符, 並計算出批量回復或多條批量回復的長度, 就像這樣:

#include <stdio.h>

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;
}

獲得了批量回復或多條批量回復的長度以後, 程序只需調用一次 read 函數, 就能夠將回復的正文數據所有讀入到內存中, 而無須對這些數據作任何的處理。

在回覆最末尾的 CR 和 LF 不做處理,丟棄它們。

Redis 協議的實現性能能夠和二進制協議的實現性能相媲美, 而且因爲 Redis 協議的簡單性, 大部分高級語言均可以輕易地實現這個協議, 這使得客戶端軟件的 bug 數量大大減小。

相關文章
相關標籤/搜索