Cocos 2.x Protobuf的js版本使用

一、基礎(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
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.推薦方案

一個解決tcp+jsb+protobuf的demo

項目中的引用的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
image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容