stevenrao——2012-07-07于深圳
通信協定可以了解兩個節點之間為了協同工作實作資訊交換,協商一定的規則和約定,例如規定位元組序,各個字段類型,使用什麼壓縮算法或加密算法等。常見的有tcp,udo,http,sip等常見協定。協定有流程規範和編碼規範。流程如呼叫流程等信令流程,編碼規範規定所有信令和資料如何打包/解包。
編碼規範就是我們通常所說的編解碼,序列化。不光是用在通信工作上,在存儲工作上我們也經常用到。如我們經常想把記憶體中對象存放到磁盤上,就需要對對象進行資料序列化工作。
本文采用先循序漸進,先舉一個例子,然後不斷提出問題-解決完善,這樣一個疊代進化的方式,介紹一個協定逐漸進化和完善,最後總結。看完之後,大家以後在工作就很容易制定和選擇自己的編碼協定。
一、緊湊模式
本文例子是A和B通信,擷取或設定基本資料,一般開發人員第一步就是定義一個協定結構:
struct userbase
{
unsigned short cmd;//1-get, 2-set, 定義一個short,為了擴充更多指令(理想那麼豐滿)
unsigned char gender; //1 – man , 2-woman, 3 - ??
char name[8]; //當然這裡可以定義為 string name;或len + value 組合,為了叙述友善,就使用簡單定長資料
}
在這種方式下,A基本不用編碼,直接從記憶體copy出來,再把cmd做一下網絡位元組序變換,發送給B。B也能解析,一切都很和諧愉快。
這時候編碼結果可以用圖表示為(1格一個位元組)

這種編碼方式,我稱之為緊湊模式,意思是除了資料本身外,沒有一點額外備援資訊,可以看成是Raw Data。在dos年代,這種使用方式非常普遍,那時候可是記憶體和網絡都是按K計算,cpu還沒有到1G。如果添加額外資訊,不光耗費捉襟見肘的cpu,連記憶體和帶寬都傷不起。
二、可擴充性
有一天,A在基本資料裡面加一個生日字段,然後告訴B
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
這是B就犯愁了,收到A的資料包,不知道第3個字段到底是舊協定中的name字段,還是新協定中birthday。這是後A,和B終于從教訓中認識到一個協定重要特性——相容性和可擴充性。
于是乎,A和B決定廢掉舊的協定,從新開始,制定一個以後每個版本相容的協定。方法很簡單,就是加一個version字段。
unsigned short version;
這樣,A和B就松一口氣,以後就可以很友善的擴充。增加字段也很友善。這種方法即使在現在,應該還有不少人使用。
二、更好的可擴充性
過了一段較長時間,A和B發現又有新的問題,就是沒增加一個字段就改變一下版本号,這還不是重點,重點是這樣代碼維護起來相當麻煩,每個版本一個case分支,到了最好,代碼裡面case 幾十個分支,看起來醜陋而且維護起來成本高。
A 和 B仔細思考了一下,覺得光靠一個version維護整個協定,不夠細,于是覺得為每個字段增加一個額外資訊——tag,雖然增加記憶體和帶寬,但是現在已經不像當年那樣,可以容許這些備援,換取易用性。
1 unsigned short version;
2 unsigned short cmd;
3 unsigned char gender;
4 unsigned int birthday;
5 char name[8];
制定完這些協定後,A和B很得意,覺得這個協定不錯,可以自由的增加和減少字段。随便擴充。
現實總是很殘酷的,不久就有新的需求,name使用8個位元組不夠,最大長度可能會達到100個位元組,A和B就愁懷了,總不能即使叫“steven”的人,每次都按照100個位元組打包,雖然不差錢,也不能這樣浪費。
于是A和B尋找各方資料,找到了ANS.1編碼規範,好東西啊.. ASN.1是一種ISO/ITU-T 标準。其中一種編碼BER(Basic Encoding Rules)簡單好用,它使用<tag, length,="" value="">三元組編碼,簡稱TLV編碼。
每個字段編碼後記憶體組織如下
字段可以是結構,即可以嵌套
A和B使用TLV打包協定後,資料記憶體組織大概如下:
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值如下
類型
Type值
類型描述
bool
1
布爾值
int8
2
帶符号的一個字元
uint8
3
int16
4
16位有符号整型
uint16
5
16位無符号整型
int32
6
32位有符号整型
uint32
7
32位無符号整型
…
string
12
字元串或二進制序列
struct
13
自定義的結構,嵌套使用
list
14
有序清單
map
15
無序清單
按照ttlv序列化後,記憶體組織如下
改完後,A和B發現,的确帶來很多好處,不光可以随心是以的增删字段,還可以修改資料類型,例如把cmd改成int cmd;可以無縫相容。真是太給力了。
三、跨語言特性
有一天來了一個新的同僚C,他寫一個新的服務,需要和A通信,但是C是用java或PHP的語言,沒有無符号類型,導緻負數解析失敗。為了解決這個問題,A重新規劃一下協定類型,做了有些剝離語言特性,定義一些共性。對使用類型做了強制性限制。雖然帶來了限制,但是帶來通用型和簡潔性,和跨語言性,大家表示都很贊同,于是有了一個類型(type)規範。
四、代碼自動化 ——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)