江湖上說,天下武功,無堅不摧,唯快不破,這句話簡直是為我量身定製。
我是一個Redis服務,最引以為傲的就是我的速度,我的 QPS 能達到10萬級別。
在我的手下有數不清的小弟,他們會時不時到我這來存放或者取走一些資料,我管他們叫做客戶端,還給他們起了英文名叫 Redis-client。
有時候一個小弟會來的非常頻繁,有時候一堆小弟會同時過來,但是,即使再多的小弟我也能管理的井井有條。
有一天,小弟們問我。
想當年,為了不讓小弟們拖垮我傲人的速度,在設計和他們的通訊協議時,我絞盡腦汁,制定了下面的三條原則:
- 實現簡單
- 針對計算機來說,解析速度快
- 針對人類來說,可讀性強
為什麼這麼設計呢?先來看看一條指令發出的過程,首先在客戶端需要對指令操作進行封裝,使用網路進行傳輸,最後在服務端進行相應的解析、執行。
這一過程如果設計成一種非常複雜的協議,那麼封裝、解析、傳輸的過程都將非常耗時,無疑會降低我的速度。什麼,你問我為什麼要遵循最後一條規則?算是對於程式設計師們的饋贈吧,我真是太善良了。
我把創造出來的這種協議稱為 RESP (REdis Serialization Protocol
)協議,它工作在 TCP 協議的上層,作為我和客戶端之間進行通訊的標準形式。
說到這,我已經有點迫不及待想讓你們看看我設計出來的傑作了,但我好歹也是個大哥,得擺點架子,不能我主動拿來給你們看。
所以我建議你直接使用客戶端發出一條向伺服器的命令,然後取出這條命令對應的報文來直觀的看一下。話雖如此,不過我已經被封裝的很嚴實了,正常情況下你是看不到我內部進行通訊的具體報文的,所以,你可以偽裝成一個Redis的服務端,來截獲小弟們發給我的訊息。
實現起來也很簡單,我和小弟之間是基於 Socket 進行通訊,所以在本地先啟動一個ServerSocket
,用來監聽Redis服務的6379埠:
public static void server() throws IOException {
ServerSocket serverSocket = new ServerSocket(6379);
Socket socket = serverSocket.accept();
byte[] bytes = new byte[1024];
InputStream input = socket.getInputStream();
while(input.read(bytes)!=0){
System.out.println(new String(bytes));
}
}
然後啟動redis-cli
客戶端,傳送一條命令:
set key1 value1
這時,偽裝的服務端就會收到報文了,在控制檯列印了:
*3
$3
set
$4
key1
$6
value1
看到這裡,隱隱約約看到了剛才輸入的幾個關鍵字,但是還有一些其他的字元,要怎麼解釋呢,是時候讓我對協議報文中的格式進行一下揭祕了。
我對小弟們說了,對大哥說話的時候得按規矩來,這樣吧,你們在請求的時候要遵循下面的規則:
*<引數數量> CRLF
$<引數1的位元組長度> CRLF
<引數1的資料> CRLF
$<引數2的位元組長度> CRLF
<引數2的資料> CRLF
...
$<引數N的位元組長度> CRLF
<引數N的資料> CRLF
首先解釋一下每行末尾的CRLF
,轉換成程式語言就是\r\n
,也就是回車加換行。看到這裡,你也就能夠明白為什麼控制檯列印出的指令是豎向排列了吧。
在命令的解析過程中,set
、key1
、value1
會被認為是3個引數,因此引數數量為3,對應第一行的*3
。
第一個引數set
,長度為3對應$3
;第二個引數key1
,長度為4對應$4
;第三個引數value1
,長度為6對應$6
。在每個引數長度的下一行對應真正的引數資料。
看到這,一條指令被轉換為協議報文的過程是不是就很好理解了?
當小弟對我傳送完請求後,作為大哥,我就要對小弟的請求進行指令回覆了,而且我得根據回覆內容進行一下分類,要不然小弟該搞不清我的指示了。
簡單字串
簡單字串回覆只有一行回覆,回覆的內容以+
作為開頭,不允許換行,並以\r\n
結束。有很多指令在執行成功後只會回覆一個OK
,使用的就是這種格式,能夠有效的將傳輸、解析的開銷降到最低。
錯誤回覆
在RESP協議中,錯誤回覆可以當做簡單字串回覆的變種形式,它們之間的格式也非常類似,區別只有第一個字元是以-
作為開頭,錯誤回覆的內容通常是錯誤型別及對錯誤描述的字串。
錯誤回覆出現在一些異常的場景,例如當傳送了錯誤的指令、運算元的數量不對時,都會進行錯誤回覆。在客戶端收到錯誤回覆後,會將它與簡單字串回覆進行區分,視為異常。
整數回覆
整數回覆的應用也非常廣泛,它以:
作為開頭,以\r\n
結束,用於返回一個整數。例如當執行incr
後返回自增後的值,執行llen
返回陣列的長度,或者使用exists
命令返回的0或1作為判斷一個key
是否存在的依據,這些都使用了整數回覆。
批量回復
批量回復,就是多行字串的回覆。它以$
作為開頭,後面是傳送的位元組長度,然後是\r\n
,然後傳送實際的資料,最終以\r\n
結束。如果要回復的資料不存在,那麼回覆長度為-1。
多條批量回復
當服務端要返回多個值時,例如返回一些元素的集合時,就會使用多條批量回復。它以*
作為開頭,後面是返回元素的個數,之後再跟隨多個上面講到過的批量回復。
到這裡,基本上我和小弟之間的通訊協議就介紹完了。剛才你嘗試了偽裝成一個服務端,這會再來試一試直接寫一個客戶端來直接和我進行互動吧。
private static void client() throws IOException {
String CRLF="\r\n";
Socket socket=new Socket("localhost", 6379);
try (OutputStream out = socket.getOutputStream()) {
StringBuffer sb=new StringBuffer();
sb.append("*3").append(CRLF)
.append("$3").append(CRLF).append("set").append(CRLF)
.append("$4").append(CRLF).append("key1").append(CRLF)
.append("$6").append(CRLF).append("value1").append(CRLF);
out.write(sb.toString().getBytes());
out.flush();
try (InputStream inputStream = socket.getInputStream()) {
byte[] buff = new byte[1024];
int len = inputStream.read(buff);
if (len > 0) {
String ret = new String(buff, 0, len);
System.out.println("Recv:" + ret);
}
}
}
}
執行上面的程式碼,控制檯輸出:
Recv:+OK
上面模仿了客戶端發出set
命令的過程,並收到了回覆。依此類推,你也可以自己封裝其他的命令,來實現一個自己的Redis客戶端來和我進行通訊。
不過記住,要叫我大哥。
最後
如果覺得對您有所幫助,小夥伴們可以點贊、轉發一下~非常感謝
微信搜尋:碼農參上,來加個好友,點贊之交也好啊~
公眾號後臺回覆“面試”、“導圖”、“架構”、“實戰”,獲得免費資料哦~