好多年前看過redis的代碼,那個時候還是2.6的版本,集群和哨兵還沒加入正式代碼,這幾年redis發展的好快。簡略翻譯一篇文章redis的https://redis.io/topics/protocol
redis的客戶端和服務器通過一種叫RESP (REdis Serialization Protocol)協議進行通訊。雖然他是為redis設計的協議,但是也可以用到其他的CS架構軟件里。
RESP的設計主要考慮了以下幾個要求:
- 容易實現
- 快速解析
- 人類可讀
RESP能夠序列化不同的數據類型像整型,字符串,數組。還有一種專門為錯誤設計的類型。從客戶端發給服務器的請求是一個代表命令參數的數組。Redis返回一個該命令指定的數據類型。
RESP是二進制安全的而且從一個進程發塊數據給另一個進程的時候不需要做轉換,因為他會在塊數據之前加上長度。
注意:這個協議只用於redis的客戶端和服務器的信息交互。redis集群的node之間使用另一種二進制協議來交換信息。
網絡層
redis的客戶端和服務器通過6379端口建立連接。
雖然RESP是不依賴TCP協議的,但是這個協議只用在TCP連接上(或者是其他的流協議,比如unix套接字)。
請求-應答模型
redis能夠接收帶各種類型參數的命令。處理收到的命令並且返回應答。這是最簡單的模型,但是有兩個例外:
- redis支持管道,所以客戶端可能一次發送多個命令然后等待應答。
- 如果redis客戶端使用了發布/訂閱模式(Pub/Sub),這個協議就變成了推送協議(push protocol),也就是說客戶端不用再發命令了,服務器收到相應消息的時候會自動發給客戶端。
除了以上兩種情況,redis協議是一個簡單的請求應答協議。
RESP協議描述
RESP協議是在redis的1.2版本引入的,之后在2.0版本成為標准協議,需要在客戶端實現。
RESP是一種序列化協議,支持以下類型:簡單字符串(sample strings),錯誤(Errors),整型(integers),塊字符串(bulk strings)和數組(arrays)。
在RESP里,數據類型是第一個字節決定的:
- 第一個字節是“+”代表簡單字符串類型。
- 第一個字節是“-”代表錯誤類型。
- 第一個字節是“:”代表整型。
- 第一個字節是“$”代表塊字符串。
- 第一個自己是“*”代表數組。
RESP能用一個特殊的塊字符串代表NULL。RESP協議不同的部分用“\r\n”(CRLF)結尾。
RESP簡單字符串
簡單字符串用如下方法編碼:一個“+”號后面跟字符串,最后是“\r\n”,字符串里不能包含"\r\n"。簡單字符串用來傳輸比較短的二進制安全的字符串。例如很多redis命令執行成功會返回“OK”,用RESP編碼就是5個字節:
|
想要發送二進制安全的字符串,需要用RESP的塊字符串。當redis返回了一個簡單字符串的時候,客戶端庫需要給調用者返回“+”號(不含)之后CRLF之前(不含)的字符串。
RESP錯誤
RESP有一種專門為錯誤設計的類型。實際上錯誤類型很像RESP簡單字符串類型,但是第一個字符是“-”。簡單字符串類型和錯誤類型的區別是客戶端把錯誤類型當成一個異常,錯誤類型包含的字符串是異常信息。格式是:
|
有錯誤發生的時候才會返回錯誤類型,例如你執行了一個對於某類型錯誤的操作,或者命令不存在等。當返回一個錯誤類型的時候客戶端庫應該發起一個異常。下面是一個錯誤類型的例子
|
“-”號之后空格或者換行符之前的字符串代表返回的錯誤類型,這只是慣例,並不是RESP要求的格式。例如ERR是一般錯誤,WRONGTYPE是更具體的錯誤表示客戶端的試圖在錯誤的類型上執行某個操作。這個稱為錯誤前綴,能讓客戶端更方便的識別錯誤類型。
客戶端可能為不同的錯誤返回不同的異常,也可能只提供一個一般的方法來捕捉錯誤並提供錯誤名。但是不能依賴客戶端提供的這些特性,因為有的客戶端僅僅返回一般錯誤,比如false。
RESP整型
RESP只是用一個CRLF結尾的字符串代表整型,第一個自己是“:”,例如":0\r\n"或者":1000\r\n"是整型返回。有很多redis命令返回整型,比如INCR,LLEN和LASTSVAE。返回的整型沒有特定的意義,對於INCR就是一個增長后的值,對於LASTSAVE就是一個UNIX時間戳。雖然沒什么意義,但是還是給他分配了一個64位的有符號的空間。整型返回值還可以用於返回true或者false,比如命令EXISTS或者SISMEMBER會返回1來代表true,返回0來代表false。其他命令比如SADD,SREM和SETNX如果執行了會返回1,沒執行就返回0。如下命令也會返回整型SETNX, DEL, EXISTS, INCR, INCRBY, DECR, DECRBY, DBSIZE, LASTSAVE, RENAMENX, MOVE, LLEN, SADD, SREM, SISMEMBER, SCARD。
RESP塊字符串
塊字符串用來代表二進制安全的字符從,長度可達512M。塊字符串用如下方式編碼:
- “$”開頭,跟着數字代表字符串長度,最后是CRLF。
- 字符串。
- 最后CRLF。
所以“foobar”會編碼成如下方式:
|
空字符串如下:
|
RESP塊字符串還能用一個特殊的格式來表示一個不存在的值,代表NULL值。在這個特殊的格式里長度是-1,不帶數據。所以NULL用如下格式表示:
|
這個被稱為空塊字符串(NULL BULK STRING)。客戶端庫API不能返回空字符串,當服務端返回空塊字符串的時候客戶端需要返回nil。例如RUBY庫需要返回‘nil’,C庫需要返回NULL(或者在返回值里設置一個特殊的標志)。
RESP數組
redis客戶端用RESP數組給服務器發命令。一些命令的返回值也是RESP數組類型,比如LRANGE。RESP數組使用如下格式:
- “*”號作為第一個字符,跟着一個數字代表元素個數,后面一個CRLF。
- 每個元素是一個RESP類型。
空數組如下:
|
包含兩個塊字符串“foo”,“bar”的數組如下:
|
*<count>CRLF在數組前面,其他的就是一個接一個的元素。例如三個整數組成的數組如下:
|
數組的元素不必是同樣的類型,可以是混合的,例如包含一個包含四個整數一個塊字符串的數組如下:
|
服務器第一行發送*5\r\n代表后面跟着5個元素,之后傳送每一個元素。空數組是存在的,也可以用來表示空值(一般情況下用空塊字符串,但是因為歷史原因兩種都可以用)。例如BLPOP命令超時,他會返回一個空數組,如下:
|
當服務器返回空數組以后客戶端的庫API應該返回一個空對象而不是空數組,區分不同狀態下的空數組是有必要的。在RESP里包含數組的數組是合法的。如下,一個數組包含兩個數組
|
上面是一個包含三個整數的數組和一個簡單字符串數組組成的數組。
包含空元素的數組
數組里的一個元素可以為空。可以用在應答里來表明這個元素丟失了。如下數組包含一個空元素:
|
第二個元素是空,客戶端庫應該返回如下:
|
注意這不是上一節說的異常,而是為了進一步說明RESP協議。
給REDIS服務器發命令
現在你熟悉了RESP序列化,寫一個REDIS客戶端庫應該不難。我們來進一步說明客戶端和服務器是怎樣交互的:
- 客戶端給服務器端發送只包含塊字符串的數組。
- 服務器會給客戶端發送任何合法的RESP數據類型。
一個典型的交互是下面這樣的,客戶端為了獲取mylist鏈表的長度發送LLEN mylist命令給服務器,服務器返回一個整數作為應答:
|
為了清楚我們把每一部分寫在了一行,實際上客戶端發送的是*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n
。
多命令和管道
如果你需要給redis服務器發命令,但是手頭只有telnet工具怎么辦呢?盡管redis協議是很容易實現的,但是用這種交互型的工具實現redis協議也不是理想的工具。因此redis也可以用一種特殊的方式接受人類可讀的命令,稱為內聯格式(inline command)。如下是客戶端服務器用內聯格式的例子:
|
下面是另一個內聯格式,返回一個整數:
|
其實就是簡單的把參數用空格分開,因為命令都不是“*”開頭的,redis就會檢測這種形式並且解析。
redis協議的高性能解析器
略吧,這一段沒什么意思。