基于protocol buffer和Lua的一种动态消息实现

这是13年做项目的时候,实现的一个技术方案,很早就想拿出来分享了。鉴于后来在网上搜索到的其他的一些protocol buffer的Lua化方案,个人感觉也不尽如人意,用起来颇不遍历,因此今天就将之前的实现方案分享出来。不足的地方,希望大家多多指教。
Protocol Buffer是Google开源的一个他们自己内部RPC和数据存储的一种格式。在消息协议处理上有很多优势。主要是数据存储速度快,消耗资源少。具体的protocol buffer的数据存储原理还有和类似xml,json的性能对比之类的,网上也有很多文章了,在这里我就不详细写了。可以参考这里这里
由于protocol buffer在性能上的优势和消息上的便利性,也常常被用于游戏开发中,常见作为前后端交互的消息格式。

基于Lua的协议设计目标

为了便于在手机上进行更新,不少游戏采用脚本化来实现,Lua就是其中首选。那么,实现protocol buffer的脚本化目标,当然是要能够达到——协议描述文件的任何修改,都无需重新编译C++,脚本可以直接获取这种变化
这样的Lua化才有意义,否则如果每次修改协议文件都需要重新编译一些东西,那岂不是太麻烦了,而且也没有达到我们的目标。
类似的,假设游戏上线之初,我们定义登录消息格式如下

message Login {
    required string username = 1;
    required string password = 2;
};

游戏运营了一段时间,也有了一定的用户量了。这时候,可能我们在用户登录的时候,需要附加一些其他的信息,来为用户提供更好的服务,修改Login消息如下

message Login {
    required string username = 1;
    required string password = 2;
    optional string appendinfo = 3; // 此为新增数据
};

那么问题就来了。

协议描述文件的更新

首先我们第一个要实现的目标是支持动态加载编译协议文件。也就是当客户端的app启动之后,检测到描述文件发生修改,更新至本地之后,app需要即时的加载新的描述文件,并使用新的描述文件来序列化/反序列化消息数据。

客户端app无需重新编译

第二个目标是显而易见的,如果已经支持动态的更新使用新的协议描述文件了,那么无需重新编译也是理所当然的。
目的就是Lua在序列化或者反序列化这些消息的时候,只需向字段里新增appendinfo的内容就可以了,而无需重新编译C++接口。

设计思路和实现

当年做这块的时候,很想找到一些现成的方案来解决,但是发现都不能满足这个要求。于是一发狠,就研究了protocol buffer的相关文档。发现本身pb就有很好的反射机制,略加处理就能满足我们的需求了。最终自己动手实现了一个能满足需求的小玩意儿。

protocol buffer相关原理

要实现这两个目标,有一些原理性的东西还是得讲一讲。
首先就是protocol buffer动态编译相关的一些东西。Protocol buffer主要是通过google::protobuf::compiler::Importer这个类来实现对未知的proto描述文件进行动态编译的。相关还涉及了google::protobuf::compiler::MultiFileErrorCollector类(用于动态编译时搜集描述文件的语法错误,如果存在的话),google::protobuf::compiler::SourceTree类(用于缓存已加载的描述文件)。说明一下,其实SourceTree这个类并不是必须的,但是由于考虑客户端安全性的问题,因此proto协议描述文件是进行了加密的,为了处理这种情况,所以需要用我们派生的SourceTree来AddFile做缓存,做个小小的中间层。
基本原理就是,使用Importer对象的import方法,就可以动态编译一个proto描述文件了。类似如下代码:

google::protobuf::compiler::MultiFileErrorCollector error_collector_;
google::protobuf::compiler::SourceTree source_tree_;

google::protobuf::compiler::Importer importer_(&source_tree_, &error_collector_);
importer_.Import(filename);

到这一步,如果成功的话,此时我们已经动态编译了这个proto文件了,这个描述文件中所定义的类的一些信息已经被缓存起来了。
那么下一步如何获取到具体的消息呢?
先放上一张具体的消息对应的protocol buffer类图


protocol buffer对应相关类

可以看到Descriptor正对应着具体一个message的描述。问题转化为如何根据message的名称获得这个Descriptor。答案是impoter的Descriptor Pool的FindMessageTypeByName方法。

