當前位置:
首頁 > 最新 > 深入理解 RPC 消息協議設計

深入理解 RPC 消息協議設計

本節我們開始講解 RPC 的消息協議設計背後的基本原理,了解 RPC 的協議開發背後有哪些需要考慮的基本點。在通曉原理之後,我們就可以自己設計一套協議來開發屬於自己的 RPC 系統。

本節主要涉及的知識點和它們之見的關係如下圖:

對於一串消息流,我們必須能確定消息邊界,提取出單條消息的位元組流片段,然後對這個片段按照一定的規則進行反序列化來生成相應的消息對象。

消息表示指的是序列化後的消息位元組流在直觀上的表現形式,它看起來是對人類友好還是對計算機友好。文本形式對人類友好,二進位形式對計算機友好。

每個消息都有其內部欄位結構,結構構成了消息內部的邏輯規則,程序要按照結構規則來決定欄位序列化的順序。

接下來,我們初步詳細拆解。

RPC 需要在一條 TCP 鏈接上進行多次消息傳遞。在連續的兩條消息之間必須有明確的分割規則,以便接收端可以將消息分割開來,這裡的接收端可以是 RPC 伺服器接收請求,也可以是 RPC 客戶端接收響應。

基於 TCP 鏈接之上的單條消息如果過大,就會被網路協議棧拆分為多個數據包進行傳送。如果消息過小,網路協議棧可能會將多個消息組合成一個數據包進行發送。對於接收端來說它看到的只是一串串的位元組數組,如果沒有明確的消息邊界規則,接收端是無從知道這一串位元組數組究竟是包含多條消息還是只是某條消息的一部分。

比較常用的兩種分割方式是特殊分割符法和長度前綴法。

消息發送端在每條消息的末尾追加一個特殊的分割符,並且保證消息中間的數據不能包含特殊分割符。比如最為常見的分割符是。當接收端遍歷位元組數組時發現了,就立即可以斷定 之前的位元組數組是一條完整的消息,可以傳遞到上層邏輯繼續進行處理。HTTP 和 Redis 協議就大量使用了 分割符。此種消息一般要求消息體的內容是文本消息。

消息發送端在每條消息的開頭增加一個 4 位元組長度的整數值,標記消息體的長度。這樣消息接受者首先讀取到長度信息,然後再讀取相應長度的位元組數組就可以將一個完整的消息分離出來。此種消息比較常用於二進位消息。

基於特殊分割符法的優點在於消息的可讀性比較強,可以直接看到消息的文本內容,缺點是不適合傳遞二進位消息,因為二進位的位元組數組裡面很容易就冒出連續的兩個位元組內容正好就是 分割符的 ascii 值。如果需要傳遞的話,一般是對二進位進行 base64 編碼轉變成普通文本消息再進行傳送。

基於長度前綴法的優點和缺點同特殊分割符法正好是相反的。長度前綴法因為適用於二進位協議,所以可讀性很差。但是對傳遞的內容本身沒有特殊限制,文本和內容皆可以傳輸,不需要進行特殊處理。HTTP 協議的 Content-Length 頭信息用來標記消息體的長度,這個也可以看成是長度前綴法的一種應用。

HTTP 協議是一種基於特殊分割符和長度前綴法的混合型協議。比如 HTTP 的消息頭採用的是純文本外加 分割符,而消息體則是通過消息頭中的 Content-Type 的值來決定長度。HTTP 協議雖然被稱之為文本傳輸協議,但是也可以在消息體中傳輸二進位數據數據的,例如音視頻圖像,所以 HTTP 協議被稱之為「超文本」傳輸協議。


每條消息都有它包含的語義結構信息,有些消息協議的結構信息是顯式的,還有些是隱式的。比如 json 消息,它的結構就可以直接通過它的內容體現出來,所以它是一種顯式結構的消息協議。

