protocol-buffers 语法指南 (proto3) 机

2021-12-21  本文已影响0人  百里驹

定义消息类型

标量值类型

默认值

枚举

使用其他消息类型

嵌套类型

更新消息类型

未知字段

任何

一个

地图

套餐

定义服务

JSON 映射

选项

生成你的类

本指南介绍了如何使用协议缓冲区语言来构建协议缓冲区数据,包括.proto文件语法以及如何从.proto文件生成数据访问类。它涵盖了协议缓冲区语言的proto3版本:有关proto2语法的信息,请参阅Proto2 语言指南

这是一份参考指南——有关使用本文档中描述的许多功能的分步示例,请参阅您所选语言的教程(目前仅适用于 proto2;更多 proto3 文档即将推出)。

定义消息类型

首先让我们看一个非常简单的例子。假设您要定义搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的特定结果页面以及每页的多个结果。这是.proto您用来定义消息类型的文件。

syntax = "proto3";

message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;}

该文件的第一行指定您正在使用proto3语法:如果您不这样做,协议缓冲区编译器将假定您使用的是proto2。这必须是文件的第一个非空、非注释行。

所述SearchRequest消息定义指定了三个字段(名称/值对),一个用于每条数据要在此类型的消息包括。每个字段都有一个名称和一个类型。

指定字段类型

在上面的例子中,所有的字段都是标量类型:两个整数(page_number和result_per_page)和一个字符串(query)。但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。

分配字段编号

如您所见,消息定义中的每个字段都有一个唯一的编号。这些字段编号用于在消息二进制格式中标识您的字段,一旦您的消息类型被使用,就不应更改。请注意,1 到 15 范围内的字段编号占用一个字节进行编码,包括字段编号和字段类型(您可以在协议缓冲区编码中找到更多相关信息)。16 到 2047 范围内的字段编号占用两个字节。因此,您应该为非常频繁出现的消息元素保留数字 1 到 15。请记住为将来可能添加的频繁出现的元素留出一些空间。

您可以指定的最小字段编号为 1,最大字段编号为 2 ^29 - 1 或 536,870,911。您也不能使用数字 19000 到 19999(FieldDescriptor::kFirstReservedNumber到FieldDescriptor::kLastReservedNumber),因为它们是为 Protocol Buffers 实现保留的 - 如果您在.proto. 同样,您不能使用任何以前保留的字段编号。

指定字段规则

消息字段可以是以下之一:

单数:格式正确的消息可以有零个或一个此字段(但不超过一个)。这是 proto3 语法的默认字段规则。

repeated:该字段可以在格式良好的消息中重复任意次数(包括零次)。重复值的顺序将被保留。

在 proto3 中,repeated标量数值类型的字段packed默认使用编码。

您可以packed在Protocol Buffer Encoding 中找到有关编码的更多信息。

添加更多消息类型

可以在单个.proto文件中定义多种消息类型。如果您要定义多个相关消息,这很有用 - 例如,如果您想定义与您的SearchResponse消息类型相对应的回复消息格式,您可以将其添加到相同的.proto:

message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;}

message SearchResponse {...}

添加评论

要向.proto文件添加注释,请使用 C/C++ 样式//和/* ... */语法。

/* SearchRequest represents a search query, with pagination options to

* indicate which results to include in the response. */

