很早之前就听说过protobuf,对此协议的评价也一直很高。但是之前接触到的项目一直没有使用这个协议,都是直接跟后端约定好数据包格式(协议号+数据包大小),然后将对应的数据结构体转换为数据字节流进行数据的传输。最近接触的一个新项目,使用到了protobuf协议,项目前端使用的是cocos2dx-lua,正好前段时间在用cocos creator进行项目开发,所以本着在开发中学习的想法,用cocos creator再简单的将项目实现一边,主要是加入protobuf协议。
在搜索引擎中搜索 cocos creator + protobuf ,得到的资料都比较旧,而且我试着参考网上给到的资料,一直失败,所以还是要根据creator、protobuf的版本来做一定的调整才行。
本文中使用到的环境是:cocos creator 2.0.10版本,protobuf 6.8.6版本,下载地址。
1、
下载好protobuf之后,导入到creator中,在提示是否导入为插件的时候选择是,然后再将允许编辑器加载勾上,不然在编辑过程中一直会提示 protobuf 未定义之类的错误:
2、
将protobuf导入到编辑器之后,就可以参考官方的例子,来编写代码了,首先看一下官方给的proto文件:
// awesome.proto package awesomepackage; syntax = "proto3"; message AwesomeMessage { string awesome_field = 1; // becomes awesomeField }
具体的protobuf语法,可以回头再去了解一下。这里有几个关键信息后续会用到,包名(awesomepackage)、消息名称(AwesomeMessage)。将proto文件放到resource目录(这个目录是creator规定用于加载动态导入资源的目录),之前lua这边是将.proto文件转成了.pb文件(具体为什么要转,.pb和.proto有什么不同,还没了解清楚),所以一开始我也是直接去加载.pb文件,一直加载不了,后来才发现网上的教程都是加载的.proto文件。
官方给的例子,加载方式是这样的:
protobuf.load("awesome.proto", function(err, root) { if (err) throw err; // Obtain a message type var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage"); }
直接运行的话,会报错,类似: return callback(Error("status " + xhr.status)); ,参考 这篇文章 ,修改protobuf.js,搜索 function fetch 修改一下:
function fetch(filename, options, callback) { if (typeof options === "function") { callback = options; options = {}; } else if (!options) options = {}; if (!callback) return asPromise(fetch, this, filename, options); // eslint-disable-line no-invalid-this // 判断是否是cocos项目 if (typeof cc !== "undefined"){ if (cc.sys.isNative){ var content = jsb.fileUtils.getStringFromFile(filename) callback(content === "" ? Error(filename + " not exits") : null, content) } else{ cc.loader.loadRes(filename, cc.TextAsset, function (error, result) { if (error) { callback(Error("status " + error)) } else { callback(null, result); } }) } return } // if a node-like filesystem is present, try it first but fall back to XHR if nothing is found. if (!options.xhr && fs && fs.readFile) ... }
主要就是那个判断是否cocos环境的地方。
我参考这个方式,试着加载了一下,root有值,但是按照这个例子,root.lookupType 返回的是null,后来参考论坛的教程,改了一下加载方式,直接使用cocos提供的加载资源的api加载:
cc.loader.loadRes(pbFiles, cc.TextAsset, function (err, protos) { if (err) { bg.Log.e("load proto error ==> ", err) return } let pr = protobuf.parse(tex) bg.Log.i(pr) let rs = pr.root.lookupType("awesomepackage.AwesomeMessage") bg.Log.i(rs) let payLoad = { awesome_field:"hello sdfsefewg"} let msg = rs.create(payLoad) bg.Log.i("msg ", msg) let buf = rs.encode(msg).finish() bg.Log.i("buf ", buf) let decode = rs.decode(buf) bg.Log.i("decode ", JSON.stringify(decode)) })
使用这种方案,可以正确的加载到,打印出来的值都是正常的。
如果有多个.proto文件,则可以用cc.loader.loadResArray批量加载,在加载结果里面,将protobuf.parse解析的结果,以文件名为key存储起来:
for (let proto of protos) { window.protoMessageMap[proto._name] = protobuf.parse(proto) }
在多.proto文件下,发、收消息的时候,使用哪个协议,我这里使用的是一个笨方法:
// 发送消息 let protocal = "包名.messageName" for (let k in protoMessageMap) { let obj = protoMessageMap[k] let found = obj.root.lookup(protocal) if (null != found) { let bodyMsg = found.create(data) let body = found.encode(bodyMsg).finish()
break } } // 接收消息 let protocal = "包名.messageName" for (let k in protoMessageMap) { let obj = protoMessageMap[k] let found = obj.root.lookup(protocal) if (null != found) { let msg = found.decode(pbmsg.body) break } }
遍历一边所有加载进来的 .proto,然后再做后续操作。这个地方有个问题就是,如果消息名重复了,就会有问题。后期再看看有没有什么解决方案吧。
另外,creator用websocket进行消息通讯时,接收到的数据包用protobu解包,需要将数据包转一下格式:
found.decode(new Uint8Array(event.data))
不然protobuf会报错。