• Protobuf:Updating A Message (Protobuf更新.proto文件后用读取旧的信息流)


            如果现有的消息类型不再满足您的所有需求。例如,您希望消息格式有一个额外的字段,但您仍然希望使用使用旧格式创建的代码,请不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。只需记住以下规则:

    一、不要更改任何现有字段的字段编号。

    二、 如果您添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。类似地,新代码创建的消息可以由旧代码解析:旧二进制文件在解析时会忽略新字段。

            这个本人使用自己定义的protobuf文件已经验证了,以下是一个具体的例子:

    1. syntax = "proto3";
    2. message BaseInfo
    3. {
    4. int32 Id = 1;
    5. string Name = 2;
    6. string CPU = 3;
    7. }
    8. message Info
    9. {
    10. string Version = 1;
    11. int64 InfoId = 2;
    12. repeated BaseInfo info = 3;
    13. }

            其中定义了两个消息:BaseInfo和Info,Info中使用了BaseInfo,我们将其初始化后序列化,序列化函数如下:

    1. void InfoSet(BaseInfo &baseinfo, const int id, const string name, const string CPU)
    2. {
    3. baseinfo.set_id(id);
    4. //baseinfo.set_name(name);
    5. baseinfo.set_cpu(CPU);
    6. }

            因为在Info中BaseInfo为可重复字段,因此可以向一个Info类数据中多次添加BaseInfo类数据,然后version和infoid信息只有一个,初始化代码如下:

    1. BaseInfo baseinfo1;
    2. BaseInfo baseinfo2;
    3. BaseInfo baseinfo3;
    4. InfoSet(baseinfo1, 1, "Apple-1", "Intel");
    5. InfoSet(baseinfo2, 2, "Apple-2", "M1");
    6. InfoSet(baseinfo3, 3, "Apple-3", "M2");
    7. Info info1;
    8. info1.add_info()->CopyFrom(baseinfo1);
    9. info1.add_info()->CopyFrom(baseinfo2);
    10. info1.add_info()->CopyFrom(baseinfo3);
    11. string version = "1.0.0.1";
    12. info1.set_version(version);
    13. info1.set_infoid(128)

           

            我们将此信息write到一个文件,为另一个Info类信息做准备。write函数如下: 

    1. void write2stringSTS(Info info,string &sg)
    2. {
    3. //string tmp = sg;
    4. bool a = info.SerializeToString(&sg);
    5. std::ofstream file1("binrary.data", ios::out|ios::binary);
    6. file1 << sg;
    7. file1.close();
    8. //return sg;
    9. }

            write成功后我们读取出来,让另一个Info类反序列化:

    1. std::string sg = read2stringSTS();
    2. Info testinfo;
    3. bool a =testinfo.ParseFromString(sg);
    4. cout << sg << endl;
    5. cout << testinfo.mutable_info(1)->id() << endl;//1是序号,代表testinfo的repeated的第2个元素
    6. cout << testinfo.version() << endl;
    7. cout << testinfo.infoid() << endl;

            read函数如下:

    1. string read2stringSTS()
    2. {
    3. ifstream file;
    4. file.open("binrary.data", ios::in|ios::binary);
    5. stringstream s;
    6. s << file.rdbuf();
    7. file.close();
    8. return s.str();
    9. }

         

              接着我们在原有的proto中更新一些信息:

    1. syntax = "proto3";
    2. message BaseInfo
    3. {
    4. int32 Id = 1;
    5. string CPU = 3;
    6. int64 CoreNumbers = 5;
    7. string CoreName = 17;
    8. }
    9. message Info
    10. {
    11. string Version = 1;
    12. int64 InfoId = 2;
    13. repeated BaseInfo info = 3;
    14. }

            更新proto后,用新的代码读取旧的信息流:

            

            第二行的空白是读取CoreNumbers,虽然旧的流中没有,但是新的代码默认将其设置为空字符串,如果是数值类型(int32 ''')那么就设置为0。

    三、只要在更新的消息类型中不再使用字段编号,就可以删除字段。 您可能想要重命名该字段,可能添加前缀“OBSOLETE_”,或保留字段编号,以便您的 .proto 的未来用户不会意外重用该编号。

    四、int32、uint32、int64、uint64 和 bool 都是兼容的——这意味着您可以将字段从其中一种类型更改为另一种类型,而不会破坏前向或后向兼容性。如果从不适合相应类型的线路中解析出一个数字,您将获得与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果一个 64 位数字被读取为int32,它将被截断为 32 位)。

    五、sint32 和 sint64 相互兼容,但与其他整数类型不兼容。

    六、只要字节是有效的 UTF-8,字符串和字节就兼容。

    七、fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。

    八、对于字符串、字节和消息字段,可选与重复兼容。 给定一个重复字段的序列化数据作为输入,如果它是一个原始类型字段,那么期望这个字段是可选的客户端将采用最后一个输入值,或者如果它是一个消息类型字段,则合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常不安全。 数字类型的重复字段可以以打包格式序列化,当需要可选字段时,将无法正确解析。

             我们将repeated字段去掉,仍然读取原有的信息流,需要将cout << testinfo.mutable_info(1)->id() << endl;中的序号去掉,不然报错。

            

            可以看到输出的id为3,就是最后一个加入的BaseInfo元素的id值。

    九、如果字节包含消息的编码版本,则嵌入式消息与字节兼容。

    十、Enum兼容int32, uint32, int64,和uint64的线格式(注意,如果值不适合,将被截断)。然而,请注意,当消息被反序列化时,客户端代码可能会以不同的方式对待它们:例如,无法识别的proto3 enum类型将保留在消息中,但当消息被反序列化时,这是如何表示的取决于语言。Int字段总是只保留它们的值。

    十一、enum在线格式上兼容int32、uint32、int64和uint64(注意将单个可选字段或扩展名更改为新字段的成员是安全的,并且二进制兼容。如果您确定没有代码一次设置多个字段,那么将多个字段移动到一个新的字段中可能是安全的。将任何字段移动到已有的字段中都是不安全的。同样,将单个字段之一更改为可选字段或扩展也是安全的。

    未知字段:

    未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。 例如,当旧二进制文件用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

    最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,我们重新引入了保留未知字段以匹配 proto2 行为。 在 3.5 及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

    Any类型:

    1. import "google/protobuf/any.proto";
    2. message ErrorStatus {
    3. string message = 1;
    4. repeated google.protobuf.Any details = 2;
    5. }

            不同的语言实现将支持运行时库助手以类型安全的方式打包和解包 Any 值——例如,在 Java 中,Any 类型将具有特殊的 pack() 和 unpack() 访问器,而在 C++ 中则有 PackFrom() 和 UnpackTo () 方法:

            If you are already familiar with proto2 syntax, the Any can hold arbitrary proto3 messages, similar to proto2 messages which can allow extensions.

            如果您已经熟悉proto2语法,那么Any可以保存任意proto3消息,类似于proto2消息,它可以允许扩展。

    Any的使用:

    1. // Storing an arbitrary message type in Any.
    2. NetworkErrorDetails details = ...;
    3. ErrorStatus status;
    4. status.add_details()->PackFrom(details);
    5. // Reading an arbitrary message from Any.
    6. ErrorStatus status = ...;
    7. for (const Any& detail : status.details()) {
    8. if (detail.Is()) {
    9. NetworkErrorDetails network_error;
    10. detail.UnpackTo(&network_error);
    11. ... processing network_error ...
    12. }
    13. }

    Oneof字段

            如果您有一条包含多个字段的消息,并且最多同时设置一个字段,您可以强制执行此行为并使用 oneof 功能节省内存。

            设置 oneof 字段将自动清除 oneof 的所有其他成员。 因此,如果您设置了多个 oneof 字段,则只有您设置的最后一个字段仍有值。

            oneof 字段与常规字段一样,除了一个 oneof 共享内存中的所有字段外,最多可以同时设置一个字段。 设置 oneof 的任何成员会自动清除所有其他成员。 您可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 中设置的值(如果有),具体取决于您选择的语言。

            请注意,如果设置了多个值,则由 proto 中的 order 确定的最后一个设置值将覆盖所有以前的值。

            oneof的使用:

    1. message SampleMessage {
    2. oneof test_oneof {
    3. string name = 4;
    4. SubMessage sub_message = 9;
    5. }
    6. }

    Oneof特性:

           1设置一个oneof字段将自动清除oneof的所有其他成员。因此,如果你设置了几个oneof字段,只有你设置的最后一个字段仍然有值。

           2. 如果解析器在线路上遇到同一个成员的多个成员,则在解析的消息中只使用最后一个看到的成员。

            3. oneof 不能重复。

            4. 反射 API 适用于 oneof 字段。

            5. 如果您将一个oneof字段设置为默认值(例如将一个int32的oneof字段设置为0),则该oneof字段的“case”将被设置,并且该值将在线路上序列化。

            6. 如果您使用 C++,请确保您的代码不会导致内存崩溃。 以下示例代码将崩溃,因为 sub_message 已通过调用 set_name() 方法删除。

    1. SampleMessage message;
    2. SubMessage* sub_message = message.mutable_sub_message();
    3. message.set_name("name"); // Will delete sub_message
    4. sub_message->set_... // Crashes here

            7. 同样在 C++ 中,如果您使用 oneofs Swap() 两条消息,则每条消息都会以另一个的 oneof 情况结束:在下面的示例中,msg1 将有一个 sub_message,而 msg2 将有一个名称。

    1. SampleMessage msg1;
    2. msg1.set_name("name");
    3. SampleMessage msg2;
    4. msg2.mutable_sub_message();
    5. msg1.swap(&msg2);
    6. CHECK(msg1.has_sub_message());
    7. CHECK(msg2.has_name());

     向后兼容特性:

            添加或删除其中一个字段时要小心。 如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 不同版本中的字段。 没有办法区分,因为无法知道线路上的未知字段是否是 oneof 的成员。

            1. 将字段移到或移出oneof:在消息被序列化和解析后,您可能会丢失一些信息(一些字段将被清除)。但是,您可以安全地将一个字段移动到一个新的字段中,如果知道只设置了一个字段,则可以移动多个字段。

            2. 删除 oneof 字段并重新添加:这可能会在消息被序列化和解析后清除您当前设置的 oneof 字段。

            3.  拆分或合并其中一个:这与移动常规字段有类似的问题。

    Map

    Tips

    如果你想创建一个关联映射作为数据定义的一部分,Protobuf提供了一个方便的快捷语法:

    map map_field = N;
    

            其中 key_type 可以是任何整数或字符串类型(因此,除浮点类型和字节之外的任何标量类型)。 请注意,枚举不是有效的 key_type。 value_type 可以是除另一个映射之外的任何类型。

            因此,例如,如果您想创建一个项目映射,其中每个项目消息都与一个字符串键相关联,您可以这样定义它:

    map projects = 3;
    

            提示:

                    1. map字段不能使用repeated。

                    2. map值的线格式排序和map迭代排序是未定义的,因此您不能依赖map项处于特定顺序。

                    3. 为 .proto 生成文本格式时,map按键排序。 数字键是按数字排序的。

                    4. 从连线解析或合并时,如果有重复的映射键,则使用最后看到的键。 从文本格式解析地图时,如果有重复的键,则解析可能会失败。

                    5. 如果您为映射字段提供键但没有值,则该字段被序列化时的行为取决于语言。 在 C++、Java、Kotlin 和 Python 中,类型的默认值是序列化的,而在其他语言中则没有序列化。

            map有关的API:https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.map

     向后兼容

            map语法和下面的代码等同, 因此不支持map的protocol buffers实现依然可以处理数据:

    1. message MapFieldEntry {
    2. key_type key = 1;
    3. value_type value = 2;
    4. }
    5. repeated MapFieldEntry map_field = N;

    Package

            区分命名空间,防止命名冲突。

            在c++中, 生成类包裹在c++ namespace中. 例如, Open将在namespace foo::bar中.

    Json

            Proto3支持JSON格式的标准编码, 让在系统之间分享数据变得容易. 编码在下面的表格中以type-by-type的基本原则进行描述。

    Json Options

            1. 发出具有默认值的字段

            2. 忽略未知字段

            3. 使用 proto 字段名称而不是 lowerCamelCase 名称

            4. 将枚举值作为整数而不是字符串发出

  • 相关阅读:
    3DTiles三维管线数据生产工具试用版
    c++——内联函数,auto关键字,基于范围的for循环,nullptr
    zenmap无法运行扫描操作
    salesforce零基础学习(一百二十一)Limitation篇之Heap Size Limitation
    命令模式(command)
    ARM上市,冲击2023年美股最大IPO
    既要技术制胜,也要体验为王:今天我们需要怎样的WLAN?
    walking机器人仿真教程-应用-实时加载地图实现多楼层导航
    c语言练习84:动态内存管理
    NLP涉及技术原理和应用简单讲解【二】:paddle(分布式训练、AMP自动混合精度训练、模型量化、模型性能分析)
  • 原文地址:https://blog.csdn.net/weixin_44120785/article/details/126891534