Redis 通信協議-瞭解 Redis 客戶端實現原理 發送命令 回復
戶端(http://redis.io/clients),不考慮Redis非常流行的原因,如果站在技術的角度看原因還有兩個:
- 客戶端與服務端之間的通信協議是在 TCP 協議之上構建的。
客戶端和伺服器通過 TCP 連接來進行數據交互, 伺服器默認的埠號為 6379 。
客戶端和伺服器發送的命令或數據一律以
(CRLF)結尾。
- 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++) {
/* Dont 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 通信協議-瞭解 Redis 客戶端實現原理
推薦閱讀: