Protocol buffers(c++版)

Protobuf 语法学习笔记

语法规则指南

字段类型

支持C++所有的基本类型:

.proto 类型说明C++类型
doubledouble
floatfloat
int32可变长度编码,对负数编码效率低,如果字段可能为负值请使用sint32int32
int64可变长度编码,对负数编码效率低,如果字段可能为负值请使用sint64int64
uint32可变长度编码uint32
uint64可变长度编码uint64
sint32可变长度编码,对负数编码效率高int32
sint64可变长度编码,对负数编码效率高int64
fixed32固定4字节,对值大于2^28编码效率比uint32更高uint32
fixed64固定8字节,对值大于2^56编码效率比uint64更高uint64
sfixed32固定4字节uint32
sfixed64固定8字节uint64
boolbool
string字符串必须包含UTF8编码或者是7-bit ASCII文本,长度不超过2^32string
bytes长度不超过2^32的任意字节序列string

还支持复合类型、自定义类型(基本上和C/C++类型定义类型一样)。

字段编号

消息定义中的每个字段都有一个唯一的编号,这些字段编号用于在消息二进制格式中标识字段,并且在使用消息类型后不应更改。最小编号1,最大编号2^29 - 1 ,注意:

1-15范围内的字段编号需要一个字节来进行编码,包括字段编号、字段类型;

16-2047范围内的字段编号占用俩个字节。因此应该为频繁出现的消息元素保留1到15,也为扩展预留字段编号;

19000 到 19999是协议内部保留字段编号,不能使用;

字段规则

类型意义
singular单个值(不能超过一个值)
repeated多个值(可以为0个,动态数组)
oneof类似C/C++ union

默认值

类型默认值
string
bytes
boolfalse
number0
enum默认为第一个enum的值,必须为0
repeated
message取决于语言

枚举

枚举常量必须定义在32位整数范围内,由于enum使用varint编码,负值效率低,因此不推荐定义为负数:

message Foo{
	enum Status{
		option allow_alias = true; // 允许枚举定义相同值的别名,如果不打开此选项编译会报错
		UNKNOWN = 0; // 必须为0,兼容proto2 和proto3
		SUCCESS = 1;
		OK = 1;
		FAILED = 2;
		NG = 2;
	}
	Status status = 3;
}

保留值

如果通过完全删除枚举条目或将其注释掉来更新枚举类型,将来的用户可以在对类型进行自己的更新时重用该数值。如果他们稍后加载相同的旧版本,这可能会导致严重问题.proto,包括数据损坏、隐私错误等。确保不会发生这种情况的一种方法是指定已删除条目的数值(和/或名称,这也可能导致 JSON 序列化问题)为reserved. 如果将来有任何用户尝试使用这些标识符,protocol buffer 编译器会抱怨。max您可以使用关键字指定保留的数值范围达到最大可能值

不能在同一语句中混合字段名称和数值:

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

复合类型

同c/c++复合类型:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

嵌套类型

同C/C++ 嵌套结构体/类定义:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

可在message之外的使用其它消息类型,如上定义的meesage SearchResponse,可以引用其定义的类型:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

当然也可以任意嵌套:

message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

导入定义

同C/C++的include,protobuf可以通过 import .proto来自其他文件的定义来使用它们。要导入另一个的定义,请在文件顶部添加一个 import 语句:.proto

import "myproject/other_protos.proto";