json 這種直觀的消息協議的可讀性非常棒,但是它的缺點也很明顯,有太多的冗餘信息。比如每個字元串都使用雙引號來界定邊界,key/value 之間必須有冒號分割,對象之間必須使用大括弧分割等等。這些還只是冗餘的小頭,最大的冗餘還在於連續的多條 json 消息即使結構完全一樣,僅僅只是 value 的值不一樣,也需要發送同樣的 key 字元串信息。

消息的結構在同一條消息通道上是可以復用的,比如在建立鏈接的開始 RPC 客戶端和伺服器之間先交流協商一下消息的結構,後續發送消息時只需要發送一系列消息的 value 值,接收端會自動將 value 值和相應位置的 key 關聯起來,形成一個完成的結構消息。在 Hadoop 系統中廣泛使用的 avro 消息協議就是通過這種方式實現的,在 RPC 鏈接建立之處就開始交流消息的結構,後續消息的傳遞就可以節省很多流量。

消息的隱式結構一般是指那些結構信息由代碼來約定的消息協議,在 RPC 交互的消息數據中只是純粹的二進位數據,由代碼來確定相應位置的二進位是屬於哪個欄位。比如下面的這段代碼

如果純粹看消息內容是無法知道節點消息內容中的哪些位元組的含義,它的消息結構是通過代碼的結構順序來確定的。這種隱式的消息的優點就在於節省傳輸流量,它完全不需要傳輸結構信息。


如果消息的內容太大,就要考慮對消息進行壓縮處理,這可以減輕網路帶寬壓力。但是這同時也會加重 CPU 的負擔,因為壓縮演算法是 CPU 計算密集型操作,會導致操作系統的負載加重。所以,最終是否進行消息壓縮,一定要根據業務情況加以權衡。

如果確定壓縮,那麼在選擇壓縮演算法包時,務必挑選那些底層用 C 語言實現的演算法庫,因為 Python 的位元組碼執行起來太慢了。比較流行的消息壓縮演算法有 Google 的 snappy 演算法,它的運行性能非常好,壓縮比例雖然不是最優的,但是離最優的差距已經不是很大。阿里的 SOFA RPC 就使用了 snappy 作為協議層壓縮演算法。


開源的流行 RPC 消息協議往往對消息流量優化到了極致,它們通過這種方式來打動用戶,吸引用戶來使用它們。比如對於一個整形數字,一般使用 4 個位元組來表示一個整數值。

但是經過研究發現,消息傳遞中大部分使用的整數值都是很小的非負整數,如果全部使用 4 個位元組來表示一個整數會很浪費。所以就發明了一個類型叫變長整數varint。數值非常小時,只需要使用一個位元組來存儲,數值稍微大一點可以使用 2 個位元組,再大一點就是 3 個位元組,它還可以超過 4 個位元組用來表達長整形數字。

其原理也很簡單,就是保留每個位元組的最高位的 bit 來標識是否後面還有位元組,1 表示還有位元組需要繼續讀,0 表示到讀到當前位元組就結束。

那如果是負數該怎麼辦呢?-1 的 16 進位數是 0xFFFFFFFF,如果要按照這個編碼那豈不是要 6 個位元組才能存的下。-1 也是非常常見的整數啊。

於是 zigzag 編碼來了,專門用來解決負數問題。zigzag 編碼將整數範圍一一映射到自然數範圍,然後再進行 varint 編碼。

zigzag 將負數編碼成正奇數,正數編碼成偶數。解碼的時候遇到偶數直接除 2 就是原值,遇到奇數就加 1 除 2 再取負就是原值。


現在我們知道了 RPC 消息結構的設計原理,遵循這些基本方法,就可以創造出一個又一個不同的消息協議。

下一節我們將講一個具體的實例,拿市場上應用最廣的開源存儲中間件 Redis 的消息協議為例進行詳細分析。


請讀者自己實現一下 varint 和 zigzag 編碼轉換器,不要求讀者實現的特別高效,但應當實現基本的輸入輸出。

另,這個作業的代碼實現會涉及到不少 Python 的位操作知識,可以溫故而知新。

掃一掃關注公眾號「碼洞」


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 碼洞 的精彩文章:

TAG:碼洞 |