更新時間:2018年10月24日15時39分 來源:傳智播客 瀏覽次數(shù):
本節(jié)我們開始講解 RPC 的消息協(xié)議設(shè)計背后的基本原理,了解 RPC 的協(xié)議開發(fā)背后有哪些需要考慮的基本點。在通曉原理之后,我們就可以自己設(shè)計一套協(xié)議來開發(fā)屬于自己的 RPC 系統(tǒng)。
本節(jié)主要涉及的知識點和它們之見的關(guān)系如下圖:
對于一串消息流,我們必須能確定消息邊界,提取出單條消息的字節(jié)流片段,然后對這個片段按照一定的規(guī)則進行反序列化來生成相應(yīng)的消息對象。
消息表示指的是序列化后的消息字節(jié)流在直觀上的表現(xiàn)形式,它看起來是對人類友好還是對計算機友好。文本形式對人類友好,二進制形式對計算機友好。
每個消息都有其內(nèi)部字段結(jié)構(gòu),結(jié)構(gòu)構(gòu)成了消息內(nèi)部的邏輯規(guī)則,程序要按照結(jié)構(gòu)規(guī)則來決定字段序列化的順序。
接下來,我們初步詳細拆解。
消息邊界
RPC 需要在一條 TCP 鏈接上進行多次消息傳遞。在連續(xù)的兩條消息之間必須有明確的分割規(guī)則,以便接收端可以將消息分割開來,這里的接收端可以是 RPC 服務(wù)器接收請求,也可以是 RPC 客戶端接收響應(yīng)。
基于 TCP 鏈接之上的單條消息如果過大,就會被網(wǎng)絡(luò)協(xié)議棧拆分為多個數(shù)據(jù)包進行傳送。如果消息過小,網(wǎng)絡(luò)協(xié)議??赡軙⒍鄠€消息組合成一個數(shù)據(jù)包進行發(fā)送。對于接收端來說它看到的只是一串串的字節(jié)數(shù)組,如果沒有明確的消息邊界規(guī)則,接收端是無從知道這一串字節(jié)數(shù)組究竟是包含多條消息還是只是某條消息的一部分。
比較常用的兩種分割方式是特殊分割符法和長度前綴法。
消息發(fā)送端在每條消息的末尾追加一個特殊的分割符,并且保證消息中間的數(shù)據(jù)不能包含特殊分割符。比如最為常見的分割符是 。當(dāng)接收端遍歷字節(jié)數(shù)組時發(fā)現(xiàn)了 ,就立即可以斷定 之前的字節(jié)數(shù)組是一條完整的消息,可以傳遞到上層邏輯繼續(xù)進行處理。HTTP 和 Redis 協(xié)議就大量使用了 分割符。此種消息一般要求消息體的內(nèi)容是文本消息。
消息發(fā)送端在每條消息的開頭增加一個 4 字節(jié)長度的整數(shù)值,標記消息體的長度。這樣消息接受者首先讀取到長度信息,然后再讀取相應(yīng)長度的字節(jié)數(shù)組就可以將一個完整的消息分離出來。此種消息比較常用于二進制消息。
基于特殊分割符法的優(yōu)點在于消息的可讀性比較強,可以直接看到消息的文本內(nèi)容,缺點是不適合傳遞二進制消息,因為二進制的字節(jié)數(shù)組里面很容易就冒出連續(xù)的兩個字節(jié)內(nèi)容正好就是 分割符的 ascii 值。如果需要傳遞的話,一般是對二進制進行 base64 編碼轉(zhuǎn)變成普通文本消息再進行傳送。
基于長度前綴法的優(yōu)點和缺點同特殊分割符法正好是相反的。長度前綴法因為適用于二進制協(xié)議,所以可讀性很差。但是對傳遞的內(nèi)容本身沒有特殊限制,文本和內(nèi)容皆可以傳輸,不需要進行特殊處理。HTTP 協(xié)議的 Content-Length 頭信息用來標記消息體的長度,這個也可以看成是長度前綴法的一種應(yīng)用。
HTTP 協(xié)議是一種基于特殊分割符和長度前綴法的混合型協(xié)議。比如 HTTP 的消息頭采用的是純文本外加 分割符,而消息體則是通過消息頭中的 Content-Type 的值來決定長度。HTTP 協(xié)議雖然被稱之為文本傳輸協(xié)議,但是也可以在消息體中傳輸二進制數(shù)據(jù)數(shù)據(jù)的,例如音視頻圖像,所以 HTTP 協(xié)議被稱之為「超文本」傳輸協(xié)議。
消息的結(jié)構(gòu)
每條消息都有它包含的語義結(jié)構(gòu)信息,有些消息協(xié)議的結(jié)構(gòu)信息是顯式的,還有些是隱式的。比如 json 消息,它的結(jié)構(gòu)就可以直接通過它的內(nèi)容體現(xiàn)出來,所以它是一種顯式結(jié)構(gòu)的消息協(xié)議。
json 這種直觀的消息協(xié)議的可讀性非常棒,但是它的缺點也很明顯,有太多的冗余信息。比如每個字符串都使用雙引號來界定邊界,key/value 之間必須有冒號分割,對象之間必須使用大括號分割等等。這些還只是冗余的小頭,最大的冗余還在于連續(xù)的多條 json 消息即使結(jié)構(gòu)完全一樣,僅僅只是 value 的值不一樣,也需要發(fā)送同樣的 key 字符串信息。
消息的結(jié)構(gòu)在同一條消息通道上是可以復(fù)用的,比如在建立鏈接的開始 RPC 客戶端和服務(wù)器之間先交流協(xié)商一下消息的結(jié)構(gòu),后續(xù)發(fā)送消息時只需要發(fā)送一系列消息的 value 值,接收端會自動將 value 值和相應(yīng)位置的 key 關(guān)聯(lián)起來,形成一個完成的結(jié)構(gòu)消息。在 Hadoop 系統(tǒng)中廣泛使用的 avro 消息協(xié)議就是通過這種方式實現(xiàn)的,在 RPC 鏈接建立之處就開始交流消息的結(jié)構(gòu),后續(xù)消息的傳遞就可以節(jié)省很多流量。
消息的隱式結(jié)構(gòu)一般是指那些結(jié)構(gòu)信息由代碼來約定的消息協(xié)議,在 RPC 交互的消息數(shù)據(jù)中只是純粹的二進制數(shù)據(jù),由代碼來確定相應(yīng)位置的二進制是屬于哪個字段。比如下面的這段代碼
如果純粹看消息內(nèi)容是無法知道節(jié)點消息內(nèi)容中的哪些字節(jié)的含義,它的消息結(jié)構(gòu)是通過代碼的結(jié)構(gòu)順序來確定的。這種隱式的消息的優(yōu)點就在于節(jié)省傳輸流量,它完全不需要傳輸結(jié)構(gòu)信息。
消息壓縮
如果消息的內(nèi)容太大,就要考慮對消息進行壓縮處理,這可以減輕網(wǎng)絡(luò)帶寬壓力。但是這同時也會加重 CPU 的負擔(dān),因為壓縮算法是 CPU 計算密集型操作,會導(dǎo)致操作系統(tǒng)的負載加重。所以,最終是否進行消息壓縮,一定要根據(jù)業(yè)務(wù)情況加以權(quán)衡。
如果確定壓縮,那么在選擇壓縮算法包時,務(wù)必挑選那些底層用 C 語言實現(xiàn)的算法庫,因為 Python 的字節(jié)碼執(zhí)行起來太慢了。比較流行的消息壓縮算法有 Google 的 snappy 算法,它的運行性能非常好,壓縮比例雖然不是最優(yōu)的,但是離最優(yōu)的差距已經(jīng)不是很大。阿里的 SOFA RPC 就使用了 snappy 作為協(xié)議層壓縮算法。
流量的極致優(yōu)化
開源的流行 RPC 消息協(xié)議往往對消息流量優(yōu)化到了極致,它們通過這種方式來打動用戶,吸引用戶來使用它們。比如對于一個整形數(shù)字,一般使用 4 個字節(jié)來表示一個整數(shù)值。
但是經(jīng)過研究發(fā)現(xiàn),消息傳遞中大部分使用的整數(shù)值都是很小的非負整數(shù),如果全部使用 4 個字節(jié)來表示一個整數(shù)會很浪費。所以就發(fā)明了一個類型叫變長整數(shù)varint。數(shù)值非常小時,只需要使用一個字節(jié)來存儲,數(shù)值稍微大一點可以使用 2 個字節(jié),再大一點就是 3 個字節(jié),它還可以超過 4 個字節(jié)用來表達長整形數(shù)字。
其原理也很簡單,就是保留每個字節(jié)的最高位的 bit 來標識是否后面還有字節(jié),1 表示還有字節(jié)需要繼續(xù)讀,0 表示到讀到當(dāng)前字節(jié)就結(jié)束。
那如果是負數(shù)該怎么辦呢?-1 的 16 進制數(shù)是 0xFFFFFFFF,如果要按照這個編碼那豈不是要 6 個字節(jié)才能存的下。-1 也是非常常見的整數(shù)啊。
于是 zigzag 編碼來了,專門用來解決負數(shù)問題。zigzag 編碼將整數(shù)范圍一一映射到自然數(shù)范圍,然后再進行 varint 編碼。
zigzag 將負數(shù)編碼成正奇數(shù),正數(shù)編碼成偶數(shù)。解碼的時候遇到偶數(shù)直接除 2 就是原值,遇到奇數(shù)就加 1 除 2 再取負就是原值。