通訊協議之序列化

發表於2016-11-10

通訊協議可以理解兩個節點之間為了協同工作實現資訊交換,協商一定的規則和約定,例如規定位元組序,各個欄位型別,使用什麼壓縮演算法或加密演算法等。常見的有tcp,udo,http,sip等常見協議。協議有流程規範和編碼規範。流程如呼叫流程等信令流程,編碼規範規定所有信令和資料如何打包/解包。

編碼規範就是我們通常所說的編解碼,序列化。不光是用在通訊工作上,在儲存工作上我們也經常用到。如我們經常想把記憶體中物件存放到磁碟上,就需要對物件進行資料序列化工作。

本文采用先循序漸進,先舉一個例子,然後不斷提出問題-解決完善,這樣一個迭代進化的方式,介紹一個協議逐步進化和完善,最後總結。看完之後,大家以後在工作就很容易制定和選擇自己的編碼協議。

一、緊湊模式

本文例子是A和B通訊,獲取或設定基本資料,一般開發人員第一步就是定義一個協議結構:

在這種方式下,A基本不用編碼,直接從記憶體copy出來,再把cmd做一下網路位元組序變換,傳送給B。B也能解析,一切都很和諧愉快。

這時候編碼結果可以用圖表示為(1格一個位元組)

通訊協議之序列化

這種編碼方式,我稱之為緊湊模式,意思是除了資料本身外,沒有一點額外冗餘資訊,可以看成是Raw Data。在dos年代,這種使用方式非常普遍,那時候可是記憶體和網路都是按K計算,cpu還沒有到1G。如果新增額外資訊,不光耗費捉襟見肘的cpu,連記憶體和頻寬都傷不起。

二、可擴充套件性

有一天,A在基本資料裡面加一個生日欄位,然後告訴B

這是B就犯愁了,收到A的資料包,不知道第3個欄位到底是舊協議中的name欄位,還是新協議中birthday。這是後A,和B終於從教訓中認識到一個協議重要特性——相容性和可擴充套件性

於是乎,A和B決定廢掉舊的協議,從新開始,制定一個以後每個版本相容的協議。方法很簡單,就是加一個version欄位。

這樣,A和B就鬆一口氣,以後就可以很方便的擴充套件。增加欄位也很方便。這種方法即使在現在,應該還有不少人使用。

三、更好的可擴充套件性

過了一段較長時間,A和B發現又有新的問題,就是沒增加一個欄位就改變一下版本號,這還不是重點,重點是這樣程式碼維護起來相當麻煩,每個版本一個case分支,到了最好,程式碼裡面case 幾十個分支,看起來醜陋而且維護起來成本高。

A 和 B仔細思考了一下,覺得光靠一個version維護整個協議,不夠細,於是覺得為每個欄位增加一個額外資訊——tag,雖然增加記憶體和頻寬,但是現在已經不像當年那樣,可以容許這些冗餘,換取易用性

通訊協議之序列化

制定完這些協議後,A和B很得意,覺得這個協議不錯,可以自由的增加和減少欄位。隨便擴充套件。

現實總是很殘酷的,不久就有新的需求,name使用8個位元組不夠,最大長度可能會達到100個位元組,A和B就愁懷了,總不能即使叫“steven”的人,每次都按照100個位元組打包,雖然不差錢,也不能這樣浪費。

於是A和B尋找各方資料,找到了ANS.1編碼規範,好東西啊.. ASN.1是一種ISO/ITU-T 標準。其中一種編碼BER(Basic Encoding Rules)簡單好用,它使用三元組編碼,簡稱TLV編碼。

每個欄位編碼後記憶體組織如下

clip_image006

欄位可以是結構,即可以巢狀

clip_image008

A和B使用TLV打包協議後,資料記憶體組織大概如下:

clip_image010

TLV具備了很好可擴充套件性,很簡單易學。同時也具備了缺點,因為其增加了2個額外的冗餘資訊,tag 和len,特別是如果協議大部分是基本資料型別int ,short, byte. 會浪費幾倍儲存空間。另外Value具體是什麼含義,需要通訊雙方事先得到描述文件,即TLV不具備結構化和自解釋特性。

