一、基础知识
参考
Protocol Buffers 在游戏中的应用
Protobuf语言指南
android与PC,C#与Java 利用protobuf 进行无障碍通讯【Socket】
1.性能好/效率高
现在,俺就来说说Google公司为啥放着好端端的XML不用,非要另起炉灶,重新造轮子。一个根本的原因是XML性能不够好。
- 先说时间开销:XML格式化(序列化)的开销倒还好;但是XML解析(反序列化)的开销就不敢恭维啦。俺之前经常碰到一些时间性能很敏感的场合,由于不堪忍受XML解析的速度,弃之如敝履。
- 再来看空间开销:熟悉XML语法的同学应该知道,XML格式为了有较好的可读性,引入了一些冗余的文本信息。所以空间开销也不是太好(不过这点缺点,俺不常碰到)。
由于Google公司赖以吹嘘的就是它的海量数据和海量处理能力。对于几十万、上百万机器的集群,动不动就是PB级的数据量,哪怕性能稍微提高0.1%也是相当可观滴。所以Google自然无法容忍XML在性能上的明显缺点。再加上Google从来就不缺造轮子的牛人,所以protobuf也就应运而生了。
Google对于性能的偏执,那可是出了名的。所以,俺对于Google搞出来protobuf是非常滴放心,性能上不敢说是最好,但肯定不会太差。
2.代码生成机制
除了性能好,代码生成机制是主要吸引俺的地方。为了说明这个代码生成机制,俺举个例子。
比如有个电子商务的系统(假设用C++实现),其中的模块A需要发送大量的订单信息给模块B,通讯的方式使用socket。
假设订单包括如下属性:
-----------------------
时间:time(用整数表示)
客户id:userid(用整数表示)
交易金额:price(用浮点数表示)
交易的描述:desc(用字符串表示)
-----------------------
如果使用protobuf实现,首先要写一个proto文件(不妨叫Order.proto),在该文件中添加一个名为"Order"的message结构,用来描述通讯协议中的结构化数据。该文件的内容大致如下:
message Order
{
required int32 time = 1;
required int32 userid = 2;
required float price = 3;
optional string desc = 4;
}
然后,使用protobuf内置的编译器编译该proto。由于本例子的模块是C++,你可以通过protobuf编译器的命令行参数,指定它生成C++语言的“订单包装类”。(一般来说,一个message结构会生成一个包装类)
然后你使用类似下面的代码来序列化/解析该订单包装类:
// 发送方
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);
// 然后调用某种socket的通讯库把序列化之后的字符串发送出去
// ......
--------------------------------
// 接收方
string sOrder;
// 先通过网络通讯库接收到数据,存放到某字符串sOrder
// ......
Order order;
if(order.ParseFromString(sOrder)) // 解析该字符串
{
cout << "userid:" << order.userid() << endl
<< "desc:" << order.desc() << endl;
}
else
{
cerr << "parse error!" << endl;
}
有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了(干这种活是典型的吃力不讨好)。
万一将来需求发生变更,要求给订单再增加一个“状态”的属性,那只需要在Order.proto文件中增加一行代码。对于发送方(模块A),只要增加一行设置状态的代码;对于接收方(模块B)只要增加一行读取状态的代码。哇塞,简直太轻松了!
另外,如果通讯双方使用不同的编程语言来实现,使用这种机制可以有效确保两边的模块对于协议的处理是一致的。
顺便跑题一下。从某种意义上讲,可以把proto文件看成是描述通讯协议的规格说明书(或者叫接口规范)。这种伎俩其实老早就有了,搞过微软的COM编程或者接触过CORBA的同学,应该都能从中看到IDL(详细解释看“这里”)的影子。它们的思想是相通滴。
3.支持“向后兼容”和“向前兼容”
还是拿刚才的例子来说事儿。为了叙述方便,俺把增加了“状态”属性的订单协议成为“新版本”;之前的叫“老版本”。
所谓的“向后兼容”(backward compatible),就是说,当模块B升级了之后,它能够正确识别模块A发出的老版本的协议。由于老版本没有“状态”这个属性,在扩充协议时,可以考虑把“状态”属性设置成非必填的,或者给“状态”属性设置一个缺省值
所谓的“向前兼容”(forward compatible),就是说,当模块A升级了之后,模块B能够正常识别模块A发出的新版本的协议。这时候,新增加的“状态”属性会被忽略。
“向后兼容”和“向前兼容”有啥用捏?俺举个例子:当你维护一个很庞大的分布式系统时,由于你无法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。
4.支持多种编程语言
俺开博以来点评的几个开源项目(比如“Sqlite”、“cURL”),都是支持很多种编程语言滴,这次的protobuf也不例外。在Google官方发布的源代码中包含了C++、Java、Python三种语言(正好也是俺最常用的三种,真爽)。如果你平时用的就是这三种语言之一,那就好办了。
假如你想把protobuf用于其它语言,咋办捏?由于Google一呼百应的号召力,开源社区对protobuf响应踊跃,近期冒出很多其它编程语言的版本(比如ActionScript、C#、Lisp、Erlang、Perl、PHP、Ruby等),有些语言还同时搞出了多个开源的项目。具体细节可以参见“这里”。
不过俺有义务提醒一下在座的各位同学。如果你考虑把protobuf用于上述这些语言,一定认真评估对应的开源库。因为这些开源库不是Google官方提供的、而且出来的时间还不长。所以,它们的质量、性能等方面可能还有欠缺。
5.protobuf有啥缺陷?
应用不够广
由于protobuf刚公布没多久,相比XML而言,protobuf还属于初出茅庐。因此,在知名度、应用广度等方面都远不如XML。由于这个原因,假如你设计的系统需要提供若干对外的接口给第三方系统调用,俺奉劝你暂时不要考虑protobuf格式。二进制格式导致可读性差
为了提高性能,protobuf采用了二进制格式进行编码。这直接导致了可读性差的问题(严格地说,是没有可读性)。虽然protobuf提供了TextFormat这个工具类(文档在“这里”),但终究无法彻底解决此问题。
可读性差的危害,俺再来举个例子。比如通讯双方如果出现问题,极易导致扯皮(都不承认自己有问题,都说是对方的错)。俺对付扯皮的一个简单方法就是直接抓包并dump成log,能比较容易地看出错误在哪一方。但是protobuf的二进制格式,导致你抓包并直接dump出来的log难以看懂。缺乏自描述
一般来说,XML是自描述的,而protobuf格式则不是。给你一段二进制格式的协议内容,如果不配合相应的proto文件,那简直就像天书一般。
由于“缺乏自描述”,再加上“二进制格式导致可读性差”。所以在配置文件方面,protobuf是肯定无法取代XML的地位滴。
二、proto3 与 proto2
Protobuf 的 proto3 与 proto2 的区别
protobuf一些注意事项
protobuf v3测试
proto3语法
1.在第一行非空白非注释行,必须写:syntax = "proto3";
2.字段规则移除了 “required”,并把 “optional” 改名为 “singular”
3.默认值:
string类型默认值是空字符串,不是null
bytes类型默认是空bytes
bool类型默认值是false
数字类型默认值是0
枚举类型默认值是第一个枚举值,即0
repeated修饰的字段,默认值是空(在对应的编程语言中通常是一个空的list)
三、js中使用
1.注意现在有两个版本,cocos论坛讨论里,推荐decodeio的protobufjs
参考
Node.js使用google-protobuf
cocos creator中使用protobuf(dcodeIO/protobuf.js 5.0)
这里网上查阅资料可能会让人混乱,其实是因为在google官方的js库出来之前,decodeio先推出了一个库叫protobufjs。
随着Google的Protobuf3的发布,Google终于开发了一个可以给JavaScript使用的库。之前大家如果在node端使用了Protobuf应该用的是protobufjs这个库,但是既然Google官方支持了JavaScript,那么我们还是要去尝试一下的。
主要存在两个解决方案 使用protobufjs 或者 谷歌官方的js解析(通过protoc.exe生成.proto对应的js文件直接使用),个人认为protojs更为方便,如果更改.proto文件都要使用protoc重新生成对应的js文件略为繁琐。所以这里我们直接采用的protobufjs。
区别很简单,如果用protoc --js_out这种,就是官方的库。如果直接加载proto文件去解析,或者pbjs导出的,就是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命令导入,closuer.js生成的js要使用goo.provide命令来导入。 - 用例:
前端后台以及游戏中使用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最新的版本,所以你们想学习大佬的教程一定要npm install protobufjs@5 这样才行,不然你就只能看着大佬的教程干瞪眼了。
ProtoBuf.js同时支持NodeJS和Browser,也就是说,现在我们可以在JS client端使用protobuf!当然,前提是我们使用的是高级浏览器,因为ProtoBuf.js依赖于ByteBuffer.js(一个对ArrayBuffer进行了封装的类库),所以,只能say byebye to IE < 10
。
由于 JavaScript 精度问题,所以 int64和 uint64等类型数据会被转换成 Long.js 对象实例,Long 并不太复杂,与 bignumber.js 类似,具体参考 Long.js API.
4.cocos论坛里“奎特尔星球代言人”提供的基于protobufjs的插件pbkiller
当creator遇上protobufjs—起步
当creator遇上protobufjs—深入
当creator遇上protobufjs—效率
当creator遇上protobufjs—pbkiller插件
自从开始写protobufjs的分享教程,就开始坚持不懈的在CocosCreator论坛上自吹自擂,无意见被CocosCreator制作人南塔斯大神看到了。一不小心收到南大神的论坛私信,询问我可否将protobuf的使用制作成Creator的插件,并邀我将插件入驻Creator付费商店。收到消息的第一时间,我异常兴奋。第一是我的经验分享竟能受到Creator官方大神的关注;其次是居然还可以入驻付费商店,对于程序员来说莫大的欣慰就是可以将代码变换现实中的价值。
通过一段时间的Creator插件学习与protobufjs源码的理解,再结合Creator项目经验,终于完成了第一版插件。在制作插件的过程中,插件的命名是最让我纠结的,因为我在曾经的项目中大量使用xxxHelper,编写了不少辅助工具。这次为了让我的第一个Creator插件看起来很牛逼一点点的感觉,我脑子冒出killer的字样,随后我就叫他:pbkiller。
5.推荐方案
一个解决tcp+jsb+protobuf的demo
项目中的引用的protobuf最开始是使用奎神的pbkiller。pbkiller是基于protobufjs5.x的。写的过程中发现低版本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或者https://www.jianshu.com/p/f727f78dcc76。不再赘述。
demo中提供了bytes和repeats的使用方法供参考。
最新版本6.8.8的:Cocos Creator 中使用 protobufjs
6.其他问题
当creator遇上protobufjs—青春升级
protobufjs序列化后如何拼接上消息Id
leaf 和cocos creator 游戏实战(一)使用protobuf完成通讯,这个例子使用的也是protobufjs。
四、WebSocket断粘包
1.摘自前端后台以及游戏中使用Google Protocol Buffer详解
websocket也是基于tcp的,相当于在tcp基础上封装了一层。 某种程度来说tcp的性能优于websocket,因为websocket就是在tcp的基础上多了一层转化,但是websocket使用更简单,用tcp的app端需要自己去读tcp流,根据包头和包体组装数据包,而websocket不需要,因为websocket会是一个整包的数据并不是流的形式。 具体来说,后端通过缓存区把数据冲刷(flush)给前端,app端拿到tcp数据流,需要根据消息头给定的消息体长度,去拿取后面多少位的数据,然后组装成一个数据包。 而websocket传输过来就是一个个的包,也就是帧并不是数据流,所以后端在给websocket数据的时候,必须要把一个整包,在缓冲区一次性冲刷过来,而给tcp的话就可以自由冲刷。
也就是说如果服务端同样采用的是websocket的话(Node.js及 ws库),我们对消息是不需要添加数据头进行数据包的组装的。websocket是按照包一次性读取的。既我们不需要在手动的定义数据包头以及添加数据包长度信息。
2.摘自解决方案:在Cocos Creator1.8中使用官方的google protobuf
![](http://img.e-com-net.com/image/info10/c3ff43a3f5094449bb64297009c0827e.jpg)