更新一个已有的消息

  1. 不要更改任何现有字段的字段编号。
  2. 如果您添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码解析。您应该记住这些元素的[默认值],以便新代码可以正确地与旧代码生成的消息交互。类似地,新代码创建的消息可以由旧代码解析:旧二进制文件在解析时会忽略新字段。有关详细信息,请参阅[未知字段]部分。
  3. 只要在更新的消息类型中不再使用字段编号,就可以删除字段。您可能想要重命名该字段,可能添加前缀“OBSOLETE_”,或将字段编号设为[保留],以便您的未来用户.proto不会意外重用该编号。
  4. int32uint32int64uint64bool都是兼容的——这意味着您可以将字段从其中一种类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从不适合相应类型的线路中解析出一个数字,您将获得与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果一个 64 位数字被读取为int32,它将被截断为 32 位)。
  5. sint32并且sint64彼此兼容,但与其他整数类型兼容。
  6. string并且bytes只要字节是有效的 UTF-8 就兼容。
  7. bytes如果字节包含消息的编码版本,则嵌入消息是兼容的。
  8. fixed32sfixed32fixed64兼容sfixed64
  9. 对于stringbytes和 消息字段,optional与 兼容repeated。给定重复字段的序列化数据作为输入,optional如果它是原始类型字段,则期望此字段的客户端将采用最后一个输入值,如果它是消息类型字段,则合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常不安全**。**数字类型的重复字段可以以[打包]optional格式序列化,当需要字段时将无法正确解析。
  10. enumint32, uint32, int64, 和uint64有线格式兼容(请注意,如果不合适,值将被截断)。但是请注意,当消息被反序列化时,客户端代码可能会以不同的方式处理它们:例如,无法识别的 proto3enum类型将保留在消息中,但是当消息被反序列化时如何表示则取决于语言。Int 字段总是只保留它们的值。
  11. 将单个值更改为new oneof的成员是安全且二进制兼容的。oneof如果您确定没有代码一次设置多个字段,则将多个字段移动到一个新字段中可能是安全的。将任何字段移动到现有字段oneof中是不安全的。

未知字段

未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,我们重新引入了保留未知字段以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

Any类型

同C/C void;

消息类型允许您将Any消息用作嵌入类型,而无需它们的 .proto 定义。AnAny包含任意序列化消息 as bytes,以及充当全局唯一标识符并解析为该消息类型的 URL。要使用该Any类型,需要[导入] google/protobuf/any.proto.

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

定义消息类型的默认类型 URL 是type.googleapis.com/_packagename_._messagename_

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

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details); //打包Any类型

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error); // 解包Any类型
    ... processing network_error ...
  }
}

如果已经熟悉[proto2 语法],则Any可以保存任意 proto3 消息,类似于可以允许[扩展]的 proto2 消息。

oneof

同C/C++ union;

可以添加任意类型,map和repeated除外:

message SampleMessage {
  oneof test_oneof { //union 
    string name = 4;
    SubMessage sub_message = 9;
  }
}
属性
  1. 设置 oneof 字段将自动清除 oneof 的所有其他成员,设置了多个 oneof 字段,则只有设置的最后一个字段仍有值。

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    message.mutable_sub_message();   // Will clear name field.
    CHECK(!message.has_name());
    
  2. 如果同一个oneof中多次写入,则只能解析看到最后一次的。

  3. 不能为repeated类型。

  4. 反射API使用oneof。

  5. 如果将 oneof 字段设置为默认值(例如将 int32 oneof 字段设置为 0),则会设置该 oneof 字段的“大小写”,并且该值将在线上序列化。

  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
  1. 在 C++ 中,俩个消息msg1msg2对象通过调用Swap()oneofs已经设置的值 :则msg1msg2将具有name:

    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是不是已经写入字段。

标签重用问题
  • 增加或删除字段 oneof:在消息被序列化和解析后,您可能会丢失一些信息(某些字段将被清除)。但是,可以安全地将单个字段移动到的oneof 中,并且如果知道只设置了一个字段,则可以移动多个字段。

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

  • 拆分或合并 oneof:这与移动常规字段有类似的问题。

Map

同C++ map类,语法:

map<key_type, value_type> map_field = N;

​ .其中key_type可以是任何整数或字符串类型(除浮点类型、标量类型、bytes类型)。注意, enum 不是有效的key_type. value_type可以是任何类型,除了另外一个Map。

例如创建一个project映射,每一个project与一个字符串映射:

map<string, Project> projects = 3;
属性
  • Map字段不能是repeated类型。
  • Map不能依赖特定的顺序
  • Map生成.proto文本格式时,按键排序(数字key按数字排序)。
  • Map从文本格式解析时,如果有重复字段可能会失败;从合并过的解决会取最后一个值。
  • Map中如果为映射字段提供键但没有值,则该字段被序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,类型的默认值是序列化的,而在其他语言中则没有序列化。
向后兼容问题

map 语法在网络上等同于以下内容,因此不支持 map 的协议缓冲区实现仍然可以处理数据:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}
repeated MapFieldEntry map_field = N;

任何支持映射的协议缓冲区实现都必须生成和接受上述定义可以接受的数据。

Package

可以将可选package说明符添加到.proto文件中,以防止协议消息类型之间的名称冲突。

package foo.bar;
message Open { ... }

