Java程式設計架構深入解析-RPC訊息協議設計

歐陽慍斐發表於2018-08-17

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

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

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

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

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

訊息邊界

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

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

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

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

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

訊息的結構

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

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

訊息的隱式結構一般是指那些結構資訊由程式碼來約定的訊息協議,在 RPC 互動的訊息資料中只是純粹的二進位制資料,由程式碼來確定相應位置的二進位制是屬於哪個欄位。比如下面的這段程式碼

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

訊息壓縮

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

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

流量的極致優化

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

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

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

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

Java程式設計架構深入解析-RPC 訊息協議設計

 

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

小結

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

感謝閱讀,

迎工作一到五年的Java程式設計師朋友們加入Java架構開發:468947140

點選連結加入群聊【Java-BATJ企業級資深架構】:https://jq.qq.com/?_wv=1027&k=57Uu0p5

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導


相關文章