Java互聯網架構-深入剖析分佈式Redis 通信協議

概述

Redis是一個開源(BSD許可),內存存儲的數據結構服務器,可用作數據庫,高速緩存和消息隊列代理。它支持字符串、哈希表、列表、集合、有序集合,位圖,hyperloglogs等數據類型。內置複製、Lua腳本、LRU收回、事務以及不同級別磁盤持久化功能,同時通過Redis Sentinel提供高可用,通過Redis Cluster提供自動分區。

協議簡介

Redis的客戶端與服務端採用一種叫做 RESP(REdis Serialization Protocol) 的網絡通信協議交換數據。 這種協議採用明文傳輸,易讀也易解析。Redis客戶端採用此協議格式來對服務端發送不同的命令,服務端會根據具體的操作而返回具體的答覆。客戶端和服務端採用的是簡單的請求-響應模型進行通信的。

發送命令

RESP 在 Redis 1.2 版本中引入, 並最終在 Redis 2.0 版本成為 Redis 服務器通信的標準方式。

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

RESP 的規定一條命令的格式如下:

*<參數數量> CR LF

$<參數 1 的字節數量> CR LF

<參數 1 的數據> CR LF

...

$<參數 N 的字節數量> CR LF

<參數 N 的數據> CR LF

命令本身也作為協議的其中一個參數來發送。

例如我們經常執行的 SET 命令,在命令行中我們輸入如下:

SET key value

使用 RESP 協議規定的格式:

*3

$3

SET

$3 # 這裡 key 一共三個字節

key

$5 # 這裡 value 一共五個字節

value

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

"*3 $3 SET $3 key $5 value "

回覆

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

通過檢查服務器發回數據的第一個字節, 可以確定這個回覆是什麼類型:

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

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

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

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

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

我們知道redis-cli只能看到最終的執行結果,那是因為redis-cli本身就按照RESP進行結果解析的,所以看不到中間結果,redis-cli.c 源碼對命令結果的解析結構如下:

static sds cliFormatReplyTTY(redisReply *r, char *prefix) {

sds out = sdsempty();

switch (r->type) {

// 處理錯誤回覆

case REDIS_REPLY_ERROR:

out = sdscatprintf(out,"(error) %s ", r->str);

break;

// 處理狀態回覆

case REDIS_REPLY_STATUS:

out = sdscat(out,r->str);

out = sdscat(out," ");

break;

// 處理整數回覆

case REDIS_REPLY_INTEGER:

out = sdscatprintf(out,"(integer) %lld ",r->integer);

break;

// 處理字符串回覆

case REDIS_REPLY_STRING:

/* If you are producing output for the standard output we want

* a more interesting output with quoted characters and so forth */

out = sdscatrepr(out,r->str,r->len);

out = sdscat(out," ");

break;

// 處理 nil

case REDIS_REPLY_NIL:

out = sdscat(out,"(nil) ");

break;

// 處理多回復

case REDIS_REPLY_ARRAY:

if (r->elements == 0) {

out = sdscat(out,"(empty list or set) ");

} else {

unsigned int i, idxlen = 0;

char _prefixlen[16];

char _prefixfmt[16];

sds _prefix;

sds tmp;

/* Calculate chars needed to represent the largest index */

i = r->elements;

do {

idxlen++;

i /= 10;

} while(i);

/* Prefix for nested multi bulks should grow with idxlen+2 spaces */

memset(_prefixlen,' ',idxlen+2);

_prefixlen[idxlen+2] = '';

_prefix = sdscat(sdsnew(prefix),_prefixlen);

/* Setup prefix format for every entry */

snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%ud) ",idxlen);

for (i = 0; i < r->elements; i++) {

/* Don't use the prefix for the first element, as the parent

* caller already prepended the index number. */

out = sdscatprintf(out,_prefixfmt,i == 0 ? "" : prefix,i+1);

/* Format the multi bulk entry */

tmp = cliFormatReplyTTY(r->element[i],_prefix);

out = sdscatlen(out,tmp,sdslen(tmp));

sdsfree(tmp);

}

sdsfree(_prefix);

}

break;

default:

fprintf(stderr,"Unknown reply type: %d ", r->type);

exit(1);

}

return out;

}

在 發送命令 一節中使用的格式除了用作命令請求協議之外, 也用在命令的回覆協議中: 這種只有一個參數的回覆格式被稱為批量回復(Bulk Reply)。

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

一個多條批量回復以 *<argc> 為前綴, 後跟多條不同的批量回復, 其中 argc 為這些批量回復的數量。

狀態回覆

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

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

