Protobuf动态解析那些事儿

摘要:
收到protobuf数据后,如何自动创建特定的ProtobufMessage对象,然后对其进行反序列化。有关Protobuf的技术介绍,请参阅Google协议缓冲区的在线帮助页面或IBM developerworks上的文章“Google协议缓冲的使用和原理”。protobuffer的动态分析并没有在谷歌protobuffer官网上介绍。有关实现,请参阅淘宝的文章“玩ProtocolBuffers”,其中详细介绍了protobuf的动态分析原理。让我在这里介绍Protobuf类图。

需求背景

在接收到 protobuf 数据之后,如何自动创建具体的 Protobuf Message 对象,再做反序列化。“自动”的意思主要有两个方面:(1)当程序中新增一个 protobuf Message 类型时,这部分代码不需要修改,不需要自己去注册消息类型,不需要重启进程,只需要提供protobuf文件;(2)当protobuf Message修改后,这部分代码不需要修改,不需要自己去注册消息类型,不需要重启进程只需要提供修改后protobuf文件

技术介绍

Protobuf的入门可以参考Google Protocol Buffer 的在线帮助 网页 或者IBM developerwor上的文章《Google Protocol Buffer 的使用和原理》

protobuf的动态解析在google protobuf buffer官网并没有什么介绍。通过google出的一些参考文档可以知道,其实,Google Protobuf 本身具有很强的反射(reflection)功能,可以根据 type name 创建具体类型的 Message 对象,我们直接利用即可,应该就可以满足上面的需求。

实现可以参考淘宝的文章《玩转Protocol Buffers 》,里面对protobuf的动态解析的原理做了详细的介绍,在此我介绍一下Protobuf  class diagram。

 Protobuf动态解析那些事儿第1张

大家通常关心和使用的是图的左半部分:MessageLite、Message、Generated Message Types (Person, AddressBook) 等,而较少注意到图的右半部分:Descriptor, DescriptorPool, MessageFactory。

上图中,其关键作用的是 Descriptor class,每个具体 Message Type 对应一个 Descriptor 对象。尽管我们没有直接调用它的函数,但是Descriptor在“根据 type name 创建具体类型的 Message 对象”中扮演了重要的角色,起了桥梁作用。上图的红色箭头描述了根据 type name 创建具体 Message 对象的过程。

实现

先直接上代码,这个代码来自于《玩转Protocol Buffers 》

#include <iostream>

#include <google/protobuf/descriptor.h>

#include <google/protobuf/descriptor.pb.h>

#include <google/protobuf/dynamic_message.h>

#include <google/protobuf/compiler/importer.h>

  

using namespace std;

using namespace google::protobuf;

using namespace google::protobuf::compiler;

  

int main(int argc,const char *argv[])

{

    DiskSourceTree sourceTree;

    //look up .proto file in current directory

    sourceTree.MapPath("","./");

    Importer importer(&sourceTree, NULL);

    //runtime compile foo.proto

    importer.Import("foo.proto");

  

const Descriptor *descriptor =    importer.pool()->

      FindMessageTypeByName("Pair");

    cout << descriptor->DebugString();

  

    // build a dynamic message by "Pair" proto

    DynamicMessageFactory factory;

    const Message *message = factory.GetPrototype(descriptor);

    // create a real instance of "Pair"

    Message *pair = message->New();

  

    // write the "Pair" instance by reflection

    const Reflection *reflection = pair->GetReflection();

  

    const FieldDescriptor *field = NULL;

    field = descriptor->FindFieldByName("key");

    reflection->SetString(pair, field,"my key");

    field = descriptor->FindFieldByName("value");

    reflection->SetUInt32(pair, field, 1111);

  

    cout << pair->DebugString();

    delete pair;

    return0;

}

 

那我们就来看看上面的代码

1)把本地地址映射为虚拟地址

DiskSourceTree sourceTree;

    //look up .proto file in current directory

sourceTree.MapPath("","./");

2)构造DescriptorPool

Importer importer(&sourceTree, NULL);

    //runtime compile foo.proto

importer.Import("foo.proto");

3)获取Descriptor

const Descriptor *descriptor = importer.pool()->FindMessageTypeByName("Pair");

4)通过Descriptor获取Message

const Message *message = factory.GetPrototype(descriptor);

5根据类型信息使用DynamicMessage new出这个类型的一个空对象

Message *pair = message->New();

