互聯網後臺服務的協議設計

互聯網後臺服務的協議設計html

1.    基本概念c++

服務(server):「服務」能夠分軟件和硬件兩個類別,本文提到的「服務」都是指軟件,是一種程序。稱之爲「服務」的程序通常具有2個特色:redis

1)    程序啓動後常駐內存,成爲守護進程。編程

2)    能與其餘進程通訊,接收請求,處理請求並作出迴應。json

本文中的服務特指基於TCP/IP 協議經過socket進行通訊的服務。api

爲何互聯網業務須要「服務」這種類型的程序呢?主要有2個緣由網絡

1)     有些功能能夠經過一個獨立的程序來完成,不用每一個程序都寫一套代碼來實現這個功能,這樣有利於程序的解耦和複用架構

2)     有些功能單機、單進程沒法完成,須要經過多臺機器、多個程序的協做完成。併發

好比memcache服務,它常駐內存並監聽TCP端口,接收來自socket的數據包,使客戶端能夠以key-value的方式存取數據,是互聯網後臺中一種經常使用的cache服務。框架

客戶端(client):本文中的客戶端指的是主動發包給服務的程序,或者說是發起請求的程序,相對的,服務就是接收請求並處理的程序。

協議:協議是一種約定,經過約定,不一樣的進程能夠對一段數據產生相同的理解,從而能夠相互協做,存在進程間通訊的程序就必定須要協議。

咱們爲何須要本身設計協議:

經過上面講的咱們能夠看出,在互聯網後臺開發中,稍微複雜一些的業務,服務是必要的,進而協議也是必要的。那麼咱們是否能夠複用已有的協議呢?主要是由於如今已有的協議都沒有能徹底match互聯網後臺開發的需求,存在這樣或那樣的問題。

協議設計的目標:

解析效率:互聯網業務具備高併發的特色,解析效率決定了使用協議的CPU成本;

編碼長度:信息編碼出來的長度,編碼長度決定了使用協議的網絡帶寬及存儲成本;

易於實現:互聯網業務須要一個輕量級的協議,而不是大而全的,CORBA這種重量級的協議就不太適合,易於實現決定了使用協議的開發成本和學習成本;

可讀性:編碼後的數據的可讀性決定了使用協議的調試及維護成本

兼容性: 互聯網的需求具備靈活多變的特色,協議會常常升級,使用協議的雙方是否能夠獨立升級協議、增減協議中的字段是很是重要的。兼容性決定了持續開發時的開發成本,我的以爲這點是互聯網協議中最重要的一個指標。

協議設計須要解決的問題:

1)     序列化/反序列化

2)     判斷包的完整性

只要解決了這2個問題,2個不一樣機器的進程就能完成通訊。

 

2.    序列化/反序列化:

序列化咱們常稱之爲編碼,或者打包,反序列化常稱之爲解碼,或者解包。經常使用的序列化/反序列化方式主要有如下幾種:

1)     TLV編碼及其變體(後面統稱爲TLV編碼):Protobuf/thrift/ASN BER都屬於這種。

TLV編碼基本原理是每一個字段打一個二進制包,每一個包包含tag、length、value 3個部分:

tag: 通常佔用1個字節,表示數據類型,有的編碼方式(Protobuf/thrift)中tag包含字段的id,有的編碼方式(ASN BER)不包含字段的id。包含字段id的序列化方式,id是字段的標誌,協議能夠靈活的增刪字段,只要保證字段id惟一,就能兼容解析,很是適合互聯網開發。

length:一個整數,表示後面數據塊的長度,Protobuf/thrift的序列化不包含length字段,由於大部分數據類型的長度均可以根據tag中的類型信息能夠獲得。

value:真正的數據內容。

舉個tag包含id的序列化方式打包解包的例子(只是舉個例子說明原理,實際上Protobuf等協議都作了比較巧妙的實現,好比varint、ZigZag編碼來儘可能減小編碼長度):

協議包括2個字段, name字段的id爲0,類型爲1(string);age字段的id爲1,類型爲2(unsigned int     )

字段id

字段類型

字段名

0

string

name

1

unsigned int

age

須要傳輸的數據:

name = "xxx"

age = 18

序列化以後大約是

字段類型(tag的一部分)

 字段id(tag的一部分)

字段值(value)

0x01

0x00

xxx

0x02

0x01

0x12

反序列化的時候,逐步解析字節流,先解析字段類型和字段id,再根據字段類型解析出後面的數據內容,獲得了一個id和值的映射關係

0 : "xxx"

1 : 18

根據協議,id=0的字段表示name,id=1的字段表示age,反序列化以後,就知道傳過來的數據是

name = "xxx",age = 18了

若是協議作了升級,增長了1個字段「gender」,刪除一個已經沒有意義的字段age,協議變成

0 string name

2 string gender

須要傳輸的數據:

name = "xxx"

gender = "male"

發送方升級了協議,序列化以後大約是

字段類型

字段id

字段值

0x01

0x00

xxx

0x01

0x02

male

反序列化以後,獲得了一個id和值的映射關係

0 : "xxx"

2 : "male"