+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 " 和 ":1000 " 都是整數回覆。

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

整數回覆也被廣泛地用於表示邏輯真和邏輯假: 比如 EXISTS 和 SISMEMBER 都用返回值 1 表示真, 0 表示假。

其他一些命令, 比如 SADD 、 SREM 和 SETNX , 只在操作真正被執行了的時候, 才返回 1 , 否則返回 0 。

以下命令都返回整數回覆: SETNX 、 DEL 、 EXISTS 、 INCR 、 INCRBY 、 DECR 、 DECRBY 、 DBSIZE 、 LASTSAVE 、 RENAMENX 、 MOVE 、 LLEN 、 SADD 、 SREM 、 SISMEMBER 、 SCARD 。

批量回復

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

客戶端:GET mykey

服務器:foobar

服務器發送的內容中:

第一字節為 "$" 符號

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

- 之後跟著一個 CRLF

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

- 最末尾是另一個 CRLF

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

"$6 foobar "

如果被請求的值不存在, 那麼批量回復會將特殊值 -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

:1

:2

:3

:4

$6

foobar

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

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

客戶端: LRANGE nokey 0 1

服務器: *0

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

客戶端: BLPOP key 1

服務器: *-1

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

多條批量回復中的空元素

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

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

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

服務器: *3

服務器: $3

服務器: foo

服務器: $-1

服務器: $3

服務器: bar

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

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

["foo", nil, "bar"]

多命令和 pipline

客戶端可以通過 pipline , 在一次寫入操作中發送多個命令:

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

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

內聯命令

當你需要和 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 ";

int len = 0;

p++;

while(*p != ' ') {

len = (len*10)+(*p - '0');

p++;

}

/* Now p points at ' ', and the len is in bulk_len. */

printf("%d ", len);

return 0;

}

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

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

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

Linux 下 使用 nc 命令操作 Redis

[coderknock ~]# nc 127.0.0.1 6379

set hello world

+OK #狀態回覆

sethx

-ERR unknown command 'sethx' #錯誤回覆:由於sethx這條命令不存在,那麼返回結果就是"-"號加上錯誤消息

incr counter

:1 #整數回覆:當命令的執行結果是整數時,返回結果就是整數回覆,例如 incr、exists、del、dbsize返回結果都是整數

get hello

$5 #字符串回覆:當命令的執行結果是字符串時,返回結果就是字符串回覆。

world #實際返回的是 $5 world

mset java jedis python redis-py

+OK

mget java python #多條字符串回覆:當命令的執行結果是多條字符串時,返回結果就是多條字符串回覆

*2

$5

jedis

$8

redis-py

get not_exist_key #無論是字符串回覆還是多條字符串回覆,如果有 nil 值,那麼會返回$-1。

$-1

mget hello not_exist_key java

*3

$5

world

$-1

$5

jedis

Python Socket 操作 Redis

使用 socket 操作 Redis:

import socket

# AF_INET指定使用 IPv4 協議

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.SOL_TCP)

s.connect(('127.0.0.1', 6379))

print('get connected from', '127.0.0.1')

# 驗證密碼

s.send(b'*2 $4 AUTH $8 admin123 ')

ra = s.recv(512)

print(ra)

# 發送一條信息

s.send(b'*3 $3 SET $8 testRESP $10 RESPpython ')

ra = s.recv(512)

print(ra)

s.close()

執行結果:

get connected from 127.0.0.1

b'+OK '

b'+OK '

我們從命令行中查詢:

redis> GET testRESP

"RESPpython"

可以看到正確的向 Redis 中插入了鍵值。

總結

到這裡,深入剖析分佈式Redis 通信協議就結束了,,不足之處還望大家多多包涵!!覺得收穫的話可以點個關注收藏轉發一波喔,謝謝大佬們支持。(吹一波,233~~)

下面和大家交流幾點編程的經驗:

1、多寫多敲代碼,好的代碼與紮實的基礎知識一定是實踐出來的

2丶 測試、測試再測試,如果你不徹底測試自己的代碼,那恐怕你開發的就不只是代碼,可能還會聲名狼藉。

3丶 簡化編程,加快速度,代碼風騷,在你完成編碼後,應回頭並且優化它。從長遠來看,這裡或那裡一些的改進,會讓後來的支持人員更加輕鬆。

最後,每一位讀到這裡的網友,感謝你們能耐心地看完。希望在成為一名更優秀的Java程序員的道路上,我們可以一起學習、一起進步。

內部交流群469717771 歡迎各位前來交流和分享, 驗證:(007)

概述

