1. 程式人生 > 程式設計 >redis通訊協議(RESP )是什麼

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也有它自己標識不同資料型別的"語法",就是用第一個位元組的符號來表示不同的資料型別:

  1. simple string 的第一個位元組是個"+"(加號),後面接著的是字串的內容,最後以CRLF(\r\n)結尾.例如:
"+OK\r\n"
複製程式碼
  1. error. error其實和string是類似的,但是RESP為了能讓不同客戶端把這種error和正常的返回結果區分開來對待 (例如redis返回error的話,就丟擲異常),特意多設計了這個資料型別. error型別的第一個位元組是"-"(減號),後面接著的是錯誤的資訊,最後以CRLF(\r\n)結尾,例如:
"-ERR unknown command 'foobar'\r\n"
複製程式碼
  1. integer 型別的第一個位元組是":"(冒號),後面接著的是整數,例如:
 ":1000\r\n"
複製程式碼
  1. 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
複製程式碼
  1. 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();
  }
複製程式碼

參考 redis.io/topics/prot…