反序列化的一方因爲沒有升級協議,不知道id=2的字段什麼意思,直接忽略,沒找到id=1的age字段,那麼使用默認值,這樣單方的升級,徹底不影響協議的解析,協議是具備兼容性的。

             

舉個tag不包含id的序列化方式打包解包的例子

若是tag中沒有字段id,那麼字段所在的位置決定字段的含義

協議包括2個字段, 第1個字段name,類型爲1(string);第2個字段age類型爲2(unsigned int )

字段類型

字段名

string

name

unsigned int

age

 

須要傳輸的數據:

name = "xxx"

age = 18

序列化以後大約是

字段類型

字段值

0x01

xxx

0x02

0x12

反序列化程序解析出第1個字段是字符串xxx,第二個字段是整數18,根據協議,第1個字段是name,第2個字段是age,這時反序列化程序就知道了name是xxx,age是18

可是相比上面有id的序列化方式,這種方式有個明顯的缺陷:一方升級了協議時,另外一方極可能須要升級協議才行,協議不具備兼容性。好比協議作了升級,增長了一個字段gender,刪除一個已經沒有意義的字段age,協議變成

string name

string gender

須要傳輸的數據:

name = "xxx"

gender = "male"

發送方升級了協議,序列化以後大約是

字段類型

字段值

0x01

xxx

0x01

male

這時接收方若是不升級協議就徹底沒法理解協議的含義

能夠看出tag包含ID的序列化方式(Protobuf/thrift)兼容性和靈活性方面優於不包含ID的方式(asn-ber)

 

TLV編碼的特色是:

解析效率高:主要是由於不須要轉義字符

編碼長度低:主要是由於元數據佔用的空間不多

不易於實現:可是有不少開源的工具,根據IDL自動生成代碼,提升開發效率

兼容性高:協議雙方能夠獨立升級   

可讀性差:二進制協議,肉眼很難識別

      

2)     文本流編碼:xml/json都屬於這種。

基本原理是把每一個字段打一個字符串形式的包,經過鍵值對(key-value)的方式存儲數據,key是字段的名字,用於區分不一樣的字段(對比上面TLV編碼採用id的方式標誌一個字段),特殊字符特別是非文本字符須要作適當轉義,轉義爲xml/json的合法字符。xml的解析效率低於json,而編碼長度高於json,json做爲序列化的方式通常是優於xml的。

一樣是上面的協議:

序列化的結果大概是

<p><name>xxx</name><age>18</age></p>

或者

{name:xxx,age:18}

  文本流編碼的特色是:解析效率低,編碼長度高,易於實現,可擴展性高,可讀性好

 

3)     固定結構編碼:

基本原理是,協議約定了傳輸字段類型和字段含義,和TLV的方式相似,可是沒有了tag和len,只有value

一樣是上面的協議:

序列化的結果大概是

xxx 0x00 0x12

反序列化的時候,根據協議中約定的字段位置、字段類型和字段含義,逐個解出相應的字段

固定結構編碼若是協議升級了又須要保證兼容性,那麼能夠在協議中增長一個「版本號」字段,而後根據版本號決定如何序列化和反序列化,這樣能夠保證協議的兼容性。可是這樣會致使代碼很是混亂和讓人費解

固定結構編碼解析效率、編碼長度、易於實現、可讀性方面略微優於TLV方式,可是靈活性和兼容性很是差,若是不使用版本號判斷就不能單方增刪字段,不能單方修改字段數據類型,甚至,把協議中的short int字段改爲int,反序列化就可能會出錯,所以除了業務邏輯很是固定的場景外不推薦使用。

 

4)     內存dump:

基本原理是,把內存中的數據直接輸出,不作任何序列化操做。反序列化的時候,直接還原內存。

通常咱們聲明c++的結構以下便可

       #pragma pack (1)

       struct

       {

              char name[64];

              unsigned int age;

       };

       #pragma pack ()      

這種方式適合c/c++語言,單機進程間交換數據。這是一種簡單高效的協議,特別適合經過共享內存交換數據的場景。可是不具備通用性,不適合跨越語言和機器,本文再也不討論這種編碼方式

 

若是沒有特別的必要,本身發明一種序列化方式通常是費力不討好的,有重複造輪子的嫌疑,因此咱們在成熟序列化方式中選擇一種便可。

綜上,咱們能夠看出,若是咱們想設計一個具備通用性,能夠用於分佈式環境,適合互聯網後臺開發,能傳遞複雜數據,具備很好的靈活性和兼容性的協議,經常使用的序列化方式是TLV編碼和字符流編碼2種。那麼根據不重複造輪子的原則,可選的編碼方式就只有Protobuf、thrift 和 json 3種了。咱們對比一下這3種編碼方式。

序列化方式對比

Protobuf/thrift VS json

根據google的測評結果,Protobuf/thrift 效率高於 json, 而可讀性弱於json。解析效率大概比json高1倍。這個具體的倍數關係我沒測試過,存疑,並且不一樣的程序使用的json庫不同,仍是應該以實測結果爲準。

參考http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking

Protobuf VS thrift

Protobuf 效率和編碼長度略有優點,文檔比thrift豐富