Redis是一個開源(BSD許可),內存存儲的數據結構服務器,可用作數據庫,高速緩存和消息隊列代理。它支持字符串、哈希表、列表、集合、有序集合,位圖,hyperloglogs等數據類型。內置複製、Lua腳本、LRU收回、事務以及不同級別磁盤持久化功能,同時通過Redis Sentinel提供高可用,通過Redis Cluster提供自動分區。

協議簡介

Redis的客戶端與服務端採用一種叫做 RESP(REdis Serialization Protocol) 的網絡通信協議交換數據。 這種協議採用明文傳輸,易讀也易解析。Redis客戶端採用此協議格式來對服務端發送不同的命令,服務端會根據具體的操作而返回具體的答覆。客戶端和服務端採用的是簡單的請求-響應模型進行通信的。

發送命令

RESP 在 Redis 1.2 版本中引入, 並最終在 Redis 2.0 版本成為 Redis 服務器通信的標準方式。

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

RESP 的規定一條命令的格式如下:

*<參數數量> CR LF

$<參數 1 的字節數量> CR LF

<參數 1 的數據> CR LF

...

$<參數 N 的字節數量> CR LF

<參數 N 的數據> CR LF

命令本身也作為協議的其中一個參數來發送。

例如我們經常執行的 SET 命令,在命令行中我們輸入如下:

SET key value

使用 RESP 協議規定的格式:

*3

$3

SET

$3 # 這裡 key 一共三個字節

key

$5 # 這裡 value 一共五個字節

value

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

"*3 $3 SET $3 key $5 value "

回覆

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

通過檢查服務器發回數據的第一個字節, 可以確定這個回覆是什麼類型:

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

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

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

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

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

我們知道redis-cli只能看到最終的執行結果,那是因為redis-cli本身就按照RESP進行結果解析的,所以看不到中間結果,redis-cli.c 源碼對命令結果的解析結構如下:

static sds cliFormatReplyTTY(redisReply *r, char *prefix) {

sds out = sdsempty();

switch (r->type) {

// 處理錯誤回覆

case REDIS_REPLY_ERROR:

out = sdscatprintf(out,"(error) %s ", r->str);

break;

// 處理狀態回覆

case REDIS_REPLY_STATUS:

out = sdscat(out,r->str);

out = sdscat(out," ");

break;

// 處理整數回覆

case REDIS_REPLY_INTEGER:

out = sdscatprintf(out,"(integer) %lld ",r->integer);

break;

// 處理字符串回覆

case REDIS_REPLY_STRING:

/* If you are producing output for the standard output we want

* a more interesting output with quoted characters and so forth */

out = sdscatrepr(out,r->str,r->len);

out = sdscat(out," ");

break;

// 處理 nil

case REDIS_REPLY_NIL:

out = sdscat(out,"(nil) ");

break;

// 處理多回復

case REDIS_REPLY_ARRAY:

if (r->elements == 0) {

out = sdscat(out,"(empty list or set) ");

} else {

unsigned int i, idxlen = 0;

char _prefixlen[16];

char _prefixfmt[16];

sds _prefix;

sds tmp;

/* Calculate chars needed to represent the largest index */

i = r->elements;

do {

idxlen++;

i /= 10;

} while(i);

/* Prefix for nested multi bulks should grow with idxlen+2 spaces */

memset(_prefixlen,' ',idxlen+2);

_prefixlen[idxlen+2] = '';

_prefix = sdscat(sdsnew(prefix),_prefixlen);

/* Setup prefix format for every entry */

snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%ud) ",idxlen);

for (i = 0; i < r->elements; i++) {

/* Don't use the prefix for the first element, as the parent

* caller already prepended the index number. */

out = sdscatprintf(out,_prefixfmt,i == 0 ? "" : prefix,i+1);

/* Format the multi bulk entry */

tmp = cliFormatReplyTTY(r->element[i],_prefix);

out = sdscatlen(out,tmp,sdslen(tmp));

sdsfree(tmp);

}

sdsfree(_prefix);

}

break;

default:

fprintf(stderr,"Unknown reply type: %d ", r->type);

exit(1);

}

return out;

}

在 發送命令 一節中使用的格式除了用作命令請求協議之外, 也用在命令的回覆協議中: 這種只有一個參數的回覆格式被稱為批量回復(Bulk Reply)。

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

一個多條批量回復以 *<argc> 為前綴, 後跟多條不同的批量回復, 其中 argc 為這些批量回復的數量。

狀態回覆

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

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

+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 " 和 ":1000 " 都是整數回覆。

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

