Protobuf 的 proto3发布也有挺长一段时间了。现在很多新项目慢慢转变用proto3来开发。这篇文章主要记录一下我在给pbc写对proto3支持时的一些信息,也许对其他童鞋也有点助益。抛砖引玉一下。
简介
pbc是云风开发的一个纯C的读写protobuf的很小巧的库,配合上它提供的lua-5.1和lua-5.3的binding可以很容易地在lua里完成对pb文件的注册和打解包。应该很多人都知道这个组件。
但是后来云风自己又发明了个sproto,然后主推在他的skynet框架中使用sproto,于是pbc就不再有功能维护了。
我们之前的也尝试直接使用了proto3,也是因为在迁移期,所以并没有使用全部的特性。但是仍然有一些向前不兼容的细节需要处理一下,所以有了这个改造
Proto2和Proto3的差异
因为主要目的是兼容,所以下面会列出proto3得不同之处,并且会标注处理方法。
移除 required属性和默认值
这部分原先本身有很多逻辑容易楼判定,所以影响不大。只是每个字段不再是三态(无/默认值/自定义值),只有两态(默认值/自定义值)。这回影响比如脏数据差分更新得逻辑,因为不再有has_xxx接口来判定数据是否有变更了。不过pbc也有对默认值数据做裁剪,所以这里反而是和pbc不谋而合了,所以这里无需修改。
移除unknown fields(据说以后会加回来)
这里主要影响更新以后客户端和服务器版本不一致得时候得数据处理。本身游戏逻辑在协议设计得时候就会考虑这个问题,如果非强制更新,会允许缺失或者忽略一些数据。所以本身影响不大。
移除extension,增加Any类型代替
同样,目前我还没有用到过这个特性。也暂时忽略。
枚举类型语义变更,现在必须提供默认值
这个影响proto文件得语法,这个对proto文件得改造量有点大。但对pbc也没什么影响。
增加了新的类型map、timestamp等
这些类型我大致看了下实现,基本上就是当成message/bytes来用,proto2是可以正常解出得。只是多做了一层结构化而已。现在得pbc即便不支持,也不影响以前得使用方式,只不过得手动打解包一层。而底层得protobuf得基础数据结构并没有变化。而且我对pbc底层结构并没有那么熟,所以也没太多时间做完这个后再去做各项测试。而且一般我们逻辑都会自己建立这种索引和结构所以不太用得到。
官方支持json格式和检查utf-8编码
这个就是方便一点,以前我们自己写过一个protobuf到json得中间件。另外很多protobuf得代码里写死了UTF-8。所以加个检查也是对得。对兼容性也没啥影响。
不再支持Group
这个我一直觉得很鸡肋,去掉也好。
所有数字类型的repeated字段现在默认是packed=true的了。
protobuf的repeated字段有两种处理方式:第一种是由多个key-value对组成,也就是说repeated的数据中,key可能会出现多次;第二种是先有一个varint,表示个数,后面跟N个value。具体编码可以参见我以前写得 《理解Protobuf的数据编码规则》。前一种就是packed=false,反之后一种就是packed=true。这里会影响解包时的组织结构,所以是一个需要修改pbc的地方。
C++ API的重要更新:允许自定义内存分配区
其他语言的我没看,C++的众多变化里我也就觉得这一个比较重要。这是可以自定义内存分配区。因为以前protobuf的message的嵌套结构,都是new出来的。估计是这样多了以后内存碎片和分配性能都比较受影响吧,所以多了这么个类似内存池的东西。感觉还是蛮有用的。虽然我一直用jemalloc所以也不太care这个malloc的开销(只要别乱搞,这里的分配开销和逻辑比任然是九牛一毛)。
大体上差异就这么多了,当然后面的版本会再有些修订也未可知。但是总体看来,要做到打解包的兼容性适配,只有移除需要改的地方,就是repeated字段那里。其他的也就是proto文件的语法有些变化,其他的都还兼容。
pbc改造
涉及的代码就一个文件: register.c,改成如下的样子。
static void
_register_field(struct pbc_rmessage * field, struct _field * f, struct _stringpool *pool) {
int origin_label;
int packed;
f->id = pbc_rmessage_integer(field, "number", 0 , 0);
f->type = pbc_rmessage_integer(field, "type", 0 , 0); // enum
origin_label = pbc_rmessage_integer(field, "label", 0, 0) - 1; // LABEL_OPTIONAL = 0
f->label = origin_label;
// 最优情况是能判定出pb文件是proto2还是proto3。
// 但是pb文件里似乎并没有这种信息,所以proto2和proto3的库选择上只能二选一了。
switch(f->type) { // 就是这里获取到field之后需要看看是否是数字类型,如果是数字类型,那么默认的repeated字段要改为packed类型。
case PTYPE_DOUBLE:
case PTYPE_FLOAT:
case PTYPE_INT64:
case PTYPE_SINT64:
case PTYPE_INT32:
case PTYPE_SINT32:
case PTYPE_UINT32:
case PTYPE_ENUM:
case PTYPE_UINT64:
case PTYPE_FIXED32:
case PTYPE_SFIXED32:
case PTYPE_SFIXED64:
case PTYPE_FIXED64:
case PTYPE_BOOL:
if (f->label == LABEL_REPEATED) {
f->label = LABEL_PACKED;
}
break;
default:
break;
}
if (pbc_rmessage_size(field , "options") > 0) {
struct pbc_rmessage * options = pbc_rmessage_message(field, "options" , 0);
// 这里是为了如果用户显式设定了packed,则以用户设定为准。这里还要处理非数字类型的情况。
if (pbc_rmessage_size(options, "packed") > 0) {
packed = pbc_rmessage_integer(options , "packed" , 0 , NULL);
if (packed) {
f->label = LABEL_PACKED;
} else {
f->label = origin_label; // 这里沿用之前老的模式读出的标签(强制修改前)
// pbc_rmessage_integer只会返回optional/required/repeated
// pbc和protobuf对于packed的信息记录不一样
}
}
}
f->type_name.n = pbc_rmessage_string(field, "type_name", 0 , NULL) +1; // abandon prefix '.'
int vsz;
const char * default_value = pbc_rmessage_string(field, "default_value", 0 , &vsz);
_set_default(pool , f , f->type, default_value , vsz);
}
注释里写得比较清楚了。就不再复述了。
有个题外话,我之前写得转表工具xresloader也很早就接入了proto3,这个工具里已经用proto3了。但是sample里同时提供了proto_v2和proto_v3的示例。这个pbc首先是用来读这里的转表工具的转出数据的。当然用老版本的pbc也可以,就是所有的数字得显式指定packed属性。
BTW
因为顺便要给客户端用,之前手动打iOS和android的包麻烦了点。而且有些为了省事是直接工程导入的,自动构建上很麻烦。所以这次干脆写了个基于cmake的一键打包到iOS和Android的静态库或动态库的脚本,放在根目录下build_android.sh和build_ios.sh。如果要编译lua-binding,则需要指定一下lua-5.1或者lua-5.3的包含目录,android的动态库还需要指定下客户端所使用的lua库目录,反正所有都写在README.md里了。
最后,所有完成的修改都放在了 https://github.com/owent-contrib/pbc/tree/proto_v3 里。这个适配只是做了兼容性适配,最好当然还是实现那些proto3的新数据结构啦。而且这个proto_v3的分支我并没有创建PR推回去。但是前面提到的Android和iOS脚本我Push回去了,云风Merge了第一版,第二版暂时还没Merge。第二版只不过是环境检测和兼容性上的一些优化罢了。