如果现有的消息类型不再满足您的所有需求。例如,您希望消息格式有一个额外的字段,但您仍然希望使用使用旧格式创建的代码,请不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。只需记住以下规则:
一、不要更改任何现有字段的字段编号。
二、 如果您添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。类似地,新代码创建的消息可以由旧代码解析:旧二进制文件在解析时会忽略新字段。
这个本人使用自己定义的protobuf文件已经验证了,以下是一个具体的例子:
- syntax = "proto3";
-
- message BaseInfo
- {
- int32 Id = 1;
- string Name = 2;
- string CPU = 3;
-
- }
- message Info
- {
- string Version = 1;
- int64 InfoId = 2;
- repeated BaseInfo info = 3;
-
- }
其中定义了两个消息:BaseInfo和Info,Info中使用了BaseInfo,我们将其初始化后序列化,序列化函数如下:
- void InfoSet(BaseInfo &baseinfo, const int id, const string name, const string CPU)
- {
- baseinfo.set_id(id);
- //baseinfo.set_name(name);
- baseinfo.set_cpu(CPU);
- }
因为在Info中BaseInfo为可重复字段,因此可以向一个Info类数据中多次添加BaseInfo类数据,然后version和infoid信息只有一个,初始化代码如下:
- BaseInfo baseinfo1;
- BaseInfo baseinfo2;
- BaseInfo baseinfo3;
- InfoSet(baseinfo1, 1, "Apple-1", "Intel");
- InfoSet(baseinfo2, 2, "Apple-2", "M1");
- InfoSet(baseinfo3, 3, "Apple-3", "M2");
- Info info1;
- info1.add_info()->CopyFrom(baseinfo1);
- info1.add_info()->CopyFrom(baseinfo2);
- info1.add_info()->CopyFrom(baseinfo3);
- string version = "1.0.0.1";
- info1.set_version(version);
- info1.set_infoid(128)
我们将此信息write到一个文件,为另一个Info类信息做准备。write函数如下:
- void write2stringSTS(Info info,string &sg)
- {
- //string tmp = sg;
- bool a = info.SerializeToString(&sg);
-
- std::ofstream file1("binrary.data", ios::out|ios::binary);
- file1 << sg;
- file1.close();
- //return sg;
- }
write成功后我们读取出来,让另一个Info类反序列化:
- std::string sg = read2stringSTS();
- Info testinfo;
- bool a =testinfo.ParseFromString(sg);
- cout << sg << endl;
- cout << testinfo.mutable_info(1)->id() << endl;//1是序号,代表testinfo的repeated的第2个元素
- cout << testinfo.version() << endl;
- cout << testinfo.infoid() << endl;
read函数如下:
- string read2stringSTS()
- {
- ifstream file;
- file.open("binrary.data", ios::in|ios::binary);
- stringstream s;
- s << file.rdbuf();
- file.close();
- return s.str();
-
- }
接着我们在原有的proto中更新一些信息:
- syntax = "proto3";
-
- message BaseInfo
- {
- int32 Id = 1;
- string CPU = 3;
- int64 CoreNumbers = 5;
- string CoreName = 17;
- }
-
-
- message Info
- {
- string Version = 1;
- int64 InfoId = 2;
- repeated BaseInfo info = 3;
-
- }
更新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类型:
- import "google/protobuf/any.proto";
-
- message ErrorStatus {
- string message = 1;
- repeated google.protobuf.Any details = 2;
- }
不同的语言实现将支持运行时库助手以类型安全的方式打包和解包 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的使用:
- // Storing an arbitrary message type in Any.
- NetworkErrorDetails details = ...;
- ErrorStatus status;
- status.add_details()->PackFrom(details);
-
- // Reading an arbitrary message from Any.
- ErrorStatus status = ...;
- for (const Any& detail : status.details()) {
- if (detail.Is
()) { - NetworkErrorDetails network_error;
- detail.UnpackTo(&network_error);
- ... processing network_error ...
- }
- }
如果您有一条包含多个字段的消息,并且最多同时设置一个字段,您可以强制执行此行为并使用 oneof 功能节省内存。
设置 oneof 字段将自动清除 oneof 的所有其他成员。 因此,如果您设置了多个 oneof 字段,则只有您设置的最后一个字段仍有值。
oneof 字段与常规字段一样,除了一个 oneof 共享内存中的所有字段外,最多可以同时设置一个字段。 设置 oneof 的任何成员会自动清除所有其他成员。 您可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 中设置的值(如果有),具体取决于您选择的语言。
请注意,如果设置了多个值,则由 proto 中的 order 确定的最后一个设置值将覆盖所有以前的值。
oneof的使用:
- message SampleMessage {
- oneof test_oneof {
- string name = 4;
- SubMessage sub_message = 9;
- }
- }
1. 设置一个oneof字段将自动清除oneof的所有其他成员。因此,如果你设置了几个oneof字段,只有你设置的最后一个字段仍然有值。
2. 如果解析器在线路上遇到同一个成员的多个成员,则在解析的消息中只使用最后一个看到的成员。
3. oneof 不能重复。
4. 反射 API 适用于 oneof 字段。
5. 如果您将一个oneof字段设置为默认值(例如将一个int32的oneof字段设置为0),则该oneof字段的“case”将被设置,并且该值将在线路上序列化。
6. 如果您使用 C++,请确保您的代码不会导致内存崩溃。 以下示例代码将崩溃,因为 sub_message 已通过调用 set_name() 方法删除。
- SampleMessage message;
- SubMessage* sub_message = message.mutable_sub_message();
- message.set_name("name"); // Will delete sub_message
- sub_message->set_... // Crashes here
7. 同样在 C++ 中,如果您使用 oneofs Swap() 两条消息,则每条消息都会以另一个的 oneof 情况结束:在下面的示例中,msg1 将有一个 sub_message,而 msg2 将有一个名称。
- SampleMessage msg1;
- msg1.set_name("name");
- SampleMessage msg2;
- msg2.mutable_sub_message();
- msg1.swap(&msg2);
- CHECK(msg1.has_sub_message());
- CHECK(msg2.has_name());
添加或删除其中一个字段时要小心。 如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 不同版本中的字段。 没有办法区分,因为无法知道线路上的未知字段是否是 oneof 的成员。
1. 将字段移到或移出oneof:在消息被序列化和解析后,您可能会丢失一些信息(一些字段将被清除)。但是,您可以安全地将一个字段移动到一个新的字段中,如果知道只设置了一个字段,则可以移动多个字段。
2. 删除 oneof 字段并重新添加:这可能会在消息被序列化和解析后清除您当前设置的 oneof 字段。
3. 拆分或合并其中一个:这与移动常规字段有类似的问题。
如果你想创建一个关联映射作为数据定义的一部分,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实现依然可以处理数据:
- message MapFieldEntry {
- key_type key = 1;
- value_type value = 2;
- }
-
- repeated MapFieldEntry map_field = N;
区分命名空间,防止命名冲突。
在c++中, 生成类包裹在c++ namespace中. 例如, Open将在namespace foo::bar中.
Proto3支持JSON格式的标准编码, 让在系统之间分享数据变得容易. 编码在下面的表格中以type-by-type的基本原则进行描述。
1. 发出具有默认值的字段
2. 忽略未知字段
3. 使用 proto 字段名称而不是 lowerCamelCase 名称
4. 将枚举值作为整数而不是字符串发出