近日在学习gRPC
框架的相关知识时接触到Protobuf
(protocol-buffers,协议缓冲区),proto3
等知识。网上很多文章/帖子经常把gRPC与proto3放在一起,为避免初学者产生混淆,这里先简单介绍一下gRPC、Protobuf、proto3三者以及他们之间的关系:
本文是系列随笔Language Guide (proto3) | gRPC proto3 语言指南的开篇。本系列翻译自官方文档,翻译过程中尽量客观准确,但或有笔者自己的理解。如您在阅读过程中发现不严谨或不准确之处,还请留言指正。
系列目录:
本指南介绍如何使用协议缓冲区语言来构造协议缓冲区数据,包括文件语法以及如何从文件生成数据访问类。它涵盖了协议缓冲区语言的proto3版本。有关proto2语法的信息,请参阅proto2 语言指南。
这是一个参考指南-如果你想要逐步使用本文档中描述的许多特性,请参阅所选语言的教程(目前仅proto2;更多proto3文档即将提供)。
ps: 本文是proto3版本的通用参考指南,如果你想要使用一些其他的特性,可以参考对应语言(C++、C#、Dart、Go、Java、Python)的教程。但是这些教程仅支持proto2,proto3版本的文档尚待提供。
上面是转载自随笔分类 - proto3 语言指南的开篇,可以通过目录逐篇查看。但单篇的内容较少,感觉不尽兴就接着往下看吧!
首先让我们看一个非常简单的例子。假设您想定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的特定结果页以及每页的结果数。下面是用于定义.proto
消息类型的文件。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
.proto3
文件的第一个非空、非注释行。在上面的例子中,所有的字段都是标量类型:两个整数和一个字符串。但是,你同样可以为你的字段指定复合类型,包括枚举和其他消息类型。
如您所见,消息定义中的每个字段都有一个唯一编号。这些字段编号用于标识消息二进制格式的字段,并且在消息类型投入使用后不应更改。请注意,1到15范围内的字段编号需要一个字节进行编码,编码内包括字段号和字段类型(您可以在协议缓冲区编码中了解更多信息)。16到2047范围内的字段编号需要两个字节(进行编码)。因此,您应该把1到15的消息编号留给非常频繁出现的消息元素。请记住为将来可能添加的频繁出现的元素留出一些空间。可以指定的最小字段号为1,最大字段号为229-1或536870911。您也不能使用数字19000到19999(字段描述符),因为它们是协议缓冲区的保留数字,如果你在你的.proto
中使用了这些数字,编译器会报错。同样,不能使用任何以前保留的字段号。
消息字段可以是以下字段之一:
repeated
(重复的):此字段可以在格式良好的消息中重复任意次数(包括零次),重复值的顺序将被保留。在一个.proto
文件中可以定义多种消息类型。建议您在一个.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
的旧版本,这可能会导致数据损坏、隐私漏洞等严重问题。确保不会发生这种情况的一种方法是使用reserved
关键字指定已删除字段的字段编号为保留编号(也要指定已删除字段名称为保留名称(name),以规避JSON序列化问题)。将来有任何用户试图使用这些字段标识符时,协议缓冲区编译器将报错。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
不能在同一个
reserved
语句中混合使用字段名和字段号。
.proto
文件生成了什么?运行协议缓冲区编译器编译.proto
文件时,编译器将以您选择的语言生成代码。您将需要使用文件中描述的消息类型,来进行获取和设置字段值、将消息序列化为输出流以及从输入流解析消息等工作。
C++
,编译器从每个.proto
成一个.h
和.cc
文件,其中每个文件中描述的每个消息类型都有一个类。Java
,编译器生成一个.Java
文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊生成器类。Python
有点不同–Python编译器生成一个模块,其中包含.proto
中每个消息类型的静态描述符,然后与元类一起使用,在运行时创建必要的Python数据访问类。Go
,编译器为文件中每种消息类型生成一个.pb.go
文件。Ruby
,编译器生成一个.rb
文件,其中包一个Ruby模块,模块中包含你文件中的消息类型。Objective-C
,编译器从每个.proto
生成一个pbobjc.h
和pbobjc.m
文件,并为文件中描述的每个消息类型生成一个类。.proto
生成一个.cs
文件,其中每个消息类型对应一个类。Dart
,编译器从每个.proto
生成一个.pb.dart
文件,其中每个消息类型对应一个类。标量消息字段可以具有以下类型之一 —— 下表显示了.proto文件中指定的类型,以及自动生成的类中相应的类型:
.proto Type | 说明 | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | 使用可变长度编码。负数的编码效率低下 —— 如果您的字段可能有负值,请改用sint32 。 |
int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | 使用可变长度编码。负数的编码效率低下 —— 如果您的字段可能有负值,请改用`sint64`。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
uint32 | 使用可变长度编码。 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | 使用可变长度编码。 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
sint32 | 使用可变长度编码。有符号int值。它们比普通的`int32`更有效地编码**负数**。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | 使用可变长度编码。有符号int值。 它们比普通的`int64`更有效地编码**负数**。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
fixed32 | 注意:总是4个字节(定长编码)。如果值通常大于228,则比`uint32`更有效。 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 注意:总是8个字节(定长编码)。如果值通常大于228,则比`uint64`更有效 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
sfixed32 | 注意:总是4个字节(定长编码)。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 注意:总是8个字节(定长编码)。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | 必须是UTF-8编码或7位ASCII文本的字符串,长度必须小于232字节。 | string | String | str/unicode[4] | string | String (UTF-8) | string | string | String |
bytes | 可以包含任何长度不超过232的任意字节序列。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List |
解析消息时,如果编码的消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
重复字段的默认值为空(通常是适当语言中的空列表)。
请注意,对于标量消息字段,一旦解析了消息,就无法判断字段是显式设置为默认值还是根本没有设置(例如,布尔类型字段值是设置为false,还是默认的false):在定义消息类型时,应该记住这一点。例如,布尔类型字段设置为
false
时会触发一些行为,如果您不希望这些行为在默认情况下也发生,那么就不要使用布尔类型。另请注意,如果标量消息字段设置为其默认值,则不会在连接上序列化该值。
有关默认值如何在生成代码中工作的更多详细信息,请参阅所选语言的生成代码指南。
定义消息类型时,可能希望其中一个字段只包含预定义值列表中的一个。例如,假设您想为每个SearchRequest添加一个corpus
(语料库)字段,其中语料库的值可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。您只需在消息定义中添加一个枚举,每个可能的值都有一个常量,就可以做到这一点。
在下面的示例中,我们添加了一个名为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
枚举的第一个常量映射到零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:
proto2
语法,按照proto2
语法,第一个枚举值是默认值(而不是0)可以通过将相同的值赋给不同的枚举常量来定义别名。为此,您需要将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
使用可变编码,因此负值效率很低,所以不建议使用。您可以在定义的消息内部定义枚举,如上面的示例所示,也可以在外部定义枚举——这些枚举可以在.proto
文件中的任何消息定义中重用。您还可以使用_MessageType_._EnumType_
语法将一条消息中声明的枚举类型用作另一条消息中的字段类型。
当使用协议缓冲区编译器编译一个使用了枚举
的.proto
文件时,对于Java
和C++
来说,会生成一个对应的枚举类型;对于Python
,会生成一个特殊EnumDescriptor
类,用于在运行时生成的类中创建一组具有整数值的符号常量。
警告:生成的代码可能会受到特定语言的枚举数限制(一种语言的枚举数低千)。请检查您计划使用的语言的限制。
在反序列化过程中,无法识别的枚举值将保留在消息中,尽管反序列化消息时如何表示这些值取决于语言。在支持具有指定枚举范围以外值的开枚举类型的语言中,例如C++
和GO
,未知的枚举值被简单地存储为其基础整数表示形式。在具有封闭枚举类型的语言(如Java)中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问基础整数。在这两种情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。
有关如何在应用程序中使用消息枚举的详细信息,请参阅所选语言的生成代码指南。
如果通过完全删除枚举条目或把它注释掉来更新枚举类型,则将来的用户可以在自己更新该类型时重用该数值。如果以后加载相同.proto
的旧版本,这可能会导致严重的数据损坏、隐私漏洞等问题。确保不会发生这种情况的一种方法是指定保留已删除条目的数值(和/或名称,这也可能导致JSON序列化问题)。如果将来有任何用户试图使用这些标识符,协议缓冲区编译器就会报错。您可以使用max
关键字指定您的保留数值范围提高到可能的最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
请注意,不能在同一个
reserved
语句中混合使用字段名和数值。
可以将其他消息类型用作字段类型。例如,假设您希望在每个SearchResponse
消息中包含Result
消息——为此,您可以在同一.proto
中定义Result
消息类型,然后在SearchResponse
中指定类型为Result
的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
请注意,此功能在Java中不可用。
在上面的示例中,Result
消息类型与SearchResponse
在同一个文件中定义——如果要用作字段类型的消息类型已经在另一个.proto
文件中定义了呢?
通过导入其他.proto
文件,可以使用这些文件中的定义。要导入另一个.proto
的定义,请在文件顶部添加一个import
语句:
import "myproject/other_protos.proto";
默认情况下,只能使用直接导入的.proto
文件中的定义。但是,有时可能需要将.proto
文件移动到新位置。不用直接移动.proto
文件并在一次更改中更新所有import
调用,现在可以在旧位置放置一个伪.proto
文件,使用import public
概念将所有导入转发到新位置。任何导入包含import public
语句的proto
的人都可以传递地依赖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.proto
import "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 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;
}
}
}
如果现有的消息类型不再满足您的所有需要(例如,您希望消息格式有一个额外的字段),但是您仍然希望使用用旧格式创建的代码,不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。记住以下规则:
OBSOLETE_
,或者使用reserved
保留字段编号,以便.proto
的未来用户不会意外地重用该编号。int32
、uint32
、int64
、uint64
和bool
都是兼容的——这意味着您可以将字段从一种类型更改为另一种类型,而不会破坏向前或向后的兼容性。如果从不适合于相应类型的Wire
中解析一个数,则将获得与在C++中将该数转换为该类型的相同效果(例如,如果64位数字被读取为一个int32
类型,则它将被截断到32位)。sint32
和sint64
彼此兼容,但与其他整数类型不兼容。UTF-8
编码,string
和bytes
就可以兼容。bytes
包含消息的编码版本,则嵌入的消息与字节兼容。fixed32
与sfixed32
兼容,fixed64
与sfixed64
兼容。string
、bytes
和消息字段,optional
与repeated
兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型字段,则期望该字段为optional
(可选的)字段的客户端将获取最后一个输入值;如果该字段是消息类型字段,则合并所有输入元素。请注意,对于数值类型(包括bool
和enum
),这通常是不安全的。数字类型的重复字段可以按压缩格式序列化,当需要可选字段时,将无法正确解析压缩格式。enum
在wire格式化
方面与int32
、uint32
、int64
和uint64
兼容(请注意,如果值不适合,它们将被截断)。但是,请注意,在反序列化消息时,客户端代码可能会对它们进行不同的处理:例如,未识别的proto3
枚举类型将保留在消息中,但在反序列化消息时如何表示这些类型取决于客户端语言。整型字段总是保持它们的值。oneof
值的成员是安全的,并且二进制兼容。如果您确定没有代码一次设置多个字段,那么将多个字段移到其中一个新oneof
字段中可能是安全的。将任何字段移到现有oneof
字段中都不安全。未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当一个旧二进制代码解析一个带有新字段的新二进制代码发送的数据时,这些新字段在旧二进制代码中成为未知字段。
最初,proto3
消息在解析过程中总是丢弃未知字段,但在3.5版中,我们重新引入了未知字段的保留,以匹配proto2
的行为。在版本3.5和更高版本中,解析期间保留未知字段,并将其包含在序列化输出中。
Any
消息类型允许您将消息作为嵌入类型使用,而无需使用它们的.proto定义。Any
包含一个以字节表示的任意序列化消息,以及一个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_
。
不同的语言实现将支持运行时库助手以类型安全的方式打包和解包任何值——例如,在Java
中,Any
类型有特殊的pack()
和unpack()
访问器,而在C++
中有PackFrom()
和UnpackTo()
方法:
// 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 ...
}
}
目前,用于处理Any
类型的运行库正在开发中。
如果您已经熟悉proto2
语法,那么Any
类型可以保存任意proto3
消息,类似于可扩展的proto2
消息。
如果消息包含多个字段,并且最多只能同时设置一个字段,则可以使用oneof
功能强制执行此行为并节省内存。
oneof
字段与常规字段类似,但oneof
共享内存中的所有字段除外,并且oneof
最多只能同时设置一个字段。设置oneof
的任何成员将自动清除所有其他成员。您可以使用特殊的case()
或WhichOneof()
方法检查oneof
中设置了哪个值(如果有被设置),具体取决于您选择的语言。
oneof
结构要在.proto
中定义oneof
结构,请使用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
的功能SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
wire
上遇到同一个oneof
的多个成员,则在解析的消息中只使用看到的最后一个成员。oneof
结构不能使用repeated
修饰符。oneof
字段。oneof
字段设置为默认值(例如将int32类型的oneof
字段设置为0),则将设置该oneof
字段的case
,并且该值将在wire
上序列化。C++
,请确保代码不会导致内存崩溃。下面的示例代码将崩溃,因为在调用set_name()
方法时已经删除了sub_message
。SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
C++
中,如果你使用Swap()
方法交换两条使用了oneof
结构的消息,每条消息都将以另外一条消息的oneof case
结束:在下面的示例中,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
字段时要小心。如果检查oneof
字段的值返回None/NOT_SET
,则可能意味着此oneof
尚未设置或已将其设置为oneof
的其他版本中的字段。因为无法知道wire
的未知字段是否是其中一个字段的成员,所以无法区分两者之间的区别。
oneof
:消息序列化和解析后,可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将single
字段移动到新的oneof
字段中,并且如果已知只设置了一个字段,则可以移动多个字段。oneof
字段并将其加回:这可能会在序列化和解析消息后清除当前设置的oneof
字段。oneof
字段:这与移动常规字段有类似的问题如果要创建关联映射作为数据定义的一部分,协议缓冲区提供了一种方便的快捷语法:
map map_field = N;
…其中key_type
可以是任何整型或字符串类型(因此,除了浮点类型和字节之外的任何标量类型)。注意enum
不是一个有效的key_type
。value_type
可以是除其他map
以外的任何类型。
因此,例如,如果您想创建一个项目映射,其中每个Project
消息都与一个字符串键相关联,您可以这样定义它:
map projects = 3;
repeated
关键字。C++
、Java
和Python
中,类型的默认值被序列化,而在其他语言中没有任何序列化。map API
目前适用于所有proto3
支持的语言。你可以在相关的API参考中找到关于你所选择语言的map API
的更多信息。map语法相当于wire
上的以下语法,因此不支持map的协议缓冲区实现仍然可以处理您的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的协议缓冲区实现都必须生成并接受上述定义可以接受的数据。
可以向.proto
文件中添加可选的package
明符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后你可以在定义你的消息类型的字段时使用包说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
包说明符对生成代码的影响取决于您选择的语言:
Open
将位于名称空间foo::bar
中。.proto
文件中显式提供option java_package
。package
指令被忽略,因为Python模块是根据它们在文件系统中的位置来组织的。.proto
文件中显式提供option go_package
。PB_
)。例如,Open
将位于名称空间Foo::Bar
中。.proto
文件中显式提供选项option csharp_namespace
。例如,Open
将位于Foo.Bar
命名空间中。协议缓冲区语言中的类型名称解析的工作方式与C++类似:首先搜索最内层的作用域,然后搜索下一个最内层的作用域,以此类推,每个包都被认为是其父包的“内层”。以“.”开头(例如.foo.bar. baz)意味着从最外层的作用域开始。
协议缓冲区编译器通过解析导入的.proto文件来解析所有类型名。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它有不同的作用域规则。
如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto
文件中定义RPC服务接口,协议缓冲区编译器将根据所选语言生成服务接口代码和存根。因此,例如,如果您想用一个方法定义一个RPC服务,该方法接受您的SearchRequest并返回一个SearchResponse,您可以在.proto文件中这样定义它:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
与协议缓冲区一起使用的最直接的RPC系统是gRPC:Google开发的一个与语言和平台无关的开源RPC系统。gRPC与协议缓冲区配合得特别好,允许您使用特殊的协议缓冲区编译器插件直接从.proto文件生成相关的RPC代码。
如果您不想使用gRPC,也可以在自己的RPC实现中使用协议缓冲区。你可以在Proto2语言指南中找到更多相关信息。
还有一些正在进行的第三方项目正在为协议缓冲区开发RPC实现。有关我们知道的项目的链接列表,请参阅第三方加载项wiki页面。
Proto3支持JSON中的规范编码,使得在系统之间共享数据更加容易。下表按类型对编码进行了描述。
如果JSON编码的数据中缺少一个值或者它的值为null,那么在解析到协议缓冲区时,它将被解释为适当的默认值。如果某个字段在协议缓冲区中有默认值,则在JSON编码的数据中默认会省略该字段以节省空间。一个实现可以提供在JSON编码的输出中使用默认值发出字段的选项。
proto3 | JSON | JSON example | 描述【译】 |
---|---|---|---|
message | object | {"fooBar": v, "g": null, …} |
生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了json_name 选项,则指定的值将用作键。解析器接受小驼峰命秘法名称(或由json_name 选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并被视为相应字段类型的默认值。 |
enum | string | "FOO_BAR" |
使用proto中指定的枚举值的名称。解析器接受枚举名和整数值。 |
map |
object | {"k": v, …} |
所有键都转换为字符串。 |
repeated V | array | [v, …] |
null被接受为空列表[]。 |
bool | true, false | true, false |
|
string | string | "Hello World!" |
|
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON值将是使用带填充的标准base64编码的字符串编码的数据。包含或不包含填充的标准或url安全base64编码都可以接受。 |
int32, fixed32, uint32 | number | 1, -10, 0 |
JSON值将是一个十进制数。可接受数字或字符串。 |
int64, fixed64, uint64 | string | "1", "-10" |
JSON值将是十进制字符串。可接受数字或字符串。 |
float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" |
JSON值将是一个数字或特殊字符串值"NaN", "Infinity",和"-Infinity"中的一个。数字或字符串都可以接受。指数符号也被接受。-0被认为等于0。 |
Any | object |
{"@type": "url", "f": v, … } |
如果`Any`类型包含一个具有特殊JSON映射的值,它将按如下方式转换:{"@type":xxx,"value":yyy} 。否则,该值将转换为JSON对象,并插入“@type”字段以指示实际的数据类型。 |
Timestamp | string | "1972-01-01T10:00:20.021Z" |
使用RFC3339,其中生成的输出将始终是**Z**规格化的,并使用0、3、6或9个小数位数。也接受“Z”以外的偏移。 |
Duration | string | "1.000340012s", "1s" |
生成的输出总是包含0、3、6或9个小数位数(取决于所需的精度),后跟“s”后缀。接受任何小数位数(没有小数也可以),只要它们符合纳秒精度,并且需要“s”后缀。 |
Struct | object |
{ … } |
任何JSON对象。见struct.proto 。 |
Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, … |
包装器在JSON中使用与包装原语类型相同的表示形式,只是在数据转换和传输期间允许并保留null。 |
FieldMask | string | "f.fooBar,h" |
见field_mask.proto |
ListValue | array | [foo, bar, …] |
|
Value | value | 任何JSON值。详见google.protobuf.Value | |
NullValue | null | JSON null | |
Empty | object | {} |
空的JSON对象 |
proto3
的JSON实现可以提供以下选项:
.proto
文件中的单个声明可以使用许多 选项 进行注释。选项不会更改声明的总体含义,但可能会影响在特定上下文中处理声明的方式。可用选项的完整列表在google/protobuf/descriptor.proto
中定义。
有些选项是文件级选项,这意味着它们应该写在顶级作用域中,而不是写在任何消息、枚举或服务定义中。有些选项是消息级别的选项,这意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。也可以在枚举类型、枚举值、字段之一、服务类型和服务方法上编写选项;但是,目前尚无任何有用的选项可供选择。
以下是一些最常用的选项:
java_package
(文件选项):您要用于生成的Java类的包。如果在.proto
文件中没有给出显式的java_package
选项,那么默认情况下将使用proto包(使用.proto
文件中的“package”关键字指定)。然而,proto包通常不是好的Java包,因为proto包不应该以反向域名开始。如果不生成Java代码,则此选项无效。option java_package = "com.example.foo";
java_multiple_files
(文件选项):如果为false,则只为该.proto
文件生成一个.java
文件,并且为顶级消息、服务和枚举生成的所有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
:协议缓冲区编译器将生成最小的类,并依赖共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用SPEED
时小得多,但操作将更慢。类仍将实现与在SPEED
模式下完全相同的公共API。这种模式在包含大量.proto
文件的应用程序中非常有用,而且不需要所有文件都非常快。LITE_RUNTIME
:协议缓冲区编译器将生成仅依赖于lite运行时库(libprotobuf-lite
而不是libprotobuf
)的类。lite运行时比完整库要小得多(大约小一个数量级),但省略了某些特性,如描述符和反射。这对于运行在手机等受限平台上的应用程序特别有用。编译器仍然会像在速度模式下一样生成所有方法的快速实现。生成的类将只在每种语言中实现MessageLite
接口,它只提供完整message
接口的方法子集。option optimize_for = CODE_SIZE;
cc_enable_arenas
(文件选项):为C++生成的代码启用arena allocation。objc_class_prefix
(文件选项):设置Objective-C类前缀,该前缀位于此.proto中所有Objective-C生成的类和枚举之前。没有默认值。你应该使用Apple推荐的3-5个大写字符的前缀。请注意,所有2个字母前缀都由Apple保留。true
,则表示该字段已弃用,不应由新代码使用。在大多数语言中,这没有实际效果。在Java中,这变成了@Deprecated
注释。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译试图使用该字段的代码时发出警告。如果该字段没有被任何人使用,并且您想阻止新用户使用它,请考虑用reserved
语句替换字段声明。int32 old_field = 6 [deprecated = true];
协议缓冲区还允许您定义和使用自己的选项。这是大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2语言指南了解详细信息。请注意,创建自定义选项会使用扩展,这些扩展只允许用于proto3
中的自定义选项。
要生成Java、Python、C++、Go、Ruby、ObjuleC或C代码,需要使用.proto
文件中定义的消息类型,还需要在.proto
上运行协议缓冲区编译器protoc
。如果尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。对于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:指定解析import
指令时要在其中查找.proto
文件的目录。如果省略,则使用当前目录。通过多次传递-proto_path
选项可以指定多个导入目录;它们将按顺序进行搜索。-I=_IMPORT_PATH_
可以用作--proto_PATH
的缩写形式。
可以提供一个或多个输出指令:
——cpp_out
在DST_DIR
中生成C++代码。更多信息请参见C++生成代码参考。——java_out
在DST_DIR
中生成Java代码。有关更多信息,请参阅Java生成代码参考。——python_out
在DST_DIR
中生成Python代码。更多信息请参见Python生成代码参考。——go_out
在DST_DIR
中生成Go代码。更多信息请参见Go生成代码参考。——ruby_out
在DST_DIR
中生成Ruby代码。更多信息请参见Ruby生成代码参考。——objc_out
在DST_DIR
中生成Objective-C代码。更多信息请参见Objective-C生成代码参考。——csharp_out
在DST_DIR
中生成c#代码。更多信息请参见C#生成代码参考。——php_out
在DST_DIR
中生成PHP代码。有关更多信息,请参阅PHP生成代码参考。为了方便起见,如果DST_DIR
以.zip
或.jar
结尾,编译器会将输出写入一个给定名称的zip格式存档文件。注意,如果输出存档已经存在,它将被覆盖;编译器不够智能,无法向现有存档添加文件。必须提供一个或多个.proto
文件作为输入。可以一次指定多个.proto
文件。尽管这些文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT_PATH
导入的其中一个路径中,以便编译器可以确定其规范名称。
结语:至此,【Language Guide (proto3) | proto3 语言指南】系列随笔全部完成。如果您对系列文章有任何疑问或者意见,欢迎留言讨论!非常希望本系列随笔能为您提供一些帮助。
原文:https://www.cnblogs.com/itheo/p/14272421.html