redis通訊協議(RESP )是什麼
什麼是RESP
RESP是REdis Serialization Protocol的簡稱,也就是專門為redis設計的一套序列化協議. 這個協議其實在redis的1.2版本時就已經出現了,但是到了redis2.0才最終成為redis通訊協議的標準
這個序列化協議聽起來很高大上,但實際上就是一個文字協議.根據官方的說法,這個協議是基於以下幾點(而妥協)設計的:
1. 實現簡單.可以減低客戶端出現bug的機率
2. 解析速度快.由於RESP能知道返回資料的固定長度,所以不用像json那樣掃描整個payload去解析,所以它的效能是能跟解析二進位制資料的效能相媲美的.
3. 可讀性好.
複製程式碼
為啥要理解RESP
其實RESP是個很簡單的東西,不用一天就能吃透. 但是我對它的認識一直都停留在一個很模糊的狀態,之前只知道它返回的不同的型別是以不同的符號開始的,具體是什麼沒有仔細去深究.
直到前幾天遇到一個bug,除錯redis客戶端的時候發現對redis的返回內容特別陌生. 今天在看AOF檔案時又遇到了它,才突然悟到:書到用時方恨少啊
於是就有這一篇部落格.
總結來說,RESP的應用場景有:
1. 開發定製化的客戶端. RESP設計成簡單的文字協議,一大原因就是為了降低各種語言開發客戶端的複雜度
2. 理解RESP方便我們分析AOF檔案,瞭解redis的內部設計
3. 平時通過抓包軟體,可以幫助快速定位redis的相關問題
4. 在沒有redis-cli的情況下,方便開發除錯redis命令
複製程式碼
RESP詳解
資料型別
一般來說,RESP只需要序列化三種陣列即可: 字串,整數,陣列. 而在實際場景中,RESP又把字串細化成了simple string,error string和bulk string三種.
所以RESP一共涉及到5種資料型別:
1. simple string. 簡單的字串
2. error. 就是表示這是一個錯誤(異常)情況
3. integer 表示這是一個整數
4. bulk string. 表示是長字串,但是必須小於512M.
5. arrays. 表示這是一個陣列,陣列元素可以是上面的任意一種型別,也可以是一個陣列
複製程式碼
像一些高階語言用int long等來表示不同資料型別一樣,RESP也有它自己標識不同資料型別的"語法",就是用第一個位元組的符號來表示不同的資料型別:
- simple string 的第一個位元組是個"+"(加號),後面接著的是字串的內容,最後以CRLF(\r\n)結尾.例如:
"+OK\r\n"
複製程式碼
- error. error其實和string是類似的,但是RESP為了能讓不同客戶端把這種error和正常的返回結果區分開來對待 (例如redis返回error的話,就丟擲異常),特意多設計了這個資料型別. error型別的第一個位元組是"-"(減號),後面接著的是錯誤的資訊,最後以CRLF(\r\n)結尾,例如:
"-ERR unknown command 'foobar'\r\n"
複製程式碼
- integer 型別的第一個位元組是":"(冒號),後面接著的是整數,例如:
":1000\r\n"
複製程式碼
- bulk string. 本質上也是字串.跟普通字串區分開來,它的第一個位元組是"$"(美元符號),緊接著是一個整數,表示字串的位元組數,位元組數後面接一個CRLF. CRLF後面是字串的內容,最後以一個CRLF結尾. 例如:
"$0\r\n" --$後面的0表示這是一個空字串
"$-1\r\n" -- $後面的-1表示這是一個null字串,Null Bulk String要求客戶端返回空物件,而不能簡單地返回個空字串
"$6\r\nABCDEF\r\n" -- ABCDEF是6個位元組,所以$後面是6
複製程式碼
- arrays的第一個位元組是"*"(星號),緊接著後面是一個數字,表示這個陣列的長度,數字後面是一個CRLF. 需要注意的是這個CRLF之後才是陣列的真正內容,而且陣列內容可以是任意型別,包括arrays和bulk string,每個元素也要以CRLF結尾. 最後以CRLF(\r\n)結尾. 舉例:
"*0\r\n" --*後面的0表示表示空的陣列
"*-1\r\n" --*後面的-1表示表示是null陣列
"*5\r\n -- *5表示這是一個擁有5個元素的陣列
+bar\r\n -- 第1個元素是簡單的字串
-unknown command\r\n -- 第2個元素是個異常
:3\r\n -- 第3個元素是個整數
$3\r\n -- 第4個元素是長度為3個位元組的長字串foo
foo\r\n -- 第4個元素的內容
*3\r\n -- 第5個元素又是個陣列
:1\r\n -- 第5個元素陣列的第1元素
:2\r\n -- 第5個元素陣列的第2元素
:3\r\n -- 第5個元素陣列的第3元素
"
複製程式碼
request-response模型
一般來說,redis客戶端和服務端互動都是通過以下兩個步驟:
1. redis傳送一個命令到服務端,然後阻塞在socket.read()方法,等待服務端的返回
2. 服務端收到一個命令,處理完成後將資料傳送回去給客戶端
複製程式碼
這個就被稱為request/reponse模型. redis的大部分命令都是使用這種模型進行通訊,除了兩種情況:
1. pipeline模式. 在pipeline模式下,客戶端可能會把多個命令收集在一起,然後一併傳送給服務端,最後等待服務端把所有命令的執行響應一併傳送回來
2. pub/sub,釋出訂閱模式下,redis客戶端只需要傳送一次訂閱命令
複製程式碼
RESP協議的request/response模型可以總結為以下兩個步驟
1. 客戶端傳送命令,一般組裝成bulk string的陣列
2. 服務端處理命令,根據不同的命令,可能返回不同的資料型別
複製程式碼
例如命令"set test1 1" 一般被序列化成
*3\r\n$3\r\nset\r\n$5\r\ntest1\r\n$1\r\n1\r\n
-- 為了方便理解,每個CRLF我們給它換一下行
*3\r\n -- 這個命令包含3個(bulk)字串
$3\r\n -- 第一個bulk string有3個位元組
set\r\n -- 第一個bulk string是set
$5\r\n -- 第二個bulk string有5個位元組
test1\r\n -- 第二個bulk string是test1
$1\r\n -- 第三個bulk string有1個位元組
1\r\n -- 第三個bulk string是1
複製程式碼
它的返回是:
+OK\r\n --一個簡單的字串
複製程式碼
再例如命令"get test1":
*2\r\n$3\r\nget\r\n$5\r\ntest1\r\n
即:
*2\r\n -- 這個命令是2個bulk字串的陣列
$3\r\n -- 第一個bulk字串有3個位元組: get
get\r\n
$5\r\n -- 第二個bulk字串有5個位元組: test1
test1\r\n
複製程式碼
這個命令的返回是:
$1\r\n -- 只有一個位元組的bulk string
1\r\n
複製程式碼
再來看一個錯誤的命令"get ",這裡我們get的命令故意不傳引數
request:
*1\r\n
$3\r\n
get\r\n
response(跟我們在redis-cli裡面獲取的提示是一樣的):
-ERR wrong number of arguments for 'get' command\r\n
複製程式碼
測試和驗證
瞭解了RESP是什麼之後,我們通常都會想動手驗證一下,它實際的執行是否跟理論一致. 這個時候有兩種方法.
telnet方式
當我們手上沒有redis-cli的時候,有時候我們想除錯redis命令就顯得比較麻煩. 這點redis做得比較人性化,當它發現它收到的資料不是以"*"開頭時,它就會嘗試解析這個字串,把它當做一個命令來處理,然後返回對應的RESP格式的響應.
來看一下用telnet執行我們上面測試的3個命令:
lhh-Mac:~ lhh$ telnet localhost 6379
Trying ::1...
Connected to localhost.
Escape character is '^]'.
set test1 1
+OK
get test1
$1
1
get
-ERR wrong number of arguments for 'get' command
quit
+OK
複製程式碼
可以看到,每個命令返回的都是RESP格式(\r\n不可見,體現為換行).
當然,你也可以傳送RESP格式的命令,但是要在本文編輯器裡面把\r\n換成換行符,再複製過去,不然會報錯.
下面例如例子中,我執行的命令是"get test1",RESP格式就是"*2\r\n$3get\r\n$5\r\ntets1".
返回的資料是"1",RESP格式就是"$1\r\n1\r\n"
由於telnet視窗的原因,request和response是連著的,注意區分
使用telnet執行RESP格式的"get test1":
lhh-Mac:~ lhh$ telnet localhost 6379
Trying ::1...
Connected to localhost.
Escape character is '^]'.
*2
$3
get
$5
test1
$1
1
複製程式碼
socket方式
在手上沒有寫程式碼的條件時,使用telnet確實很方便,當編輯起來不方便.當如果用IDE的話,我們還是有更好的方式的,就是寫程式碼來測試驗證.
畢竟"talk is cheap,show me the code"嘛.
redis是基於tcp通訊的,所以簡單使用socket就好,程式碼如下:
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",6379);
OutputStream outputStream = socket.getOutputStream();
BufferedReader bufferedReader
= new BufferedReader(new InputStreamReader(socket.getInputStream()));
outputStream.write("*2\r\n$3\r\nget\r\n$5\r\ntest1\r\n".getBytes());
int num = 0;
char ch;
while((num=bufferedReader.read()) != -1){
ch = (char)num;
System.out.print(ch);
}
socket.close();
}
複製程式碼