message SearchRequest {string query = 1;int32 page_number = 2;  // Which page number do we want?int32 result_per_page = 3;  // Number of results to return per page.}

保留字段

如果您通过完全删除某个字段或将其注释掉来更新消息类型,则未来的用户可以在对类型进行自己的更新时重复使用该字段编号。如果他们以后加载相同的旧版本,这可能会导致严重的问题.proto,包括数据损坏、隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号(和/或名称,这也可能导致 JSON 序列化问题)是reserved. 如果任何未来的用户尝试使用这些字段标识符,协议缓冲区编译器会抱怨。

message Foo {

reserved

2, 15, 9 to 11;

reserved

"foo", "bar";}

请注意,您不能在同一reserved语句中混合使用字段名称和字段编号。

什么是从你的?.proto

当您在 上运行协议缓冲区编译器时.proto,编译器会以您选择的语言生成代码,您需要使用文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流,并从输入流解析您的消息。

对于C++,编译器从 each生成一个.hand.cc文件,文件中.proto描述的每个消息类型都有一个类。

对于Java,编译器.java为每个消息类型生成一个带有类的文件,以及Builder用于创建消息类实例的特殊类。

对于Kotlin,除了 Java 生成的代码之外,编译器还.kt为每种消息类型生成一个文件,其中包含可用于简化创建消息实例的 DSL。

Python稍有不同——Python 编译器生成一个模块,其中包含 中每种消息类型的静态描述符,.proto然后与元类一起使用以在运行时创建必要的 Python 数据访问类。

对于Go,编译器为.pb.go文件中的每种消息类型生成一个类型的文件。

对于Ruby,编译器会生成一个.rb带有 Ruby 模块的文件,其中包含您的消息类型。

对于Objective-C,编译器从 each生成一个pbobjc.handpbobjc.m文件,文件中.proto描述的每个消息类型都有一个类。

对于C#,编译器.cs从 each生成一个文件.proto,其中包含文件中描述的每种消息类型的类。

对于Dart,编译器会为.pb.dart文件中的每种消息类型生成一个带有类的文件。

您可以按照所选语言的教程(即将推出 proto3 版本)了解有关使用每种语言的 API 的更多信息。有关更多 API 详细信息,请参阅相关API 参考(proto3 版本也即将推出)。

标量值类型

标量消息字段可以具有以下类型之一——该表显示了.proto文件中指定的类型,以及自动生成的类中的相应类型:

当您在Protocol Buffer Encoding 中序列化您的消息时,您可以找到有关如何编码这些类型的更多信息。

[1] Kotlin 使用来自 Java 的相应类型,即使是无符号类型,以确保在混合 Java/Kotlin 代码库中的兼容性。

[2]在 Java 中,无符号 32 位和 64 位整数使用它们的有符号对应物表示,最高位简单地存储在符号位中。

[3]在所有情况下,为字段设置值将执行类型检查以确保其有效。

[4] 64 位或无符号 32 位整数在解码时总是表示为 long,但如果在设置字段时给出 int,则可以是 int。在所有情况下,该值必须适合设置时表示的类型。见[2]。

[5] Python 字符串在解码时表示为 unicode,但如果给出 ASCII 字符串,则可以是 str(这可能会发生变化)。

[6] 64 位机器上使用整数,32 位机器上使用字符串。

默认值

解析消息时,如果编码的消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:

对于字符串,默认值为空字符串。

对于字节,默认值为空字节。

对于 bool,默认值为 false。

对于数字类型,默认值为零。

对于enums,默认值是第一个定义的 enum value,它必须是 0。

对于消息字段,未设置该字段。它的确切值取决于语言。有关详细信息,请参阅生成的代码指南

重复字段的默认值为空(通常是相应语言的空列表)。

请注意,对于标量消息字段,一旦解析了消息,就无法判断字段是否明确设置为默认值(例如,布尔值是否设置为false)或根本没有设置:您应该牢记这一点在定义消息类型时。例如,false如果您不希望默认情况下也发生该行为,则不要设置一个布尔值来开启某些行为。还要注意的是,如果一个标消息字段设置为默认值,该值将不会在电线上连载。

有关默认值如何在生成的代码中工作的更多详细信息,请参阅所选语言的生成代码指南

枚举

当您定义消息类型时,您可能希望其中一个字段只有一个预定义的值列表。例如,假设你想添加一个corpus字段每个SearchRequest,其中语料库可以UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO。您可以通过enum为每个可能的值添加一个常量到您的消息定义中来非常简单地做到这一点。

在以下示例中,我们添加了一个具有所有可能值的enum调用Corpus和一个类型为的字段Corpus:

message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;enum Corpus {

UNIVERSAL

= 0;

WEB

= 1;

IMAGES

= 2;

LOCAL

= 3;

NEWS

= 4;

PRODUCTS

= 5;

VIDEO

= 6;}Corpus corpus = 4;}

如您所见,Corpus枚举的第一个常量映射到零:每个枚举定义都必须包含一个映射到零的常量作为其第一个元素。这是因为:

必须有一个零值,以便我们可以使用 0 作为数字默认值

零值需要是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。

