基本功:消息协议
消息是信息交換的主體,簡單的講,就是兩個進程約定一個協議格式。消息表示指的是序列化后的消息字節流在直觀上的表現形式,它看起來是對人類友好還是對計算機友好。文本形式對人類友好,二進制形式對計算機友好。每個消息都有其內部字段結構,結構構成了消息內部的邏輯規則,程序要按照結構規則來決定字段序列化的順序。接下來將帶你了解 RPC 的消息協議背后有哪些需要考慮的基本點。
目錄
1. 消息邊界
1.1 特殊分割符法
1.2 長度前綴法
2. 消息表示
2.1 文本消息
2.2?二進制消息
2.3?序列化協議考慮的因素
2.4 混合模式-HTTP 協議
3. 消息的結構
4. 消息壓縮
消息協議的基本原理
1. 消息邊界
RPC 需要在一條 TCP 鏈接上進行多次消息傳遞。基于 TCP 鏈接之上的單條消息如果過大,就會被網絡協議棧拆分為多個數據包進行傳送。如果消息過小,網絡協議棧可能會將多個消息組合成一個數據包進行發送。
問題:對于接收端來說它看到的只是一串串的字節數組,如果沒有明確的消息邊界規則,接收端是無從知道這一串字節數組究竟是包含多條消息還是只是某條消息的一部分?
比較常用的兩種分割方式是特殊分割符法和長度前綴法。
1.1 特殊分割符法
消息發送端在每條消息的末尾追加一個特殊的分割符,并且保證消息中間的數據不能包含特殊分割符。比如最為常見的分割符是\r\n。當接收端遍歷字節數組時發現了\r\n,就立即可以斷定\r\n?之前的字節數組是一條完整的消息。HTTP 和 Redis 協議就大量使用了\r\n?分割符。此種消息一般要求消息體的內容是文本消息。
優點
消息的可讀性比較強,可以直接看到消息的文本內容。
缺點
不適合傳遞二進制消息,因為二進制的字節數組里面很容易就冒出連續的兩個字節內容正好就是\r\n?分割符的 ascii 值。如果需要傳遞的話,一般是對二進制進行 base64 編碼轉變成普通文本消息再進行傳送。
1.2 長度前綴法
消息發送端在每條消息的開頭增加一個 4 字節長度的整數值,標記消息體的長度。這樣消息接受者首先讀取到長度信息,然后再讀取相應長度的字節數組就可以將一個完整的消息分離出來。此種消息比較常用于二進制消息。
基于長度前綴法的優點和缺點同特殊分割符法正好是相反的。長度前綴法因為適用于二進制協議,所以可讀性很差。但是對傳遞的內容本身沒有特殊限制,文本和內容皆可以傳輸,不需要進行特殊處理。HTTP 協議的 Content-Length 頭信息用來標記消息體的長度,這個也可以看成是長度前綴法的一種應用。
2. 消息表示
二進制消息和文本消息的表示方式就是我們熟悉的序列化反序列化。
使用“對象”來進行數據的操縱:
class User{
?????????std::String user_name;
?????????uint64_t user_id;
?????????uint32_t user_age;
};
User u = new User(“shenjian”);
u.setUid(123);
u.setAge(35);
但當需要對數據進行存儲或者傳輸時,“對象”就不這么好用了,往往需要把數據轉化成連續空間的“二進制字節流”,一些典型的場景是:
- 數據庫索引的磁盤存儲:數據庫的索引在內存里是b+樹,但這個格式是不能夠直接存儲到磁盤上的,所以需要把b+樹轉化為連續空間的二進制字節流,才能存儲到磁盤上
- 緩存的KV存儲:redis/memcache是KV類型的緩存,緩存存儲的value必須是連續空間的二進制字節流,而不能夠是User對象
- 數據的網絡傳輸:socket發送的數據必須是連續空間的二進制字節流,也不能是對象
所謂序列化(Serialization),就是將“對象”形態的數據轉化為“連續空間二進制字節流”形態數據的過程。這個過程的逆過程叫做反序列化。
這是一個非常細節的問題,要是讓你來把“對象”轉化為字節流,你會怎么做?
2.1 文本消息
很容易想到的就是xml(或者json)這類具有自描述特性的標記性語言:規定好轉換規則,發送方很容易把User類的一個對象序列化為xml進行信息的交換
<class name=”User”>
<element name=”user_name” type=”std::String” value=”shenjian” />
<element name=”user_id” type=”uint64_t” value=”123” />
<element name=”user_age” type=”uint32_t” value=”35” />
</class>
2.2?二進制消息
以上面的User對象為例,你可以設計一個二進制消息方式來進行序列化:整個二進制字節流共12+29+27+24=92字節。
-
第一行:序號4個字節(設0表示類名),類名長度4個字節(長度為4),接下來4個字節是類名(”User”),共12字節
-
第二行:序號4個字節(1表示第一個屬性),屬性長度4個字節(長度為9),接下來9個字節是屬性名(”user_name”),屬性值長度4個字節(長度為8),屬性值8個字節(值為”shenjian”),共29字節
-
第三行:序號4個字節(2表示第二個屬性),屬性長度4個字節(長度為7),接下來7個字節是屬性名(”user_id”),屬性值長度4個字節(長度為8),屬性值8個字節(值為123),共27字節
-
第四行:序號4個字節(3表示第三個屬性),屬性長度4個字節(長度為8),接下來8個字節是屬性名(”user_name”),屬性值長度4個字節(長度為4),屬性值4個字節(值為35),共24字節
實際的序列化協議要考慮的細節遠比這個多,例如:強類型的語言不僅要還原屬性名,屬性值,還要還原屬性類型;復雜的對象不僅要考慮普通類型,還要考慮對象嵌套類型等。無論如何,序列化的思路都是類似的。
2.3?序列化協議考慮的因素
不管使用成熟協議xml/json,還是自定義二進制協議來序列化對象,序列化協議設計時都需要考慮以下這些因素。
-
解析效率:這個應該是序列化協議應該首要考慮的因素,像xml/json解析起來比較耗時,需要解析doom樹,二進制自定義協議解析起來效率就很高
-
壓縮率,傳輸有效性:同樣一個對象,xml/json傳輸起來有大量的xml標簽,信息有效性低,二進制自定義協議占用的空間相對來說就小多了
-
擴展性與兼容性:是否能夠方便的增加字段,增加字段后舊版客戶端是否需要強制升級,都是需要考慮的問題,xml/json和上面的二進制協議都能夠方便的擴展
-
可讀性與可調試性:這個很好理解,xml/json的可讀性就比二進制協議好很多
-
跨語言:上面的兩個協議都是跨語言的,有些序列化協議是與開發語言緊密相關的,例如dubbo的序列化協議就只能支持Java的RPC調用
-
通用性:xml/json非常通用,都有很好的第三方解析庫,各個語言解析起來都十分方便,上面自定義的二進制協議雖然能夠跨語言,但每個語言都要寫一個簡易的協議客戶端
有哪些常見的序列化方式?
-
xml/json:解析效率,壓縮率都較差,擴展性、可讀性、通用性較好
-
thrift
-
protobuf:Google出品,必屬精品,各方面都不錯,強烈推薦,屬于二進制協議,可讀性差了點,但也有類似的to-string協議幫助調試問題
-
Avro
-
CORBA
2.4 混合模式-HTTP 協議
HTTP 協議是一種基于特殊分割符和長度前綴法的混合型協議。比如 HTTP 的消息頭采用的是純文本外加\r\n?分割符,而消息體則是通過消息頭中的 Content-Type 的值來決定長度。HTTP 協議雖然被稱之為文本傳輸協議,但是也可以在消息體中傳輸二進制數據數據的,例如音視頻圖像,所以 HTTP 協議被稱之為「超文本」傳輸協議。
3. 消息的結構
每條消息都有它包含的語義結構信息,有些消息協議的結構信息是顯式的,還有些是隱式的。
顯式
比如 json 消息,它的結構就可以直接通過它的內容體現出來,所以它是一種顯式結構的消息協議。json 這種直觀的消息協議的可讀性非常棒,但是它的缺點也很明顯,有太多的冗余信息。
隱式
消息的隱式結構一般是指那些結構信息由代碼來約定的消息協議,在 RPC 交互的消息數據中只是純粹的二進制數據,由代碼來確定相應位置的二進制是屬于哪個字段。
消息的結構在同一條消息通道上是可以復用的,比如在建立鏈接的開始 RPC 客戶端和服務器之間先交流協商一下消息的結構,后續發送消息時只需要發送一系列消息的 value 值,接收端會自動將 value 值和相應位置的 key 關聯起來,形成一個完成的結構消息。在 Hadoop 系統中廣泛使用的 avro 消息協議就是通過這種方式實現的,在 RPC 鏈接建立之處就開始交流消息的結構,后續消息的傳遞就可以節省很多流量。
如果純粹看消息內容是無法知道節點消息內容中的哪些字節的含義,它的消息結構是通過代碼的結構順序來確定的。這種隱式的消息的優點就在于節省傳輸流量,它完全不需要傳輸結構信息。
4. 消息壓縮
如果消息的內容太大,就要考慮對消息進行壓縮處理,這可以減輕網絡帶寬壓力。但是這同時也會加重 CPU 的負擔,因為壓縮算法是 CPU 計算密集型操作,會導致操作系統的負載加重。所以,最終是否進行消息壓縮,一定要根據業務情況加以權衡。
更多關于壓縮之前也講過。
總結
- 上一篇: 电脑上如何进行MP4格式转换成其它格式?
- 下一篇: 【程序源代码】小电影小程序