然后,可以在定义消息类型的字段时使用包说明符:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

包说明符影响生成代码的方式取决于语言,同命名空间 。

包名称的解析

与C++类似,由内->外。protobuf编译器通过解析导入的.proto文件来解析所有类型名称。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它有不同的范围规则。

Service

在 RPC(远程过程调用)系统中使用定义的消息类型,可以在一个.proto文件中定义一个 RPC 服务接口,并且protobuf编译器将以选择的语言生成服务接口代码和存根。例如,定义一个 RPC 服务,它的方法接受你的SearchRequest并返回 SearchResponse,你可以在你的.proto文件中定义它,如下所示:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

与protobuf一起使用的最直接的 RPC 系统是[gRPC]:由 Google 开发的一种语言和平台中立的开源 RPC 系统。gRPC 特别适用于协议缓冲区,并允许.proto使用特殊的协议缓冲区编译器插件直接从文件中生成相关的 RPC 代码。

如果不想使用 gRPC,也可以将协议缓冲区与自己的 RPC 实现一起使用。[可以在Proto2 语言指南]中找到更多相关信息。

还有一些正在进行的第三方项目为 Protocol Buffers 开发 RPC 实现。有关我们了解的项目的链接列表,请参阅[第三方附加组件 wiki 页面]。

JSON映射

Proto3 支持 JSON 中的规范编码,从而更容易在系统之间共享数据。下表中按类型描述了编码,

如果 JSON 编码的数据中缺少某个值,或者其值为null,则在解析到protobuf为null将被解释为适当的[默认值。]如果某个字段在协议缓冲区中具有默认值,则在 JSON 编码的数据中默认将其省略以节省空间。实现可以提供选项以在 JSON 编码的输出中发出具有默认值的字段。

proto3json说明
messageobject{“fooBar”: v, “g”: null, …}生成 JSON 对象。消息字段名称映射到 lowerCamelCase 并成为 JSON 对象键。如果指定了field 选项,则指定的值将用作键。解析器接受 lowerCamelCase 名称(或选项指定的名称)和原始 proto 字段名称。是所有字段类型的可接受值,并被视为相应字段类型的默认值。json_name``json_name``null
enumstring“FOO_BAR”使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。
map<K,V>object{“k”: v, …}所有键都转换为字符串。
repeated Varray[v, …]null视为空列表[]
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON 值将是使用带有填充的标准 base64 编码编码为字符串的数据。接受带有/不带有填充的标准或 URL 安全的 base64 编码。
int32, fixed32, uint32number1, -10, 0JSON 值将是一个十进制数。接受数字或字符串
int64、fixed64、uint64string“1”, “-10”JSON 值将是一个十进制字符串。接受数字或字符串。
float, doublenumber1.1, -10.0, 0, “NaN”, “Infinity”JSON 值将是一个数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”之一。接受数字或字符串。也接受指数符号。-0 被认为等同于 0。
Anyobject{“@type”: “url”, “f”: v, … }如果Any包含具有特殊 JSON 映射的值,则将按如下方式转换:. 否则,会将值转换为 JSON 对象,并插入字段以指示实际数据类型。{"@type": xxx, "value": yyy}``"@type
Timestampstring“1972-01-01T10:00:20.021Z”使用 RFC 3339,其中生成的输出将始终进行 Z 归一化,并使用 0、3、6 或 9 位小数。也接受除“Z”之外的偏移量。
Durationstring“1.000340012s”, “1s”生成的输出始终包含 0、3、6 或 9 个小数位数,具体取决于所需的精度,后跟后缀“s”。接受任何小数位(没有也可以),只要它们符合纳秒精度并且需要后缀“s”。
Structobject{ … }任何 JSON 对象
Wrapper typesvarious types2, “2”, “foo”, true, “true”, null, 0, …包装器在 JSON 中使用与包装的原始类型相同的表示,除了null在数据转换和传输期间允许和保留。
FieldMaskstring“f.fooBar,h”
ListValuearray[foo, bar, …]
ValuevalueAny JSON value(null_value 、number_value 、string_value、bool_value、struct_value、list_value)
NullValuenulljson null
Emptyobject{}一个空的 JSON 对象
JSON 选项
  • Emit fields with default values:默认情况下,proto3 JSON 输出中会省略具有默认值的字段。实现可以提供一个选项来覆盖此行为并使用其默认值输出字段。
  • Ignore unknown fields:Proto3 JSON 解析器默认应该拒绝未知字段,但可能会提供一个选项来忽略解析中的未知字段。
  • 使用 proto 字段名称而不是 lowerCamelCase 名称:默认情况下,proto3 JSON 打印机应将字段名称转换为 lowerCamelCase 并将其用作 JSON 名称。实现可能会提供一个选项来使用 proto 字段名称作为 JSON 名称。Proto3 JSON 解析器需要接受转换后的 lowerCamelCase 名称和 proto 字段名称。
  • 将枚举值作为整数而不是字符串发出:默认情况下,在 JSON 输出中使用枚举值的名称。可以提供一个选项来代替使用枚举值的数值。