6通过Messagereflection操作message的各个字段

  const Reflection *reflection = pair->GetReflection(); 

    const FieldDescriptor *field = NULL;

    field = descriptor->FindFieldByName("key");

    reflection->SetString(pair, field,"my key");

    field = descriptor->FindFieldByName("value");

reflection->SetUInt32(pair, field, 1111);

直接copy上面代码看起来我们上面的需求就满足了,只是唯一的缺点就是每次来个包加载一次配置文件,当时觉得性能应该和读取磁盘的性能差不多,但是经过测试性能极差,一个进程每秒尽可以处理1000多个包,经过分析性能瓶颈不在磁盘,而在频繁调用malloc和free上。

看来我们得重新考虑实现,初步的实现想法:只有protobuf描述文件更新时再重新加载,没有更新来包只需要使用加载好的解析就可以。这个方案看起来挺好的,性能应该不错,经过测试,性能确实可以,每秒可以处理3万左右的包,但是实现中遇到了困难。要更新原来的Message,必须更新Importer和Factory,那么要更新这些东西,就涉及到了资源的释放。经过研究这些资源的释放顺序特别重要,下面就介绍一下protobuf相关资源释放策略。

动态的Message是我们用DynamicMessageFactory构造出来的,因此销毁Message必须用同一个DynamicMessageFactory。 动态更新.proto文件时,我们销毁老的并使用新的DynamicMessageFactory,在销毁DynamicMessageFactory之前,必须先删除所有经过它构造的Message。

  原理:DynamicMessageFactory里面包含DynamicMessage的共享信息,析构DynamicMessage时需要用到。生存期必须保持Descriptor>DynamicMessageFactory>DynamicMessage。 

释放顺序必须是:释放所有DynamicMessage,释放DynamicMessageFactory,释放Importer。

总结

资源释放前,必须要了解资源的构造原理,通过构造原理反推释放顺序,这样就少走弯路、甚至不走。

 

 

参考文献

Google Protocol Buffer 的在线帮助 网页 

一种自动反射消息类型的 Google Protobuf 网络传输方案

《玩转Protocol Buffers 》

《Google Protocol Buffer 的使用和原理》

 

 

 

免责声明:文章转载自《Protobuf动态解析那些事儿》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇C# 如何提取SaveFileDialog的保存路径uniapp中组件属性设置不生效的解决方案下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

C语言之生产者与消费者模型

多线程并发应用程序有一个经典的模型,即生产者/消费者模型。系统中,产生消息的是生产者,处理消息的是消费者,消费者和生产者通过一个缓冲区进行消息传递。生产者产生消息后提交到缓冲区,然后通知消费者可以从中取出消息进行处理。消费者处理完信息后,通知生产者可以继续提供消息。 要实现这个模型,关键在于消费者和生产者这两个线程进行同步。也就是说:只有缓冲区中有消息时,...

Modbus消息帧

  两种传输模式中(ASCII和RTU),传输设备以将Modbus消息转为有起点和终点的帧,这就允许接收的设备在消息起始处开始工作,读地址分配信息,判断哪一个设备被选中(广播方式则传给所以设备),判知何时信息已完成。部分的消息也能侦测到并且错误能设置为返回结果。   1、ASCII帧   使用ASCII模式,消息以冒号(:)字符(ASCII 3AH)开始,...

【RabbitMQ】 RabbitMQ安装

  MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了...

MySQL Error--Error Code

mysql error code(备忘)1005:创建表失败1006:创建数据库失败1007:数据库已存在,创建数据库失败1008:数据库不存在,删除数据库失败1009:不能删除数据库文件导致删除数据库失败1010:不能删除数据目录导致删除数据库失败1011:删除数据库文件失败1012:不能读取系统表中的记录1020:记录已被其他用户修改1021:硬盘剩余...

初入spring boot(五 )websocket

一、广播式   广播式即服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器   1.配置websocket,需要在配置类上使用@EnableWebSocketMessageBroker开启websocket支持,并通过继承AbstractWebSocketMessageBrokerConfigurer类,重写其方法来配置websocket...

钉钉、钉应用(微应用和E应用)开发介绍

钉钉,数字化新工作方式,让工作更简单     目前在钉钉的官网可以看到,超过700万家企业组织正在使用钉钉。笔者也相信,这一数字每天都在增加。获得群众的认可,也是理所当然的,体验过钉钉,就能感觉到,钉钉的考勤、签到、审批、日报、周报、钉消息、视频会议等等做得非常好。笔者已使用钉钉将近4年,能体验到的唯一不足就是PC端的钉钉偶尔会觉得有点卡,当然这个不能排除...