一、基礎(chǔ)知識
參考
Protocol Buffers 在游戲中的應(yīng)用
Protobuf語言指南
android與PC,C#與Java 利用protobuf 進行無障礙通訊【Socket】
1.性能好/效率高
現(xiàn)在,俺就來說說Google公司為啥放著好端端的XML不用,非要另起爐灶,重新造輪子。一個根本的原因是XML性能不夠好。
- 先說時間開銷:XML格式化(序列化)的開銷倒還好;但是XML解析(反序列化)的開銷就不敢恭維啦。俺之前經(jīng)常碰到一些時間性能很敏感的場合,由于不堪忍受XML解析的速度,棄之如敝履。
- 再來看空間開銷:熟悉XML語法的同學(xué)應(yīng)該知道,XML格式為了有較好的可讀性,引入了一些冗余的文本信息。所以空間開銷也不是太好(不過這點缺點,俺不常碰到)。
由于Google公司賴以吹噓的就是它的海量數(shù)據(jù)和海量處理能力。對于幾十萬、上百萬機器的集群,動不動就是PB級的數(shù)據(jù)量,哪怕性能稍微提高0.1%也是相當(dāng)可觀滴。所以Google自然無法容忍XML在性能上的明顯缺點。再加上Google從來就不缺造輪子的牛人,所以protobuf也就應(yīng)運而生了。
Google對于性能的偏執(zhí),那可是出了名的。所以,俺對于Google搞出來protobuf是非常滴放心,性能上不敢說是最好,但肯定不會太差。
2.代碼生成機制
除了性能好,代碼生成機制是主要吸引俺的地方。為了說明這個代碼生成機制,俺舉個例子。
比如有個電子商務(wù)的系統(tǒng)(假設(shè)用C++實現(xiàn)),其中的模塊A需要發(fā)送大量的訂單信息給模塊B,通訊的方式使用socket。
假設(shè)訂單包括如下屬性:
-----------------------
時間:time(用整數(shù)表示)
客戶id:userid(用整數(shù)表示)
交易金額:price(用浮點數(shù)表示)
交易的描述:desc(用字符串表示)
-----------------------
如果使用protobuf實現(xiàn),首先要寫一個proto文件(不妨叫Order.proto),在該文件中添加一個名為"Order"的message結(jié)構(gòu),用來描述通訊協(xié)議中的結(jié)構(gòu)化數(shù)據(jù)。該文件的內(nèi)容大致如下:
message Order
{
required int32 time = 1;
required int32 userid = 2;
required float price = 3;
optional string desc = 4;
}
然后,使用protobuf內(nèi)置的編譯器編譯該proto。由于本例子的模塊是C++,你可以通過protobuf編譯器的命令行參數(shù),指定它生成C++語言的“訂單包裝類”。(一般來說,一個message結(jié)構(gòu)會生成一個包裝類)
然后你使用類似下面的代碼來序列化/解析該訂單包裝類:
// 發(fā)送方
Order order;
order.set_time(XXXX);
order.set_userid(123);
order.set_price(100.0f);
order.set_desc("a test order");
string sOrder;
order.SerailzeToString(&sOrder);
// 然后調(diào)用某種socket的通訊庫把序列化之后的字符串發(fā)送出去
// ......
--------------------------------
// 接收方
string sOrder;
// 先通過網(wǎng)絡(luò)通訊庫接收到數(shù)據(jù),存放到某字符串sOrder
// ......
Order order;
if(order.ParseFromString(sOrder)) // 解析該字符串
{
cout << "userid:" << order.userid() << endl
<< "desc:" << order.desc() << endl;
}
else
{
cerr << "parse error!" << endl;
}
有了這種代碼生成機制,開發(fā)人員再也不用吭哧吭哧地編寫那些協(xié)議解析的代碼了(干這種活是典型的吃力不討好)。
萬一將來需求發(fā)生變更,要求給訂單再增加一個“狀態(tài)”的屬性,那只需要在Order.proto文件中增加一行代碼。對于發(fā)送方(模塊A),只要增加一行設(shè)置狀態(tài)的代碼;對于接收方(模塊B)只要增加一行讀取狀態(tài)的代碼。哇塞,簡直太輕松了!
另外,如果通訊雙方使用不同的編程語言來實現(xiàn),使用這種機制可以有效確保兩邊的模塊對于協(xié)議的處理是一致的。
順便跑題一下。從某種意義上講,可以把proto文件看成是描述通訊協(xié)議的規(guī)格說明書(或者叫接口規(guī)范)。這種伎倆其實老早就有了,搞過微軟的COM編程或者接觸過CORBA的同學(xué),應(yīng)該都能從中看到IDL(詳細解釋看“這里”)的影子。它們的思想是相通滴。
3.支持“向后兼容”和“向前兼容”
還是拿剛才的例子來說事兒。為了敘述方便,俺把增加了“狀態(tài)”屬性的訂單協(xié)議成為“新版本”;之前的叫“老版本”。
所謂的“向后兼容”(backward compatible),就是說,當(dāng)模塊B升級了之后,它能夠正確識別模塊A發(fā)出的老版本的協(xié)議。由于老版本沒有“狀態(tài)”這個屬性,在擴充協(xié)議時,可以考慮把“狀態(tài)”屬性設(shè)置成非必填的,或者給“狀態(tài)”屬性設(shè)置一個缺省值
所謂的“向前兼容”(forward compatible),就是說,當(dāng)模塊A升級了之后,模塊B能夠正常識別模塊A發(fā)出的新版本的協(xié)議。這時候,新增加的“狀態(tài)”屬性會被忽略。
“向后兼容”和“向前兼容”有啥用捏?俺舉個例子:當(dāng)你維護一個很龐大的分布式系統(tǒng)時,由于你無法同時升級所有模塊,為了保證在升級過程中,整個系統(tǒng)能夠盡可能不受影響,就需要盡量保證通訊協(xié)議的“向后兼容”或“向前兼容”。
4.支持多種編程語言
俺開博以來點評的幾個開源項目(比如“Sqlite”、“cURL”),都是支持很多種編程語言滴,這次的protobuf也不例外。在Google官方發(fā)布的源代碼中包含了C++、Java、Python三種語言(正好也是俺最常用的三種,真爽)。如果你平時用的就是這三種語言之一,那就好辦了。
假如你想把protobuf用于其它語言,咋辦捏?由于Google一呼百應(yīng)的號召力,開源社區(qū)對protobuf響應(yīng)踴躍,近期冒出很多其它編程語言的版本(比如ActionScript、C#、Lisp、Erlang、Perl、PHP、Ruby等),有些語言還同時搞出了多個開源的項目。具體細節(jié)可以參見“這里”。
不過俺有義務(wù)提醒一下在座的各位同學(xué)。如果你考慮把protobuf用于上述這些語言,一定認真評估對應(yīng)的開源庫。因為這些開源庫不是Google官方提供的、而且出來的時間還不長。所以,它們的質(zhì)量、性能等方面可能還有欠缺。
5.protobuf有啥缺陷?
應(yīng)用不夠廣
由于protobuf剛公布沒多久,相比XML而言,protobuf還屬于初出茅廬。因此,在知名度、應(yīng)用廣度等方面都遠不如XML。由于這個原因,假如你設(shè)計的系統(tǒng)需要提供若干對外的接口給第三方系統(tǒng)調(diào)用,俺奉勸你暫時不要考慮protobuf格式。二進制格式導(dǎo)致可讀性差
為了提高性能,protobuf采用了二進制格式進行編碼。這直接導(dǎo)致了可讀性差的問題(嚴格地說,是沒有可讀性)。雖然protobuf提供了TextFormat這個工具類(文檔在“這里”),但終究無法徹底解決此問題。
可讀性差的危害,俺再來舉個例子。比如通訊雙方如果出現(xiàn)問題,極易導(dǎo)致扯皮(都不承認自己有問題,都說是對方的錯)。俺對付扯皮的一個簡單方法就是直接抓包并dump成log,能比較容易地看出錯誤在哪一方。但是protobuf的二進制格式,導(dǎo)致你抓包并直接dump出來的log難以看懂。缺乏自描述
一般來說,XML是自描述的,而protobuf格式則不是。給你一段二進制格式的協(xié)議內(nèi)容,如果不配合相應(yīng)的proto文件,那簡直就像天書一般。
由于“缺乏自描述”,再加上“二進制格式導(dǎo)致可讀性差”。所以在配置文件方面,protobuf是肯定無法取代XML的地位滴。
二、proto3 與 proto2
Protobuf 的 proto3 與 proto2 的區(qū)別
protobuf一些注意事項
protobuf v3測試
proto3語法
1.在第一行非空白非注釋行,必須寫:syntax = "proto3";
2.字段規(guī)則移除了 “required”,并把 “optional” 改名為 “singular”
3.默認值:
string類型默認值是空字符串,不是null
bytes類型默認是空bytes
bool類型默認值是false
數(shù)字類型默認值是0
枚舉類型默認值是第一個枚舉值,即0
repeated修飾的字段,默認值是空(在對應(yīng)的編程語言中通常是一個空的list)
三、js中使用
1.注意現(xiàn)在有兩個版本,cocos論壇討論里,推薦decodeio的protobufjs
參考
Node.js使用google-protobuf
cocos creator中使用protobuf(dcodeIO/protobuf.js 5.0)
這里網(wǎng)上查閱資料可能會讓人混亂,其實是因為在google官方的js庫出來之前,decodeio先推出了一個庫叫protobufjs。
隨著Google的Protobuf3的發(fā)布,Google終于開發(fā)了一個可以給JavaScript使用的庫。之前大家如果在node端使用了Protobuf應(yīng)該用的是protobufjs這個庫,但是既然Google官方支持了JavaScript,那么我們還是要去嘗試一下的。
主要存在兩個解決方案 使用protobufjs 或者 谷歌官方的js解析(通過protoc.exe生成.proto對應(yīng)的js文件直接使用),個人認為protojs更為方便,如果更改.proto文件都要使用protoc重新生成對應(yīng)的js文件略為繁瑣。所以這里我們直接采用的protobufjs。
區(qū)別很簡單,如果用protoc --js_out這種,就是官方的庫。如果直接加載proto文件去解析,或者pbjs導(dǎo)出的,就是decodeio的庫了。
2.官方的https://github.com/protocolbuffers/protobuf/tree/master/js
- 安裝方式:
npm install google-protobuf - 生成文件:
protoc ./test.proto --js_out=import_style=commonjs,binary:.
這里有兩種形式,一種是common.js,一種是closure(google style).common.js生成的js要使用 require命令導(dǎo)入,closuer.js生成的js要使用goo.provide命令來導(dǎo)入。 - 用例:
前端后臺以及游戲中使用Google Protocol Buffer詳解
解決方案:在Cocos Creator1.8中使用官方的google protobuf
3.decodeio的https://github.com/protobufjs/protobuf.js
- 安裝方式:
npm install protobufjs - 生成文件:
pbjs -t static-module -w commonjs -o compiled.js file1.proto file2.proto - 用例:cocos creator中使用protobuf(dcodeIO/protobuf.js 5.0)
- 注意事項:在 creator 與protobuf的那些事提到了版本問題:大佬們寫的教程基本都是 基于protobuf5 也是他們說的直接在項目下面 npm install protobufjs 但是npm下來的卻是protobuf最新的版本,所以你們想學(xué)習(xí)大佬的教程一定要npm install protobufjs@5 這樣才行,不然你就只能看著大佬的教程干瞪眼了。
ProtoBuf.js同時支持NodeJS和Browser,也就是說,現(xiàn)在我們可以在JS client端使用protobuf!當(dāng)然,前提是我們使用的是高級瀏覽器,因為ProtoBuf.js依賴于ByteBuffer.js(一個對ArrayBuffer進行了封裝的類庫),所以,只能say byebye to IE < 10。
由于 JavaScript 精度問題,所以 int64和 uint64等類型數(shù)據(jù)會被轉(zhuǎn)換成 Long.js 對象實例,Long 并不太復(fù)雜,與 bignumber.js 類似,具體參考 Long.js API.
4.cocos論壇里“奎特爾星球代言人”提供的基于protobufjs的插件pbkiller
當(dāng)creator遇上protobufjs—起步
當(dāng)creator遇上protobufjs—深入
當(dāng)creator遇上protobufjs—效率
當(dāng)creator遇上protobufjs—pbkiller插件
自從開始寫protobufjs的分享教程,就開始堅持不懈的在CocosCreator論壇上自吹自擂,無意見被CocosCreator制作人南塔斯大神看到了。一不小心收到南大神的論壇私信,詢問我可否將protobuf的使用制作成Creator的插件,并邀我將插件入駐Creator付費商店。收到消息的第一時間,我異常興奮。第一是我的經(jīng)驗分享竟能受到Creator官方大神的關(guān)注;其次是居然還可以入駐付費商店,對于程序員來說莫大的欣慰就是可以將代碼變換現(xiàn)實中的價值。
通過一段時間的Creator插件學(xué)習(xí)與protobufjs源碼的理解,再結(jié)合Creator項目經(jīng)驗,終于完成了第一版插件。在制作插件的過程中,插件的命名是最讓我糾結(jié)的,因為我在曾經(jīng)的項目中大量使用xxxHelper,編寫了不少輔助工具。這次為了讓我的第一個Creator插件看起來很牛逼一點點的感覺,我腦子冒出killer的字樣,隨后我就叫他:pbkiller。
5.推薦方案
項目中的引用的protobuf最開始是使用奎神的pbkiller。pbkiller是基于protobufjs5.x的。寫的過程中發(fā)現(xiàn)低版本protobufjs中對bytes,repeated,int64的使用太麻煩了。然后果斷放棄了pbkiller,使用了最新的protobufjs6.8.6.
protobufjs的github:https://github.com/dcodeIO/protobuf.js
可以通過npm install -g protobufjs命令去獲取。
也可以自己動手集成google protobuf。
https://github.com/google/protobuf6
參考:http://forum.cocos.com/t/cocoscreator-protobuf/61045或者http://m.itdecent.cn/p/f727f78dcc76。不再贅述。
demo中提供了bytes和repeats的使用方法供參考。
最新版本6.8.8的:Cocos Creator 中使用 protobufjs
6.其他問題
當(dāng)creator遇上protobufjs—青春升級
protobufjs序列化后如何拼接上消息Id
leaf 和cocos creator 游戲?qū)崙?zhàn)(一)使用protobuf完成通訊,這個例子使用的也是protobufjs。
四、WebSocket斷粘包
1.摘自前端后臺以及游戲中使用Google Protocol Buffer詳解
websocket也是基于tcp的,相當(dāng)于在tcp基礎(chǔ)上封裝了一層。 某種程度來說tcp的性能優(yōu)于websocket,因為websocket就是在tcp的基礎(chǔ)上多了一層轉(zhuǎn)化,但是websocket使用更簡單,用tcp的app端需要自己去讀tcp流,根據(jù)包頭和包體組裝數(shù)據(jù)包,而websocket不需要,因為websocket會是一個整包的數(shù)據(jù)并不是流的形式。 具體來說,后端通過緩存區(qū)把數(shù)據(jù)沖刷(flush)給前端,app端拿到tcp數(shù)據(jù)流,需要根據(jù)消息頭給定的消息體長度,去拿取后面多少位的數(shù)據(jù),然后組裝成一個數(shù)據(jù)包。 而websocket傳輸過來就是一個個的包,也就是幀并不是數(shù)據(jù)流,所以后端在給websocket數(shù)據(jù)的時候,必須要把一個整包,在緩沖區(qū)一次性沖刷過來,而給tcp的話就可以自由沖刷。
也就是說如果服務(wù)端同樣采用的是websocket的話(Node.js及 ws庫),我們對消息是不需要添加數(shù)據(jù)頭進行數(shù)據(jù)包的組裝的。websocket是按照包一次性讀取的。既我們不需要在手動的定義數(shù)據(jù)包頭以及添加數(shù)據(jù)包長度信息。
2.摘自解決方案:在Cocos Creator1.8中使用官方的google protobuf
