lua 获取网络时间_Lua动态解析Protocol buffer

11456923d5909a7394740dc0a0be8dba.png

Protocol Buffer是Google开源的一个他们自己内部RPC和数据存储的一种格式。在消息协议处理上有很多优势。主要是数据存储速度快,消耗资源少。具体的protocol buffer的数据存储原理还有和类似xml,json的性能对比之类的,网上也有很多文章了,在这里我就不详细写了。

由于protocol buffer在性能上的优势和消息上的便利性,也常常被用于游戏开发中,常见作为前后端交互的消息格式。

基于Lua的协议设计目标

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

message 

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

message 

那么问题就来了。

协议描述文件的更新

首先我们第一个要实现的目标是支持动态加载编译协议文件。也就是当客户端的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

到这一步,如果成功的话,此时我们已经动态编译了这个proto文件了,这个描述文件中所定义的类的一些信息已经被缓存起来了。

那么下一步如何获取到具体的消息呢?

先放上一张具体的消息对应的protocol buffer类图

64ac90102d855a6ad144781c10f513ea.png
图片来源Wiki

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

const 

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

google

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

if 

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

protocol buffer到Lua table

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

// 用之前的Login举例,反序列化之后,Lua应该收到这样一个table

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

const 

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

for 

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

if 

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

void 

如果是message嵌套message的情况呢?一样也能使用protocol buffer提供的接口轻松处理。

序列化的原理和处理思路基本上是一样的。我们只需要根据file和message创建一个动态Message对象,然后,遍历Lua table的key值,然后根据table的key和value填充刚才New出来的动态Message对象,在调用SerializeToString序列化一下就可以了。

譬如,类似上文说的Login消息。脚本如要传递如下消息给服务器,username值为"test",password值为"test123",那么只需要传递

local 

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

local 

就可以了。而C++层无需重新编译。

其中有些细节还需要注意一下,类似required类型的消息,C++是需要判断脚本table是否包含了required,应给应用层较好的提示信息,容错处理要稍微注意。

至此,已经实现我们最初的动态编译protocol buffer到Lua脚本的目标了!

[1]

参考

  1. ^本文已获得作者授权,最早发布于 https://www.jianshu.com/p/703fb3f5397a
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值