您可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将该allow_alias选项设置为true,否则协议编译器将在找到别名时生成错误消息。

message MyMessage1 {enum EnumAllowingAlias {option allow_alias = true;

UNKNOWN

= 0;

STARTED

= 1;

RUNNING

= 1;}}message MyMessage2 {enum EnumNotAllowingAlias {

UNKNOWN

= 0;

STARTED

= 1;// RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.}}

枚举常量必须在 32 位整数范围内。由于enum值在线路上使用varint 编码,负值效率低下,因此不推荐使用。您可以enum在消息定义中定义s,如上例所示,enum也可以在外部定义 – 这些s 可以在.proto文件中的任何消息定义中重用。您还可以使用enum语法将在一条消息中声明的类型用作另一条消息中的字段类型_MessageType_._EnumType_。

当您在.proto使用 的上运行协议缓冲区编译器时enum,生成的代码将具有对应enum的 Java、Kotlin 或 C++ 或EnumDescriptorPython的特殊类,用于在运行时创建一组具有整数值的符号常量 -生成的类。

**注意:** 生成的代码可能会受到特定语言的枚举数限制(一种语言低至数千)。请查看您计划使用的语言的限制。

在反序列化期间,无法识别的枚举值将保留在消息中,尽管在反序列化消息时如何表示取决于语言。在支持值超出指定符号范围的开放枚举类型的语言中,例如 C++ 和 Go,未知枚举值只是作为其底层整数表示存储。在 Java 等具有封闭枚举类型的语言中,枚举中的 case 用于表示无法识别的值,并且可以使用特殊访问器访问底层整数。在任何一种情况下,如果消息被序列化,无法识别的值仍将与消息一起序列化。

有关如何enum在应用程序中使用 message的更多信息,请参阅所选语言的生成代码指南

保留值

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

enum Foo {

reserved

2, 15, 9 to 11, 40 to max;

reserved

"FOO", "BAR";}

请注意,您不能在同一reserved语句中混合使用字段名称和数值。

使用其他消息类型

您可以使用其他消息类型作为字段类型。例如,假设您想Result在每条SearchResponse消息中包含消息 - 为此,您可以在其中定义一个Result消息类型.proto,然后指定一个类型为Resultin的字段SearchResponse:

message SearchResponse {repeated Result results = 1;}

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

导入定义

在上面的例子中,Result消息类型是在同一个文件中定义的SearchResponse——如果你想用作字段类型的消息类型已经在另一个.proto文件中定义了怎么办?

您可以.proto通过导入其他文件中的定义来使用它们。要导入 another.proto的定义,请在文件顶部添加一个 import 语句:

import "myproject/other_protos.proto";

默认情况下,您只能使用来自直接导入.proto文件的定义。但是,有时您可能需要将.proto文件移动到新位置。.proto您可以.proto在旧位置放置一个占位符文件,以使用该import public概念将所有导入转发到新位置,而不是直接移动文件并在一次更改中更新所有调用站点。

请注意,公共导入功能在 Java 中不可用。

import public任何导入包含该import public语句的原型的代码都可以传递依赖项。例如:

// new.proto// All definitions are moved here// old.proto// This is the proto that all clients are importing.import public "new.proto";import "other.proto";// client.protoimport "old.proto";// You use definitions from old.proto and new.proto, but not other.proto

协议编译器使用-I/--proto_path标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果没有给出标志,它会在调用编译器的目录中查找。通常,您应该将--proto_path标志设置为项目的根目录,并对所有导入使用完全限定名称。

使用 proto2 消息类型

可以导入proto2消息类型并在 proto3 消息中使用它们,反之亦然。但是,proto2 枚举不能直接在 proto3 语法中使用(如果导入的 proto2 消息使用它们也没关系)。

嵌套类型

您可以在其他消息类型中定义和使用消息类型,如下例所示——这里的Result消息是在SearchResponse消息内部定义的:

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

如果要在其父消息类型之外重用此消息类型,则将其称为_Parent_._Type_:

message SomeOtherMessage {SearchResponse.Result result = 1;}

您可以随意嵌套消息:

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

更新消息类型

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

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

如果添加新字段,使用“旧”消息格式的代码序列化的任何消息仍可以由新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。类似地,由新代码创建的消息可以由旧代码解析:旧二进制文件在解析时简单地忽略新字段。有关详细信息,请参阅未知字段部分。

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

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

sint32并且sint64彼此兼容但与其他整数类型兼容。

string并且bytes只要字节是有效的 UTF-8 就兼容。

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

fixed32与兼容sfixed32,并fixed64用sfixed64。

对于string、bytes和 消息字段,optional与 兼容repeated。给定重复字段的序列化数据作为输入,optional如果它是原始类型字段,则期望此字段的客户端将采用最后一个输入值,如果它是消息类型字段,则合并所有输入元素。请注意,这不是一般的数值类型,包括布尔变量和枚举安全。数字类型的重复字段可以被序列化为packed格式,在optional预期字段时不会被正确解析。

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

将单个值更改为 的成员oneof是安全且二进制兼容的。oneof如果您确定没有代码一次设置多个,则将多个字段移入一个新字段可能是安全的。将任何字段移动到现有字段中oneof都是不安全的。

未知字段

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

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

任何

该Any消息类型,可以使用邮件作为嵌入式类型,而不必自己.proto定义。AnAny包含一个任意序列化的消息 as bytes,以及一个 URL,该 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()andunpack()访问器,而在 C++ 中有PackFrom()andUnpackTo()方法:

// 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>()) {NetworkErrorDetails network_error;

detail

.UnpackTo(&network_error);... processing network_error ...}}