thrift 內建的數據類型更多(有map和set)

thrift官方比Protobuf支持更多的編程語言,並有RPC框架,可是Protobuf有不少第三方的支持,一樣提供了多種語言的支持和RPC框架的實現

參考http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns

參考http://blog.mirthlab.com/2009/06/01/thrift-vs-protocol-bufffers-vs-json/

我的比較傾向於Protobuf,主要是考慮到文檔和第三方支持多,目前使用的更普遍。

至此,咱們就選定了2種序列化方式Protobuf和json,若是併發度很是高,數據量很是大,使用Protobuf,不然使用json.

 

3. 判斷包的完整性:   

通常有兩種方法:

1)     在序列化後的buffer前面增長一個採用固定結構編碼的頭部,頭部長度和結構固定,其中有個字段存儲包總長度。收包時,先接收固定字節數的頭部,解出這個包完整長度,按此長度接收包體。

2)     在序列化後的buffer前面增長一個字符流的頭部,其中有個字段存儲包總長度,根據特殊字符(好比根據\n 或者\0)判斷頭部的完整性。這樣一般比1要麻煩一些,http、memcached和radis採用的是這種方式。收包的時候,先判斷已收到的數據中是否包含結束符,收到結束符後解析包頭,解出這個包完整長度,按此長度接收包體。      

 

至此,咱們已經獲得了一個協議框架,採用這個協議框架,再根據業務須要約定字段含義,就能夠獲得一個具體的協議,能夠用於把一個機器上的消息,發送到另外一個機器,並讓對方徹底理解消息的含義。可是若是這就是這個協議框架的所有,那這個協議就太弱了,由於若是一個程序只知道協議框架而不知道協議的字段內容,那它除了能夠收包和發包外,作不了任何事情,而在客戶端和服務之間搭建一個代理層,來作容災、監控、統計、路由、認證等等事情是一種常見的架構模式,這樣這些公共的處理邏輯就不用每一個服務都作一次了,服務能夠專一於業務,而把這些邏輯交由代理層來作。換句話說,咱們須要爲協議框架增長一個頭部,並約定一些全部業務均可以使用的公共字段。

 

4. 協議頭部:

那麼頭部中能夠增長哪些字段呢?這個取決於你但願代理幫你作哪些事情。一般如下字段是能夠考慮的:

seq                     //消息序列號,能夠用於排查問題,也能夠用於某些IO模型中包的解析

protocol version       //協議版本號,能夠用於協議的兼容

request useragent    //請求者機器環境,包括操做系統、客戶端版本等等信息

request user ip        //請求者ip

request user id        //請求者id

client ip               //客戶端ip

client id               //客戶端業務id

server ip                     //服務ip

server id                     //服務id

server server cmd     //服務命令字

retcode                     //返回碼

有了這些字段,代理層就能徹底監控到服務的訪問狀況,並生成報表

 

 5.    我設計的協議:

有了上面的理論,咱們就能夠真正的設計協議了。我設計的這個協議能夠應用於互聯網後臺服務的絕大部分場景,協議中把一個包分爲3個部分:

包頭的第1部分:固定8字節:協議標誌(2字節) 包頭長度(2字節)  包體長度(4字節)

包頭的第2部分:這部分主要是前面第4點提到的公共頭部,包括seq等字段,採用Protobuf序列化,包頭的字段是能夠增刪的,即便沒有任何字段,也不影響數據傳遞,可是可能影響你的代理作的工做;

包體:採用Protobuf序列化,具體內容取決於業務。

 

 6. 一些經常使用的協議:

http協議:http協議是咱們最多見的協議,咱們是否能夠採用http協議做爲互聯網後臺的協議呢?這個通常是不適當的,主要是考慮到如下2個緣由:

1)     http協議只是一個框架,沒有指定包體的序列化方式,因此還須要配合其餘序列化的方式使用才能傳遞業務邏輯數據。

2)     http協議解析效率低,並且比較複雜(不知道有沒有人以爲http協議簡單,其實不是http協議簡單,而是http你們比較熟悉而已)

有些狀況下是可使用http協議的:

1)     對公網用戶api,http協議的穿透性最好,因此最適合;

2)     效率要求沒那麼高的場景;

3)     但願提供更多人熟悉的接口,好比新浪微、騰訊博提供的開放接口,就是http的;

 

memcache的協議

基本原理是:先發送字符流,以\r\n做爲結束標誌,字符流中不容許存在特殊字符。

再發送一個數據包,能夠包含任何字符,數據包的長度已經在前面的字符流中指定。

memcache的協議並無包含業務數據序列化和反序列化的部分,只有包頭和一個buffer,是一種適合於業務邏輯簡單場景下的協議。參考:http://www.ccvita.com/306.html

 

redis協議:

基本原理是:先發送一個字符串表示參數個數,而後再逐個發送參數,每一個參數發送的時候,先發送一個字符串表示參數的數據長度,再發送參數的內容。

redis的協議和memcache相似,可是memcached只能帶一個二進制字段,redis能夠帶多個

參考:http://www.redisdoc.com/en/latest/topic/protocol.html

相關文章
相關標籤/搜索