RESP是REdis Serialization Protocol的簡稱,也就是專門爲redis設計的一套序列化協議. 這個協議其實在redis的1.2版本時就已經出現了,可是到了redis2.0才最終成爲redis通信協議的標準redis
這個序列化協議聽起來很高大上, 但實際上就是一個文本協議.根據官方的說法, 這個協議是基於如下幾點(而妥協)設計的:json
1. 實現簡單.能夠減低客戶端出現bug的機率
2. 解析速度快.因爲RESP能知道返回數據的固定長度,因此不用像json那樣掃描整個payload去解析, 因此它的性能是能跟解析二進制數據的性能相媲美的.
3. 可讀性好.
複製代碼
其實RESP是個很簡單的東西,不用一天就能吃透. 可是我對它的認識一直都停留在一個很模糊的狀態, 以前只知道它返回的不一樣的類型是以不一樣的符號開始的,具體是什麼沒有仔細去深究.數組
直到前幾天遇到一個bug, 調試redis客戶端的時候發現對redis的返回內容特別陌生. 今天在看AOF文件時又遇到了它,才忽然悟到:書到用時方恨少啊bash
因而就有這一篇博客.併發
總結來講,RESP的應用場景有:socket
1. 開發定製化的客戶端. RESP設計成簡單的文本協議, 一大緣由就是爲了下降各類語言開發客戶端的複雜度
2. 理解RESP方便咱們分析AOF文件,瞭解redis的內部設計
3. 平時經過抓包軟件,能夠幫助快速定位redis的相關問題
4. 在沒有redis-cli的狀況下, 方便開發調試redis命令
複製代碼
通常來講,RESP只須要序列化三種數組便可: 字符串, 整數, 數組. 而在實際場景中, RESP又把字符串細化成了simple string, error string和bulk string三種.tcp
因此RESP一共涉及到5種數據類型:編輯器
1. simple string. 簡單的字符串
2. error. 就是表示這是一個錯誤(異常)狀況
3. integer 表示這是一個整數
4. bulk string. 表示是長字符串,可是必須小於512M.
5. arrays. 表示這是一個數組,數組元素能夠是上面的任意一種類型,也能夠是一個數組
複製代碼
像一些高級語言用int long等來表示不一樣數據類型同樣, RESP也有它本身標識不一樣數據類型的"語法", 就是用第一個字節的符號來表示不一樣的數據類型:性能
"+OK\r\n"
複製代碼
"-ERR unknown command 'foobar'\r\n"
複製代碼
":1000\r\n"
複製代碼
"$0\r\n" --$後面的0表示這是一個空字符串
"$-1\r\n" -- $後面的-1表示這是一個null字符串,Null Bulk String要求客戶端返回空對象,而不能簡單地返回個空字符串
"$6\r\nABCDEF\r\n" -- ABCDEF是6個字節,因此$後面是6
複製代碼
"*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元素 "
複製代碼
通常來講,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是什麼以後, 咱們一般都會想動手驗證一下,它實際的運行是否跟理論一致. 這個時候有兩種方法.
當咱們手上沒有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
複製代碼
在手上沒有寫代碼的條件時, 使用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();
}
複製代碼