const google::protobuf::Descriptor *desc_ = importer_.pool()->FindMessageTypeByName(msgname);

轻轻松松,这一句就可以获得这个具体message的Descriptor了。再接下来,就是通过Descriptor来创建一个Message实例了。作为本身就具备很强反射机制的protocol buffer,本身就提供了MessageFactory类,支持使用Descriptor来创建消息原型。如下:

google::protobuf::DynamicMessageFactory factory_;
const google::protobuf::Message *proto_type_ = factory_.GetPrototype(desc_);

这样就可以获得一个消息原型了(如果成功的话)。接着使用这个proto_type_来new一个Message,这个Message就是我们想要的动态消息了。至此,工作已经大致完成了1/3了。

if (proto_type_) {
    google::protobuf::Message *dynamic_msg_ = proto_type_->New();
    dynamic_msg_->ParseFromString(data); // 接着我们就可以反序列化二进制数据了
}

没有错误的话,我们获得的dynamic_msg_就包含了我们想要的关于指定message的具体信息。
下面的工作就是如何传递给Lua呢?Lua table是一个很不错的选择。

protocol buffer到Lua table

Lua table天然的设计简直就是传递消息的利器。将message结构转换为lua table实在是再合适不过了(不过如果您有更好的方案可以和我交流)。
大致目标如下:

// 用之前的Login举例,反序列化之后,Lua应该收到这样一个table
t = {
    username = 'xxx',
    password = 'xxx',
}

OK,第一步我们需要知道传递给Lua哪些key-value。祭出反射大杀器Reflection类。每个Message都可以获取到自身的Reflection。再根据这个Reflection我们可以List出所有的Field。然后遍历这些Field做Lua压表key-value的操作。

const google::protobuf::Reflection  *ref_ = msg_->GetReflection();
std::vector< const google::protobuf::FieldDescriptor* > fields_;
ref_->ListFields(*msg_, &fields_); // 存储反序列化之后所有存在的key(如果是optional的话,有可能是不存在的哦)

接着遍历这些存在的数据的key,开始压表

for (unsigned int i = 0; i < fields_.size(); ++i) {
    const google::protobuf::FieldDescriptor *field_ = fields_[i];
    lua_pushstring(tolua_S, field_->name().c_str()); // 首先压入key
    // 接着压表
}

这里需要特殊处理一下的是repeated的这种字段。可能相同的key会存在多项。这种情况下怎么传递给Lua呢?因为Lua的key-value是唯一的。方法也很简单,对这种repeated类型的再压一个table,变成table嵌套table就可以了。

if (field_->is_repeated()) {
    lua_newtable(tolua_S);
    // 递归处理
    GetProto(...);
} else {
    GetProto(...);
}

GetProto函数是自定义的,功能很简单,根据Field的value的数据类型进行数据压栈就好了。

void GetProto(lua_State *tolua_S, google::protobuf::Message *msg_, const google::protobuf::FieldDescriptor* field_) {
    if (field_->is_repeated()) {
        // 单独处理下repeated类型的key的问题
    }
    const google::protobuf::Reflection  *ref_ = msg_->GetReflection();
    // 根据type类型set lua table的key-value
    switch (field_->type()) {
        ...
    }
}

如果是message嵌套message的情况呢?一样也能使用protocol buffer提供的接口轻松处理。
序列化的原理和处理思路基本上是一样的。我们只需要根据file和message创建一个动态Message对象,然后,遍历Lua table的key值,然后根据table的key和value填充刚才New出来的动态Message对象,在调用SerializeToString序列化一下就可以了。
譬如,类似上文说的Login消息。脚本如要传递如下消息给服务器,username值为"test",password值为"test123",那么只需要传递

local t = {
    username = "test",
    password = "test123",
}

这样一个table给C++接口作为参数就可以了。未来有新增的消息,比如之前说的appendinfo,传递的table只需变成

local t = {
    username = "test",
    password = "test123",
    appendinfo = "append",
}

就可以了。而C++层无需重新编译。
其中有些细节还需要注意一下,类似required类型的消息,C++是需要判断脚本table是否包含了required,应给应用层较好的提示信息,容错处理要稍微注意。
之后,空一点我会上传代码。至此,已经实现我们最初的动态编译protocol buffer到Lua脚本的目标了!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容