目前,用于处理 Any 类型的运行时库正在开发中

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

一个

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

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

使用 Oneof

要在您的 oneof 中定义 oneof,您.proto可以使用oneof后跟 oneof 名称的关键字,在这种情况下test_oneof:

message SampleMessage {

oneof test_oneof

{string name = 4;SubMessage sub_message = 9;}}

然后将 oneof 字段添加到 oneof 定义中。您可以添加任何类型的map字段,字段和repeated字段除外。

在您生成的代码中,oneof 字段与常规字段具有相同的 getter 和 setter。您还可以获得一种特殊的方法来检查设置了 oneof 中的哪个值(如果有)。您可以在相关API 参考 中找到有关所选语言的 oneof API 的更多信息。

一个特点

设置 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());

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

oneof 不能是repeated。

反射 API 适用于 oneof 字段。

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

如果您使用 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

同样在 C++ 中,如果你的Swap()两条消息带有 oneofs,则每条消息都会以另一个的 oneof 情况结束:在下面的示例中,msg1将有一个sub_message和msg2将有一个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:这与移动常规字段有类似的问题。

地图

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

map<key_type, value_type> map_field = N;

...其中key_type可以是任何整数或字符串类型(因此,除了浮点类型和之外的任何标量类型bytes)。请注意, enum 不是有效的key_type. 的value_type可以是任何类型的除另一地图。

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

map<string, Project> projects = 3;

地图字段不能是repeated。

地图值的连线格式排序和地图迭代排序是未定义的,因此您不能依赖地图项的特定顺序。

为 a 生成文本格式时.proto,地图按键排序。数字键按数字排序。

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

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

生成的地图 API 目前可用于所有 proto3 支持的语言。您可以在相关API 参考 中找到有关所选语言的地图 API 的更多信息。

向后兼容

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

message MapFieldEntry {

key_type key

= 1;

value_type value

= 2;}

repeated MapFieldEntry map_field = N;

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

套餐

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

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

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

message Foo {...

foo

.bar.Open open = 1;...}

包说明符影响生成代码的方式取决于您选择的语言:

C++ 中,生成的类被包装在 C++ 命名空间中。例如,Open将在命名空间中foo::bar。

JavaKotlin 中,该包用作 Java 包,除非您option java_package在.proto文件中明确提供。

Python 中, package 指令被忽略,因为 Python 模块是根据它们在文件系统中的位置组织的。

Go 中,包用作 Go 包名称,除非您option go_package在.proto文件中明确提供。

Ruby 中,生成的类被包裹在嵌套的 Ruby 命名空间中,转换为所需的 Ruby 大写样式(第一个字母大写;如果第一个字符不是字母,PB_则在前面)。例如,Open将在命名空间中Foo::Bar。

C# 中,包在转换为 PascalCase 后用作命名空间,除非您option csharp_namespace在.proto文件中明确提供。例如,Open将在命名空间中Foo.Bar。