整數回覆也被廣泛地用於表示邏輯真和邏輯假: 比如 EXISTS 和 SISMEMBER 都用返回值 1 表示真, 0 表示假。

其他一些命令, 比如 SADD 、 SREM 和 SETNX , 只在操作真正被執行了的時候, 才返回 1 , 否則返回 0 。

以下命令都返回整數回覆: SETNX 、 DEL 、 EXISTS 、 INCR 、 INCRBY 、 DECR 、 DECRBY 、 DBSIZE 、 LASTSAVE 、 RENAMENX 、 MOVE 、 LLEN 、 SADD 、 SREM 、 SISMEMBER 、 SCARD 。

批量回復

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

客戶端:GET mykey

服務器:foobar

服務器發送的內容中:

第一字節為 "$" 符號

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

- 之後跟著一個 CRLF

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

- 最末尾是另一個 CRLF

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

"$6 foobar "

如果被請求的值不存在, 那麼批量回復會將特殊值 -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

:1

:2

:3

:4

$6

foobar

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

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

客戶端: LRANGE nokey 0 1

服務器: *0

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

客戶端: BLPOP key 1

服務器: *-1

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

多條批量回復中的空元素

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

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

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

服務器: *3

服務器: $3

服務器: foo

服務器: $-1

服務器: $3

服務器: bar

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

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

["foo", nil, "bar"]

多命令和 pipline

客戶端可以通過 pipline , 在一次寫入操作中發送多個命令:

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

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

內聯命令

當你需要和 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 ";

int len = 0;

p++;

while(*p != ' ') {

len = (len*10)+(*p - '0');

p++;

}

/* Now p points at ' ', and the len is in bulk_len. */

printf("%d ", len);

return 0;

}

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

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

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

Linux 下 使用 nc 命令操作 Redis

[coderknock ~]# nc 127.0.0.1 6379

set hello world

+OK #狀態回覆

sethx

-ERR unknown command 'sethx' #錯誤回覆:由於sethx這條命令不存在,那麼返回結果就是"-"號加上錯誤消息

incr counter

:1 #整數回覆:當命令的執行結果是整數時,返回結果就是整數回覆,例如 incr、exists、del、dbsize返回結果都是整數

get hello

$5 #字符串回覆:當命令的執行結果是字符串時,返回結果就是字符串回覆。

world #實際返回的是 $5 world

mset java jedis python redis-py

+OK

mget java python #多條字符串回覆:當命令的執行結果是多條字符串時,返回結果就是多條字符串回覆

*2

$5

jedis

$8

redis-py

get not_exist_key #無論是字符串回覆還是多條字符串回覆,如果有 nil 值,那麼會返回$-1。

$-1

mget hello not_exist_key java

*3

$5

world

$-1

$5

jedis

Python Socket 操作 Redis

使用 socket 操作 Redis:

import socket

# AF_INET指定使用 IPv4 協議

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.SOL_TCP)

s.connect(('127.0.0.1', 6379))

print('get connected from', '127.0.0.1')

# 驗證密碼

s.send(b'*2 $4 AUTH $8 admin123 ')

ra = s.recv(512)

print(ra)

# 發送一條信息

s.send(b'*3 $3 SET $8 testRESP $10 RESPpython ')

ra = s.recv(512)

print(ra)

s.close()

執行結果:

get connected from 127.0.0.1

b'+OK '

b'+OK '

我們從命令行中查詢:

redis> GET testRESP

"RESPpython"

可以看到正確的向 Redis 中插入了鍵值。

總結

到這裡,深入剖析分佈式Redis 通信協議就結束了,,不足之處還望大家多多包涵!!覺得收穫的話可以點個關注收藏轉發一波喔,謝謝大佬們支持。(吹一波,233~~)

下面和大家交流幾點編程的經驗:

1、多寫多敲代碼,好的代碼與紮實的基礎知識一定是實踐出來的

2丶 測試、測試再測試,如果你不徹底測試自己的代碼,那恐怕你開發的就不只是代碼,可能還會聲名狼藉。

3丶 簡化編程,加快速度,代碼風騷,在你完成編碼後,應回頭並且優化它。從長遠來看,這裡或那裡一些的改進,會讓後來的支持人員更加輕鬆。

最後,每一位讀到這裡的網友,感謝你們能耐心地看完。希望在成為一名更優秀的Java程序員的道路上,我們可以一起學習、一起進步。

內部交流群469717771 歡迎各位前來交流和分享, 驗證:(007)

Java互聯網架構-深入剖析分佈式Redis 通信協議

相關推薦

推薦中...