Redis:我是如何與客戶端進行通訊的

江湖上說,天下武功,無堅不摧,惟快不破,這句話簡直是爲我量身定製。java

我是一個Redis服務,最引覺得傲的就是個人速度,個人 QPS 能達到10萬級別。程序員

在個人手下有數不清的小弟,他們會時不時到我這來存放或者取走一些數據,我管他們叫作客戶端,還給他們起了英文名叫 Redis-client。redis

有時候一個小弟會來的很是頻繁,有時候一堆小弟會同時過來,可是,即便再多的小弟我也能管理的層次分明。shell

有一天,小弟們問我。數組

想當年,爲了避免讓小弟們拖垮我傲人的速度,在設計和他們的通訊協議時,我絞盡腦汁,制定了下面的三條原則:服務器

  • 實現簡單
  • 針對計算機來講,解析速度快
  • 針對人類來講,可讀性強

爲何這麼設計呢?先來看看一條指令發出的過程,首先在客戶端須要對指令操做進行封裝,使用網絡進行傳輸,最後在服務端進行相應的解析、執行。網絡

這一過程若是設計成一種很是複雜的協議,那麼封裝、解析、傳輸的過程都將很是耗時,無疑會下降個人速度。什麼,你問我爲何要遵循最後一條規則?算是對於程序員們的饋贈吧,我真是太善良了。app

我把創造出來的這種協議稱爲 RESP (REdis Serialization Protocol)協議,它工做在 TCP 協議的上層,做爲我和客戶端之間進行通信的標準形式。socket

說到這,我已經有點火燒眉毛想讓大家看看我設計出來的傑做了,但我好歹也是個大哥,得擺點架子,不能我主動拿來給大家看。設計

因此我建議你直接使用客戶端發出一條向服務器的命令,而後取出這條命令對應的報文來直觀的看一下。話雖如此,不過我已經被封裝的很嚴實了,正常狀況下你是看不到我內部進行通信的具體報文的,因此,你能夠假裝成一個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,也就是回車加換行。看到這裏,你也就可以明白爲何控制檯打印出的指令是豎向排列了吧。

在命令的解析過程當中,setkey1value1會被認爲是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客戶端來和我進行通訊。

不過記住,要叫我大哥。


若是文章對您有所幫助,歡迎關注公衆號 碼農參上

第一時間收到更新。

相關文章
相關標籤/搜索