四、自解釋性

當A和B採用TLV協議後,似乎問題都解決了。但是還是覺得不是很完美,決定增加自解釋特性,這樣抓包就能知道各個欄位型別,不用看協議描述文件。這種改進的型別就是 TT[L]V(tag,type,length,value),其中L在type是定長的基本資料型別如int,short, long, byte時候,因為其長度是已知的,所以L不需要。

於是定義了一些type值如下

%e6%8d%95%e8%8e%b7%e8%a1%a81

按照ttlv序列化後,記憶體組織如下

clip_image012

改完後,A和B發現,的確帶來很多好處,不光可以隨心所以的增刪欄位,還可以修改資料型別,例如把cmd改成int cmd;可以無縫相容。真是太給力了。

五、跨語言特性

有一天來了一個新的同事C,他寫一個新的服務,需要和A通訊,但是C是用java或PHP的語言,沒有無符號型別,導致負數解析失敗。為了解決這個問題,A重新規劃一下協議型別,做了有些剝離語言特性,定義一些共性。對使用型別做了強制性約束。雖然帶來了約束,但是帶來通用型和簡潔性,和跨語言性,大家表示都很贊同,於是有了一個型別(type)規範。

%e6%8d%95%e8%8e%b7%e8%a1%a82

六、程式碼自動化 ——IDL語言的產生

但是A和B發現了新的煩惱,就是每搞一套新的協議,都要從頭編解碼,除錯,雖然TLV很簡單,但是寫編解碼是一個毫無技術含量的枯燥體力活,一個非常明顯的問題是,由於大量copy/past,不管是對新手還是老手,非常容易犯錯,一犯錯,定位排錯非常耗時。於是A想到使用工具自動生成程式碼。

IDL(Interface Description Language),它是一種描述語言,也是一箇中間語言,IDL一個使命就是規範和約束,就像前面提到,規範使用型別,提供跨語言特性。通過工具分析idl檔案,生成各種語言程式碼

Gencpp.exe sample.idl 輸出 sample.cpp sample.h

Genphp.exe sample.idl 輸出 sample.php

Genjava.exe sample.idl 輸出 sample.java

是不是簡單高效J

七、總結

大家看到這裡,是不是覺得很面熟。是的,協議講到最後,其實就是和facebook的thrift和google protocol buffer協議大同小異了。包括公司無線使用的jce協議。咋一看這些協議的idl檔案,發現幾乎是一樣的。只是有些細小差異化。

這些協議在一些細節上增加了一些特性:

1、壓縮,這裡壓縮不是指gzip之類通用壓縮,是指標對整數壓縮,如int型別,很多情況下值是小於127(值為0的情況特別多),就不需要佔用4個位元組,所以這些協議做了一些細化處理,把int型別按照情況,只使用1/2/3/4位元組,實際上還是一種ttlv協議。

2、reuire/option 特性: 這個特性有兩個作用,1、還是壓縮,有時候一個協議很多欄位,有些欄位可以帶上也可以不帶上,不賦值的時候不是也要帶一個預設值打包,這樣很浪費,如果欄位是option特性,沒有賦值的話,就不用打包。2、有點邏輯上約束功能,規定哪些欄位必須有,加強校驗。

序列化是通訊協議的基礎,不管是信令通道還是資料通道,還是rpc,都需要使用到。在設計協議早期就考慮到擴充套件性和跨語言特性。會為以後省去不少麻煩。

Ps

本篇主要介紹二進位制通訊協議序列化,沒有講文字協議。從某種意義來講,文字協議天生具有相容和可擴充套件性。不像二進位制需要考慮那麼多問題。文字協議易於除錯(如抓包就是可見字元,telnet即可除錯,資料包可以手工生成不借助特殊工具),簡單易學是其最強大的優勢。

二進位制協議優勢就是效能和安全性。但是除錯麻煩。

兩者各有千秋,按需選擇。(stevenrao)

相關文章