包和名称解析

协议缓冲区语言中的类型名称解析的工作方式与 C++ 类似:首先搜索最内部的范围,然后搜索下一个最内部的范围,依此类推,每个包都被认为是其父包的“内部”。一个领先的'.' (例如,.foo.bar.Baz) 表示从最外层范围开始。

协议缓冲区编译器通过解析导入的.proto文件来解析所有类型名称。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它具有不同的范围规则。

定义服务

如果您想在 RPC(远程过程调用)系统中使用您的消息类型,您可以在.proto文件中定义一个 RPC 服务接口,协议缓冲区编译器将以您选择的语言生成服务接口代码和存根。因此,例如,如果您想使用接受您SearchRequest并返回 a的方法定义 RPC 服务SearchResponse,您可以在您的.proto文件中按如下方式定义它:

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

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

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

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

JSON 映射

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

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

原型3JSONJSON 示例笔记

信息目的{"fooBar": v, "g": null, …}生成 JSON 对象。消息字段名称映射到小驼峰并成为 JSON 对象键。如果指定了字段选项,则指定的值将用作键。解析器接受lowerCamelCase 名称(或选项指定的名称)和原始原型字段名称。是所有字段类型的可接受值,并被视为相应字段类型的默认值。json_namejson_namenull

枚举细绳"FOO_BAR"使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。

地图<K,V>目的{"k": v, …}所有键都转换为字符串。

重复V大批[v, …]null被接受为空列表[]。

布尔值真假true, false

细绳细绳"Hello World!"

字节base64 字符串"YWJjMTIzIT8kKiYoKSctPUB+"JSON 值将是使用带填充的标准 base64 编码编码为字符串的数据。接受带/不带填充的标准或 URL 安全 base64 编码。

int32、fixed32、uint32数字1, -10, 0JSON 值将是一个十进制数。接受数字或字符串。

int64、fixed64、uint64细绳"1", "-10"JSON 值将是一个十进制字符串。接受数字或字符串。

浮动,双数字1.1, -10.0, 0, "NaN", "Infinity"JSON 值将是数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”之一。接受数字或字符串。指数符号也被接受。-0 被认为等价于 0。

任何object{"@type": "url", "f": v, … }如果 Any 包含一个具有特殊 JSON 映射的值,它将按如下方式转换:. 否则,该值将转换为 JSON 对象,并插入该字段以指示实际数据类型。{"@type": xxx, "value": yyy}"@type"

时间戳细绳"1972-01-01T10:00:20.021Z"使用 RFC 3339,其中生成的输出将始终进行 Z 归一化并使用 0、3、6 或 9 位小数。也接受除“Z”以外的偏移量。

期间细绳"1.000340012s", "1s"生成的输出始终包含 0、3、6 或 9 个小数位,具体取决于所需的精度,后跟后缀“s”。接受任何小数位数(也没有),只要它们适合纳秒精度并且需要后缀“s”。

结构object{ … }任何 JSON 对象。见。struct.proto

包装类型各种类型2, "2", "foo", true, "true", null, 0, …包装器在 JSON 中使用与包装基元类型相同的表示,除了null在数据转换和传输期间允许和保留。

场掩码细绳"f.fooBar,h"见。field_mask.proto

列表值大批[foo, bar, …]

价值价值任何 JSON 值。查看google.protobuf.Value了解详情。

空值空值JSON 空

空的目的{}一个空的 JSON 对象

JSON 选项

proto3 JSON 实现可能提供以下选项:

发出具有默认值的字段:默认情况下,proto3 JSON 输出中会省略具有默认值的字段。实现可能会提供一个选项来覆盖此行为并使用其默认值输出字段。

忽略未知字段:默认情况下,Proto3 JSON 解析器应拒绝未知字段,但可能会提供一个选项来忽略解析中的未知字段。

使用 proto 字段名称而不是 lowerCamelCase 名称:默认情况下,proto3 JSON 打印机应将字段名称转换为 lowerCamelCase 并将其用作 JSON 名称。一个实现可以提供一个选项来使用 proto 字段名称作为 JSON 名称。Proto3 JSON 解析器需要接受转换后的小写字母名称和 proto 字段名称。

