網(wǎng)絡(luò)通信離不開各種各樣的協(xié)議,著名的tcp历谍,http等協(xié)議構(gòu)建了我們常見的web應(yīng)用现拒。http協(xié)議是基于tcp的應(yīng)用層協(xié)議。同樣的望侈,redis的協(xié)議也是基于tcp的應(yīng)用層協(xié)議印蔬,即RESP(REdis Serialization Protocol)。
RESP設(shè)計(jì)巧妙脱衙,它的追求在于下面三個(gè)方面:
- 易于實(shí)現(xiàn)
- 解析高效
- 易于人讀
協(xié)議基礎(chǔ)
RESP協(xié)議規(guī)定侥猬,客戶端通過tcp網(wǎng)絡(luò)連接到redis服務(wù)器。通信過程類似一問一答的方式捐韩,與HTTP類似退唠,客戶端發(fā)送命令請(qǐng)求到服務(wù)器,服務(wù)器執(zhí)行時(shí)候返回結(jié)果荤胁。
客戶端和服務(wù)器發(fā)送的命令或數(shù)據(jù)一律以 \r\n
(CRLF)結(jié)尾瞧预。正式因?yàn)镃RLF的存在,讓redis的解析十分方便和可讀寨蹋。
請(qǐng)求協(xié)議
我們知道松蒜,redis數(shù)據(jù)庫操作的命令無非就三種,第一中是不帶參數(shù)的命令已旧,例如PING
秸苗, FLUSHDB
;其次是帶有參數(shù)的讀取命令运褪,例如GET key
惊楼,LLEN queue
;還有帶有參數(shù)并設(shè)置值的寫操作秸讹,例如 SET hello world
檀咙,CONFIG SET TIMEOUT 30
。
命令和參數(shù)之間璃诀,通常都是用空格隔開弧可。RESP協(xié)議規(guī)定,這樣的請(qǐng)求中劣欢,命令和各參數(shù)之間必須使用CRLF分割棕诵,同時(shí)還必須提供命令頭標(biāo)簽,即token凿将。字節(jié)串的第一個(gè)字符到CRLF的內(nèi)容即為頭標(biāo)簽校套。
參數(shù)中的字符串和數(shù)字
字串
一個(gè)redis數(shù)據(jù)庫操作包含命令+參數(shù)
。當(dāng)然有的命令參數(shù)可以省略牧抵。無論編碼命令還是參數(shù)笛匙,其編碼方式都是一樣的侨把。
命令中,無論請(qǐng)求和返回的結(jié)果妹孙,無怪乎就只有字符串和數(shù)字秋柄。編碼字符串的格式如下:
$ + string_length + string + CRLF
,即以$
符號(hào)開頭蠢正,然后跟字節(jié)串的長(zhǎng)度华匾,然后跟著一個(gè)CRLF,再然后就是字節(jié)串本身机隙,最后以一個(gè)CRLF結(jié)尾蜘拉。
例如 hello
編碼成 $5\r\nhello\r\n
, 21.7
編碼成$3\r\n21.7\r\n
有鹿。其中$5\r\n
和$3\r\n
都是Token旭旭。
數(shù)字
數(shù)字類型和字符串類似,不同的在于數(shù)字使用:
開頭葱跋,同時(shí)不需要說明數(shù)字的長(zhǎng)度持寄。
: + number + \r\n
,即:
開頭娱俺,然后跟著數(shù)字稍味,最后以CRLF結(jié)尾即可。
注意荠卷,redis中絕大多數(shù)參數(shù)都是字串模庐,只有返回響應(yīng)的時(shí)候會(huì)處理數(shù)字類型,例如incr
操作油宜。其他情況下掂碱,請(qǐng)求的參數(shù),還是返回響
應(yīng)中的數(shù)字慎冤,其實(shí)都是數(shù)字字符串疼燥。例如下面是一個(gè)集合,返回元素的時(shí)候蚁堤,里面的元素都是字串
127.0.0.1:6379> sadd set hello
(integer) 1
127.0.0.1:6379> sadd set 世界
(integer) 1
127.0.0.1:6379> sadd set 1
(integer) 1
127.0.0.1:6379> sadd set 21.7
(integer) 1
127.0.0.1:6379> smembers set
1) "1"
2) "21.7"
3) "\xe4\xb8\x96\xe7\x95\x8c"
4) "hello"
sadd的set的參數(shù)中醉者,1
,和21.7
都是字串類型披诗,返回自然是字串撬即,而返回表示sadd成功的則是數(shù)字類型。所有請(qǐng)求的命令都是字串類型藤巢,哪怕寫的是數(shù)字字面量搞莺,redis最終還是當(dāng)成字串處理息罗。
編碼命令
上面我們了解單獨(dú)的命令和參數(shù)的編碼方式掂咒。下面介紹命令+參數(shù)的組合編碼方式。
組合編碼也很簡(jiǎn)單,無非就是將編碼的命令(字符串)拼接起來绍刮,同時(shí)再最開始追加一個(gè)* + 組合命令或參數(shù)的個(gè)數(shù)
的方式:
*<參數(shù)數(shù)量> CR LF
$<參數(shù) 1 的字節(jié)數(shù)量> CR LF
<參數(shù) 1 的數(shù)據(jù)> CR LF
...
$<參數(shù) N 的字節(jié)數(shù)量> CR LF
<參數(shù) N 的數(shù)據(jù)> CR LF
例如 PING
編碼字符串的方式為$4\r\nPING\r\n
温圆,組合的編碼為*1\r\n$4\r\nPING\r\n
。即
*1
$4
PING
GET hello
編碼為*2\r\n$3\r\nGET\r\n$5\r\nhello\r\n
*2
$3
GET
$5
hello
GET
和hello
兩個(gè)字符串分別編碼孩革,然后組合起來岁歉,編碼了兩個(gè)參數(shù),因此*
后面跟著是2膝蜈。
再看一個(gè)例子锅移,SET 中國 21.7
的將編碼成*3\r\n$3\r\nSET\r\n$6\r\n\xe4\xb8\xad\xe5\x9b\xbd\r\n$4\r\n21.7\r\n
*3
$3
SET
$6
\xe4\xb8\xad\xe5\x9b\xbd
$4
21.7
漢字中國
的按照utf-8
編碼的方式編碼成bytes為*\xe4\xb8\xad\xe5\x9b\xbd
,一個(gè)漢字三個(gè)字節(jié)饱搏,因此中國
的長(zhǎng)度是6非剃。
python3的字串原生支持
unicode
,計(jì)算機(jī)操作字串的時(shí)候是unicode推沸,當(dāng)需要網(wǎng)絡(luò)傳輸和寫入文件的時(shí)候备绽,都必須把unicode的字串編碼成bytes結(jié)構(gòu)。同理鬓催,當(dāng)從網(wǎng)絡(luò)或者文件中讀取數(shù)據(jù)的時(shí)候肺素,也需要解碼成unicode。目前國際通用的編碼方式以utf-8
為主宇驾。
值得注意的是倍靡,將命令參數(shù)組合的編碼方式,不僅是請(qǐng)求的時(shí)候如此课舍,在redis的響應(yīng)回復(fù)中菌瘫,其中的多批量回復(fù)(multi-bulk reply)也是采用了同樣的編碼方式返回。
無論單獨(dú)編碼數(shù)字布卡,字串還是命令組合雨让,我們都把開頭到第一個(gè)CRLF
的內(nèi)容看成頭標(biāo)簽(Token
)。
響應(yīng)協(xié)議
既然有請(qǐng)求的協(xié)議規(guī)定忿等,當(dāng)然也有響應(yīng)回復(fù)的協(xié)議栖忠。請(qǐng)求的命令已經(jīng)介紹了三種,其差別就在于參數(shù)的個(gè)數(shù)贸街。對(duì)于響應(yīng)的回復(fù)庵寞,則會(huì)比請(qǐng)求的情況更多。
RESP協(xié)議規(guī)定薛匪,所有答復(fù)的第一個(gè)字節(jié)規(guī)定了響應(yīng)答復(fù)的類型捐川,后除了類型之外,和請(qǐng)求中編碼字符串和數(shù)字的類似逸尖。響應(yīng)大概類型如下:
- 狀態(tài)回復(fù)(status reply)的第一個(gè)字節(jié)是
+
- 錯(cuò)誤回復(fù)(error reply)的第一個(gè)字節(jié)是
-
- 整數(shù)回復(fù)(integer reply)的第一個(gè)字節(jié)是
:
- 批量回復(fù)(bulk reply)的第一個(gè)字節(jié)是
$
- 多條批量回復(fù)(multi bulk reply)的第一個(gè)字節(jié)是
*
狀態(tài)回復(fù)
對(duì)于客戶端命令古沥,執(zhí)行一個(gè)查詢或者一個(gè)寫入操作瘸右,通常會(huì)返回操作的是否成功的狀態(tài)判定。狀態(tài)回復(fù)是單行回復(fù)岩齿。例如上面ping命令的返回就是ok太颤。
ok的狀態(tài)的編碼返回為+OK\r\n
,雖然OK
也是字串盹沈,但是在狀態(tài)中返回龄章,我們只需要知道狀態(tài)結(jié)果,不需要顯示的告訴返回多少字符乞封,后面可以知道做裙,字符串編碼的時(shí)候需要標(biāo)記字符數(shù),純粹是為了讀取socket的時(shí)候肃晚,確定分包邊界菇用。由于狀態(tài)回復(fù)只是單行字串,因此最后一個(gè)CRLF就能確定包的分界陷揪。
錯(cuò)誤回復(fù)
與狀態(tài)回復(fù)類似惋鸥,錯(cuò)誤回復(fù)的第一個(gè)字符是-
,然后跟著錯(cuò)誤的類型悍缠。錯(cuò)誤類型之后以一個(gè)空格結(jié)束卦绣,然后就是錯(cuò)誤的信息(msg),錯(cuò)誤信息是一段文字飞蚓,可以包含空格滤港,錯(cuò)誤信息結(jié)束之后也依然是一個(gè)CRLF表示回復(fù)結(jié)束。
例如命令不存在的時(shí)候會(huì)返回錯(cuò)誤:
127.0.0.1:6379> hello
(error) ERR unknown command 'hello'
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range
返回的錯(cuò)誤編碼回復(fù)依次是-ERR unknow command 'hello\r\n'
和-ERR value is not an integer or out of range\r\n
整數(shù)回復(fù)
所謂整數(shù)回復(fù)趴拧,即返回?cái)?shù)字類型的回復(fù)溅漾。整數(shù)回復(fù)以:
開頭,其后跟著數(shù)字著榴,最后以CRLF結(jié)尾添履。這和之前介紹的數(shù)字編碼的方式一樣。
例如:0\r\n
和 :1000\r\n
都是整數(shù)回復(fù)脑又。什么時(shí)候返回?cái)?shù)字類型呢暮胧?一般是incr數(shù)字操作和一些返回?cái)?shù)字表示邏輯的操作,例如DEL EXISTS等问麸。
127.0.0.1:6379> INCRBYFLOAT float 1.2
"1.2"
127.0.0.1:6379> INCRBYFLOAT float 1.2
"2.4"
127.0.0.1:6379> INCRBY integer 2
(integer) 2
127.0.0.1:6379> INCRBY integer 2
(integer) 4
127.0.0.1:6379> DEL integer
(integer) 1
INCRBY 和 INCRBYFLOAT都是自增參數(shù)的數(shù)字往衷,前者返回的是數(shù)字類型,后者返回的是字符串類型严卖。也就是我們接下來介紹的批量回復(fù)席舍。
批量回復(fù)
所謂的批量回復(fù),只redis返回的二進(jìn)制安全的字串哮笆。很大查詢返回都屬于批量回復(fù)来颤。例如上面的例子中汰扭,INCRBYFLOAT的返回即使批量回復(fù)。
批量回復(fù)以$
符號(hào)開頭脚曾,然后跟著字符串的長(zhǎng)度值,然后就是CRLF和實(shí)際的字串?dāng)?shù)據(jù)启具,最后以CRLF結(jié)束本讥。和字符串的編碼一模一樣茅糜。
例如查詢命令 get hello
捶闸,返回結(jié)果為world
的情況下,批量回復(fù)的原始數(shù)據(jù)編碼為$5\r\nworld\r\n
消玄。
當(dāng)請(qǐng)求的key的value不存在的時(shí)候薯演,將會(huì)返回-1
用于表示長(zhǎng)度的值撞芍。例如get non-existing-key
將收到$-1\r\n
的返回。對(duì)于這種空回復(fù)跨扮,客戶端語言就可以靈活處理序无,比較推薦的做法就是使用編程語言表示對(duì)象不存在的值,例如python的None
衡创,Golang的nil
帝嗡。
多批量回復(fù)
常見的返回是批量回復(fù),此外還有一個(gè)多批量回復(fù)璃氢,顧名思義哟玷,就是多個(gè)批量回復(fù)的組合。操作序列或者集合類的數(shù)據(jù)結(jié)構(gòu)一也,就會(huì)返回多批量回復(fù)巢寡。例如上面的smembers set
命令的返回將會(huì)是:
*4\r\n$1\r\n1\r\n$3\r\n21.7\r\n$6\r\n\xe4\xb8\x96\xe7\x95\x8c\r\n$5\r\nhello\r\n
這和我們編碼請(qǐng)求的方式一模一樣。由此可見椰苟,RESP的設(shè)計(jì)非常優(yōu)雅抑月。
多批量回復(fù)是批量回復(fù)的組合,那么也會(huì)有返回-1
的情況舆蝴。此外爪幻,當(dāng)讀取一個(gè)集合,如果集合沒有元素须误,則不是返回nil挨稿,而是返回空集合的結(jié)果,接多批量回復(fù)的結(jié)果為*0\r\n
京痢。
官網(wǎng)的還給出了一個(gè)例子關(guān)于多批量回復(fù)的空元素奶甘。例如下面的多重批量回復(fù):
*3
$3
foo
$-1
$3
bar
其中, 回復(fù)中的第二個(gè)元素為空祭椰。因此最終轉(zhuǎn)換成python的對(duì)象應(yīng)該是這樣的:["foo", None, "bar"]
臭家。
總結(jié)
RESP的協(xié)議基于tcp的應(yīng)用層協(xié)議疲陕,主要用到了字符和數(shù)字的編碼和CRLF的組合編碼方式。我們認(rèn)識(shí)到字串的編碼和數(shù)字編碼的方式钉赁。請(qǐng)求的時(shí)候使用類似多批量回復(fù)方式編碼蹄殃,回復(fù)的響應(yīng)有多種,可以根據(jù)第一個(gè)字節(jié)做不同情況的處理你踩,比如狀態(tài)回復(fù)诅岩,錯(cuò)誤回復(fù),批量回復(fù)和多批量回復(fù)带膜。
對(duì)于多批量回復(fù)吩谦,空和無窮的方式也需要考慮,前者會(huì)返回一個(gè)0表示空膝藕,后者會(huì)返回-1表示無窮大(block情況也類似)式廷。
了解了協(xié)議的編碼方式,下一步芭挽,我們就使用代碼實(shí)現(xiàn)這個(gè)協(xié)議的編碼和解碼過程滑废。