教育行業(yè)A股IPO第一股(股票代碼 003032)

全國(guó)咨詢/投訴熱線:400-618-4000

深入理解 RPC 消息協(xié)議設(shè)計(jì)

更新時(shí)間:2018年10月24日15時(shí)39分 來源:傳智播客 瀏覽次數(shù):

  本節(jié)我們開始講解 RPC 的消息協(xié)議設(shè)計(jì)背后的基本原理,了解 RPC 的協(xié)議開發(fā)背后有哪些需要考慮的基本點(diǎn)。在通曉原理之后,我們就可以自己設(shè)計(jì)一套協(xié)議來開發(fā)屬于自己的 RPC 系統(tǒng)。

  本節(jié)主要涉及的知識(shí)點(diǎn)和它們之見的關(guān)系如下圖:

  

  對(duì)于一串消息流,我們必須能確定消息邊界,提取出單條消息的字節(jié)流片段,然后對(duì)這個(gè)片段按照一定的規(guī)則進(jìn)行反序列化來生成相應(yīng)的消息對(duì)象。

  消息表示指的是序列化后的消息字節(jié)流在直觀上的表現(xiàn)形式,它看起來是對(duì)人類友好還是對(duì)計(jì)算機(jī)友好。文本形式對(duì)人類友好,二進(jìn)制形式對(duì)計(jì)算機(jī)友好。

  每個(gè)消息都有其內(nèi)部字段結(jié)構(gòu),結(jié)構(gòu)構(gòu)成了消息內(nèi)部的邏輯規(guī)則,程序要按照結(jié)構(gòu)規(guī)則來決定字段序列化的順序。

  接下來,我們初步詳細(xì)拆解。

  消息邊界

  RPC 需要在一條 TCP 鏈接上進(jìn)行多次消息傳遞。在連續(xù)的兩條消息之間必須有明確的分割規(guī)則,以便接收端可以將消息分割開來,這里的接收端可以是 RPC 服務(wù)器接收請(qǐng)求,也可以是 RPC 客戶端接收響應(yīng)。

  基于 TCP 鏈接之上的單條消息如果過大,就會(huì)被網(wǎng)絡(luò)協(xié)議棧拆分為多個(gè)數(shù)據(jù)包進(jìn)行傳送。如果消息過小,網(wǎng)絡(luò)協(xié)議??赡軙?huì)將多個(gè)消息組合成一個(gè)數(shù)據(jù)包進(jìn)行發(fā)送。對(duì)于接收端來說它看到的只是一串串的字節(jié)數(shù)組,如果沒有明確的消息邊界規(guī)則,接收端是無從知道這一串字節(jié)數(shù)組究竟是包含多條消息還是只是某條消息的一部分。

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

  

  消息發(fā)送端在每條消息的末尾追加一個(gè)特殊的分割符,并且保證消息中間的數(shù)據(jù)不能包含特殊分割符。比如最為常見的分割符是 。當(dāng)接收端遍歷字節(jié)數(shù)組時(shí)發(fā)現(xiàn)了 ,就立即可以斷定 之前的字節(jié)數(shù)組是一條完整的消息,可以傳遞到上層邏輯繼續(xù)進(jìn)行處理。HTTP 和 Redis 協(xié)議就大量使用了 分割符。此種消息一般要求消息體的內(nèi)容是文本消息。

  

  消息發(fā)送端在每條消息的開頭增加一個(gè) 4 字節(jié)長(zhǎng)度的整數(shù)值,標(biāo)記消息體的長(zhǎng)度。這樣消息接受者首先讀取到長(zhǎng)度信息,然后再讀取相應(yīng)長(zhǎng)度的字節(jié)數(shù)組就可以將一個(gè)完整的消息分離出來。此種消息比較常用于二進(jìn)制消息。

  基于特殊分割符法的優(yōu)點(diǎn)在于消息的可讀性比較強(qiáng),可以直接看到消息的文本內(nèi)容,缺點(diǎn)是不適合傳遞二進(jìn)制消息,因?yàn)槎M(jìn)制的字節(jié)數(shù)組里面很容易就冒出連續(xù)的兩個(gè)字節(jié)內(nèi)容正好就是 分割符的 ascii 值。如果需要傳遞的話,一般是對(duì)二進(jìn)制進(jìn)行 base64 編碼轉(zhuǎn)變成普通文本消息再進(jìn)行傳送。

  基于長(zhǎng)度前綴法的優(yōu)點(diǎn)和缺點(diǎn)同特殊分割符法正好是相反的。長(zhǎng)度前綴法因?yàn)檫m用于二進(jìn)制協(xié)議,所以可讀性很差。但是對(duì)傳遞的內(nèi)容本身沒有特殊限制,文本和內(nèi)容皆可以傳輸,不需要進(jìn)行特殊處理。HTTP 協(xié)議的 Content-Length 頭信息用來標(biāo)記消息體的長(zhǎng)度,這個(gè)也可以看成是長(zhǎng)度前綴法的一種應(yīng)用。

  

  HTTP 協(xié)議是一種基于特殊分割符和長(zhǎng)度前綴法的混合型協(xié)議。比如 HTTP 的消息頭采用的是純文本外加 分割符,而消息體則是通過消息頭中的 Content-Type 的值來決定長(zhǎng)度。HTTP 協(xié)議雖然被稱之為文本傳輸協(xié)議,但是也可以在消息體中傳輸二進(jìn)制數(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é)議的可讀性非常棒,但是它的缺點(diǎn)也很明顯,有太多的冗余信息。比如每個(gè)字符串都使用雙引號(hào)來界定邊界,key/value 之間必須有冒號(hào)分割,對(duì)象之間必須使用大括號(hào)分割等等。這些還只是冗余的小頭,最大的冗余還在于連續(xù)的多條 json 消息即使結(jié)構(gòu)完全一樣,僅僅只是 value 的值不一樣,也需要發(fā)送同樣的 key 字符串信息。

  消息的結(jié)構(gòu)在同一條消息通道上是可以復(fù)用的,比如在建立鏈接的開始 RPC 客戶端和服務(wù)器之間先交流協(xié)商一下消息的結(jié)構(gòu),后續(xù)發(fā)送消息時(shí)只需要發(fā)送一系列消息的 value 值,接收端會(huì)自動(dòng)將 value 值和相應(yīng)位置的 key 關(guān)聯(lián)起來,形成一個(gè)完成的結(jié)構(gòu)消息。在 Hadoop 系統(tǒng)中廣泛使用的 avro 消息協(xié)議就是通過這種方式實(shí)現(xiàn)的,在 RPC 鏈接建立之處就開始交流消息的結(jié)構(gòu),后續(xù)消息的傳遞就可以節(jié)省很多流量。

  消息的隱式結(jié)構(gòu)一般是指那些結(jié)構(gòu)信息由代碼來約定的消息協(xié)議,在 RPC 交互的消息數(shù)據(jù)中只是純粹的二進(jìn)制數(shù)據(jù),由代碼來確定相應(yīng)位置的二進(jìn)制是屬于哪個(gè)字段。比如下面的這段代碼

  

  如果純粹看消息內(nèi)容是無法知道節(jié)點(diǎn)消息內(nèi)容中的哪些字節(jié)的含義,它的消息結(jié)構(gòu)是通過代碼的結(jié)構(gòu)順序來確定的。這種隱式的消息的優(yōu)點(diǎn)就在于節(jié)省傳輸流量,它完全不需要傳輸結(jié)構(gòu)信息。

  消息壓縮

  如果消息的內(nèi)容太大,就要考慮對(duì)消息進(jìn)行壓縮處理,這可以減輕網(wǎng)絡(luò)帶寬壓力。但是這同時(shí)也會(huì)加重 CPU 的負(fù)擔(dān),因?yàn)閴嚎s算法是 CPU 計(jì)算密集型操作,會(huì)導(dǎo)致操作系統(tǒng)的負(fù)載加重。所以,最終是否進(jìn)行消息壓縮,一定要根據(jù)業(yè)務(wù)情況加以權(quán)衡。

  如果確定壓縮,那么在選擇壓縮算法包時(shí),務(wù)必挑選那些底層用 C 語言實(shí)現(xiàn)的算法庫,因?yàn)?Python 的字節(jié)碼執(zhí)行起來太慢了。比較流行的消息壓縮算法有 Google 的 snappy 算法,它的運(yùn)行性能非常好,壓縮比例雖然不是最優(yōu)的,但是離最優(yōu)的差距已經(jīng)不是很大。阿里的 SOFA RPC 就使用了 snappy 作為協(xié)議層壓縮算法。

  流量的極致優(yōu)化

  開源的流行 RPC 消息協(xié)議往往對(duì)消息流量?jī)?yōu)化到了極致,它們通過這種方式來打動(dòng)用戶,吸引用戶來使用它們。比如對(duì)于一個(gè)整形數(shù)字,一般使用 4 個(gè)字節(jié)來表示一個(gè)整數(shù)值。

  但是經(jīng)過研究發(fā)現(xiàn),消息傳遞中大部分使用的整數(shù)值都是很小的非負(fù)整數(shù),如果全部使用 4 個(gè)字節(jié)來表示一個(gè)整數(shù)會(huì)很浪費(fèi)。所以就發(fā)明了一個(gè)類型叫變長(zhǎng)整數(shù)varint。數(shù)值非常小時(shí),只需要使用一個(gè)字節(jié)來存儲(chǔ),數(shù)值稍微大一點(diǎn)可以使用 2 個(gè)字節(jié),再大一點(diǎn)就是 3 個(gè)字節(jié),它還可以超過 4 個(gè)字節(jié)用來表達(dá)長(zhǎng)整形數(shù)字。

  其原理也很簡(jiǎn)單,就是保留每個(gè)字節(jié)的最高位的 bit 來標(biāo)識(shí)是否后面還有字節(jié),1 表示還有字節(jié)需要繼續(xù)讀,0 表示到讀到當(dāng)前字節(jié)就結(jié)束。

  

  那如果是負(fù)數(shù)該怎么辦呢?-1 的 16 進(jìn)制數(shù)是 0xFFFFFFFF,如果要按照這個(gè)編碼那豈不是要 6 個(gè)字節(jié)才能存的下。-1 也是非常常見的整數(shù)啊。

  于是 zigzag 編碼來了,專門用來解決負(fù)數(shù)問題。zigzag 編碼將整數(shù)范圍一一映射到自然數(shù)范圍,然后再進(jìn)行 varint 編碼。

  

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



作者:傳智播客JavaEE培訓(xùn)學(xué)院
首發(fā):http://java.itcast.cn/

0 分享到:
和我們?cè)诰€交談!