将枚举值作为整数而不是字符串发出:默认情况下,在 JSON 输出中使用枚举值的名称。可以提供一个选项来代替使用枚举值的数值。

选项

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

有些选项是文件级选项,这意味着它们应该写在顶级范围内,而不是在任何消息、枚举或服务定义中。一些选项是消息级别的选项,这意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、一个字段、服务类型和服务方法上;然而,目前没有任何有用的选择。

以下是一些最常用的选项:

java_package(文件选项):您要用于生成的 Java/Kotlin 类的包。如果文件中未java_package给出显式选项.proto,则默认情况下将使用 proto 包(使用文件中的“package”关键字指定.proto)。然而,proto 包通常不会成为好的 Java 包,因为 proto 包不会以反向域名开头。如果不生成 Java 或 Kotlin 代码,则此选项无效。

option java_package = "com.example.foo";

java_outer_classname(文件选项):要生成的包装 Java 类的类名(以及文件名)。如果文件中没有明确java_outer_classname指定.proto,类名将通过将.proto文件名转换为驼峰式大小写来构造(因此foo_bar.proto变为FooBar.java)。如果该java_multiple_files选项被禁用,则所有其他类/枚举/等。为.proto文件生成的将在此外部包装器 Java 类中生成为嵌套类/枚举/等。如果不生成 Java 代码,则此选项无效。

option java_outer_classname = "Ponycopter";

java_multiple_files(文件选项):如果为 false,则只.java为该.proto文件生成一个文件,所有 Java 类/枚举/等。为顶级消息、服务和枚举生成的消息将嵌套在外部类中(请参阅 参考资料java_outer_classname)。如果为 true,.java将为每个 Java 类/枚举/等生成单独的文件。为顶级消息、服务和枚举生成,并且为此.proto文件生成的包装器 Java 类将不包含任何嵌套类/枚举/等。这是一个布尔选项,默认为false。如果不生成 Java 代码,则此选项无效。

option java_multiple_files = true;

optimize_for(文件选项):可以设置为SPEED、CODE_SIZE、 或LITE_RUNTIME。这会通过以下方式影响 C++ 和 Java 代码生成器(以及可能的第三方生成器):

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

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

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

option optimize_for = CODE_SIZE;

cc_enable_arenas(文件选项):为 C++ 生成的代码启用arena 分配

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

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

int32 old_field = 6 [deprecated = true];

自定义选项

Protocol Buffers 还允许您定义和使用您自己的选项。这是大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2 语言指南了解详细信息。请注意,创建自定义选项使用extensions,仅允许用于 proto3 中的自定义选项。

生成你的类

要生成 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,您需要使用.proto文件中定义的消息类型,您需要protoc在.proto. 如果您尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。对于 Go,您还需要为编译器安装一个特殊的代码生成器插件:您可以在 GitHub 上的golang/protobuf存储库中找到此插件和安装说明。

协议编译器的调用方式如下:

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。有关更多信息,请参阅C++ 生成的代码参考

--java_out生成 Java 代码DST_DIR。有关更多信息,请参阅Java 生成的代码参考

--kotlin_out生成额外的 Kotlin 代码DST_DIR。有关更多信息,请参阅Kotlin 生成的代码参考

--python_out生成 Python 代码DST_DIR。有关更多信息,请参阅Python 生成的代码参考

--go_out生成 Go 代码DST_DIR。有关更多信息,请参阅Go 生成的代码参考

--ruby_out生成 Ruby 代码DST_DIR。有关更多信息,请参阅Ruby 生成的代码参考

--objc_out在DST_DIR. 有关更多信息,请参阅Objective-C 生成的代码参考

--csharp_out生成 C# 代码DST_DIR。有关更多信息,请参阅C# 生成的代码参考

--php_out生成 PHP 代码DST_DIR。有关更多信息,请参阅PHP 生成的代码参考

为方便起见,如果DST_DIR以.zip或结尾.jar,编译器会将输出写入具有给定名称的单个 ZIP 格式存档文件。.jar输出还将根据 Java JAR 规范的要求提供清单文件。请注意,如果输出存档已经存在,它将被覆盖;编译器不够聪明,无法将文件添加到现有存档中。

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

上一篇下一篇

猜你喜欢

热点阅读