OPTION

文件中的各个声明.proto可以用许多选项进行注释。选项不会改变声明的整体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表在 中定义google/protobuf/descriptor.proto

optimize_for(文件选项):可以设置为SPEEDCODE_SIZELITE_RUNTIME。这会通过以下方式影响 C++ 和 Java 代码生成器(可能还有第三方生成器):

  • SPEED(默认):protocol buffer 编译器将生成用于对消息类型进行序列化、解析和执行其他常见操作的代码。这段代码是高度优化的。

  • CODE_SIZE:protocol buffer 编译器将生成最少的类,并将依赖共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此生成的代码将比 with 小得多SPEED,但操作会更慢。类仍将实现与模式中完全相同的公共 API SPEED。此模式在包含大量.proto文件且不需要所有文件都非常快的应用程序中最有用。

    option optimize_for = CODE_SIZE;
    
  • LITE_RUNTIME:protocol buffer 编译器将生成仅依赖于“lite”运行时库的类(libprotobuf-lite,而不是libprotobuf)。lite 运行时比完整库小得多(大约小一个数量级),但省略了描述符和反射等某些功能。这对于在手机等受限平台上运行的应用程序特别有用。编译器仍将生成所有方法的快速实现,就像它在SPEED模式中所做的那样。生成的类只会实现MessageLite每种语言的接口,它只提供完整Message接口方法的子集。

  • cc_enable_arenas(文件选项):为 C++ 生成的代码启用竞技场分配。

  • objc_class_prefix(文件选项):设置 Objective-C 类前缀,该前缀添加到所有来自此 .proto 的 Objective-C 生成的类和枚举中。没有默认值。[应该使用Apple 推荐的]介于 3-5 个大写字符之间的前缀。请注意,所有 2 个字母前缀均由 Apple 保留。

  • eprecated(字段选项):如果设置为true,则表示该字段已弃用,不应被新代码使用。在大多数语言中,这没有实际效果。在 Java 中,这成为@Deprecated注解。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段未被任何人使用并且您希望阻止新用户使用它,请考虑将字段声明替换为保留语句。

    int32 old_field = 6 [deprecated = true];
    

自定义选项

Protocol Buffers 还允许您定义和使用自己的选项。大多数人不需要的高级功能。(proto2)

生成类(代码)

要生成需要使用.proto文件中定义的消息类型的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,如下命令:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH指定.proto解析import指令时在其中查找文件的目录。如果省略,则使用当前目录。--proto_path多次传递该选项可以指定多个导入目录;他们将被按顺序搜索。-I=_IMPORT_PATH_可以用作 的简写形式--proto_path

  • 提供一个或多个输出指令

    • --cpp_out生成 C++ 代码DST_DIR
    • --java_out生成 Java 代码DST_DIR
    • --kotlin_outDST_DIR
    • --python_out生成 Python 代码DST_DIR
    • --go_out生成 Go 代码DST_DIR
    • --ruby_out生成 Ruby 代码DST_DIR
    • --objc_outDST_DIR
    • --csharp_out生成 C# 代码DST_DIR
    • --php_out生成 PHP 代码DST_DIR

    如果输出文件已经存在,它将被覆盖;编译器无法将文件添加到现有存档中。.zip``.jar``.jar

  • 必须提供一个或多个.proto文件作为输入。.proto可以一次指定多个文件。尽管文件是相对于当前目录命名的,但每个文件必须位于其中一个IMPORT_PATHs 中,以便编译器可以确定其规范名称。

其他说明

  1. 一个proto文件中可以定义多个message;

  2. 注释方式同C/C++的样式;

学习来源于官网,仅作为学习笔记!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值