定义消息类型
首先让我们看一个非常简单的例子。假设您想要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的特定结果页面以及每个页面的多个结果。下面是用于定义消息类型的.proto文件。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
文件的第一行指定您正在使用proto3语法,否则协议缓冲区编译器将假定您正在使用proto2。这必须是文件的第一个非空、非注释行。
SearchRequest消息定义指定了三个字段(名称/值对),每个字段对应您希望包含在此类型消息中的数据块。每个字段都有一个名称和类型。
指定字段类型
在前面的示例中,所有字段都是标量类型:两个整数(page_number和results_per_page)和一个字符串(query),可以像为字段指定其他消息类型(枚举和复合类型)。
分配字段编号
必须为消息定义中的每个字段提供1到536,870,911之间的数字,并具有以下限制:
1. 给定的数字在该消息的所有字段中必须是唯一的。
2.字段号19,000到19999是为协议缓冲区实现保留的。如果您在消息中使用这些保留字段号之一,协议缓冲区编译器将报错。
3.不能使用任何先前保留的字段号或任何已分配给扩展的字段号。
一旦使用了消息类型,就不能更改此号码,因为它标识了消息连接格式中的字段。“更改”字段编号相当于删除该字段并创建具有相同类型但具有新编号的新字段。
字段号永远不应该被重用。永远不要从保留列表中取出字段号以便与新字段定义一起重用。
对于最常设置的字段,应该使用字段号1到15。较低的字段数值在连线格式中占用的空间较少。例如,1到15范围内的字段号需要一个字节来编码。16到2047范围内的字段号占用两个字节。
指定字段标签
消息字段可以是以下字段之一:
optional:可选字段有两种可能的状态:
该字段已设置,并包含从连接显式设置或解析的值。它将被序列化到线路上。
该字段未设置,将返回默认值。它不会被序列化到线路上。
repeated:此字段类型可以在格式良好的消息中重复0次或多次。重复值的顺序将被保留。
Map:这是一个配对的键/值字段类型。有关此字段类型的更多信息,
添加更多消息类型
可以在单个.proto文件中定义多个消息类型。如果你要定义多个相关的消息,这是很有用的——例如,如果你想定义与你的SearchResponse消息类型对应的回复消息格式,你可以将它添加到相同的.proto:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
虽然可以在单个.proto文件中定义多个消息类型(例如message、enum和service),但是当在单个文件中定义具有不同依赖关系的大量消息时,也会导致依赖关系膨胀。建议每个.proto文件包含尽可能少的消息类型。
添加注释
要在.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 results_per_page = 3; // Number of results to return per page.
}
删除字段
如果操作不当,删除字段可能会导致严重的问题。
当您不再需要字段,并且所有引用都已从客户端代码中删除时,您可以从消息中删除字段定义。但是,您必须保留已删除的字段号。如果您不保留字段编号,开发人员可能会在将来重用该编号。
您还应该保留字段名,以允许消息的JSON和TextFormat编码继续解析。
保留字段号
如果您通过完全删除一个字段或将其注释掉来更新消息类型,那么将来的开发人员可以在对该类型进行更新时重用该字段号。这可能导致严重的问题,如重用字段号的后果中所述。为了确保不会发生这种情况,请将已删除的字段号添加到保留列表中。
如果将来的开发人员试图使用这些保留的字段号,协议编译器将生成错误消息。
message Foo {
reserved 2, 15, 9 to 11;
}
保留字段的取值范围是包含的(9 ~ 11与9、10、11相同)。
保留字段名称
以后重用旧的字段名通常是安全的,除非使用序列化字段名的TextProto或JSON编码。为避免此风险,可将已删除的字段名添加到保留列表中。
保留名称仅影响协议编译器行为,而不影响运行时行为,但有一个例外:TextProto实现可能在解析时使用保留名称丢弃未知字段(不会像其他未知字段一样引发错误)(今天只有c++和Go实现这样做)。运行时JSON解析不受保留名称的影响。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意,不能在同一reserved语句中混合字段名和字段号。
在.proto上运行 protocol buffer编译器时,编译器会用您所选择的语言生成代码,这些代码将用于处理文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息。
对于c++,编译器从每个.proto生成.h和.cc文件,并为文件中描述的每种消息类型提供一个类。
标量值类型
标量消息字段可以具有以下类型之一-该表显示了.proto文件中指定的类型,以及自动生成类中的相应类型:
默认值
在解析消息时,如果编码的消息不包含特定的隐式呈现元素,则访问已解析对象中的相应字段将返回该字段的默认值。这些默认值是特定于类型的:
对于字符串,默认值是空字符串。
对于字节,默认值为空字节。
对于bool,默认值为false。
对于数字类型,默认值为零。
对于枚举,默认值是第一个定义的枚举值,该值必须为0。
对于消息字段,没有设置该字段。它的确切值与语言有关。
请注意,对于标量消息字段,一旦解析了消息,就无法判断字段是显式设置为默认值(例如布尔值是否设置为false)还是根本没有设置:在定义消息类型时应该记住这一点。例如,如果你不希望某些行为在默认情况下也发生,不要使用布尔值在设置为false时打开某些行为。还要注意,如果将标量消息字段设置为其默认值,则该值将不会在网络上序列化。如果浮点数或双精度值被设置为+0,它将不被序列化,但-0被认为是不同的,将被序列化。
枚举
在定义消息类型时,您可能希望其中一个字段只具有预定义值列表中的一个。例如,假设您想为每个SearchRequest添加一个语料库字段,其中语料库可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。只需在消息定义中添加一个枚举,并为每个可能的值添加一个常量,就可以非常简单地做到这一点。
在下面的示例中,我们添加了一个名为Corpus的枚举,其中包含所有可能的值,以及一个类型为Corpus的字段:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
如上所示,Corpus枚举的第一个常量映射为0:每个枚举定义必须包含一个映射为0的常量作为其第一个元素。这是因为:
必须有一个零值,以便我们可以使用0作为数字默认值。
为了与proto2语义兼容,零值需要是第一个元素,在proto2语义中,除非显式指定不同的值,否则第一个枚举值是默认值。
可以通过将相同的值赋给不同的枚举常量来定义别名。为此,您需要将allow_alias选项设置为true。否则,当找到别名时, protocol buffer编译器将生成一条警告消息。虽然所有别名值在反序列化期间都是有效的,但序列化时总是使用第一个值。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
枚举数常量必须在32位整数的范围内。由于枚举值在网络上使用可变编码,因此负值是低效的,因此不建议使用。可以在消息定义内定义枚举,如前面的示例所示,也可以在消息定义外定义枚举——这些枚举可以在.proto文件中的任何消息定义中重用。您还可以使用在一个消息中声明的枚举类型作为另一个消息中字段的类型,使用语法_MessageType_._EnumType_。
当您在使用枚举的.proto上运行protocol buffer编译器时,生成的代码将具有对应的用于Java、Kotlin或c++的枚举,或者用于Python的特殊EnumDescriptor类,该类用于在运行时生成的类中创建一组具有整数值的符号常量。
使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设你想在每个SearchResponse消息中包含Result消息——要做到这一点,你可以在同一个.proto中定义一个Result消息类型,然后在SearchResponse中指定一个Result类型的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
导入定义
在前面的示例中,Result消息类型是在与SearchResponse相同的文件中定义的——如果您想要用作字段类型的消息类型已经在另一个.proto文件中定义了怎么办?
您可以通过导入其他.proto文件来使用它们。要导入另一个.proto的定义,你可以在文件的顶部添加一个import语句:
import "myproject/other_protos.proto";
默认情况下,只能使用直接导入的.proto文件中的定义。但是,有时可能需要将.proto文件移动到新位置。与直接移动.proto文件并在一次更改中更新所有调用位置不同,您可以在旧位置中放置一个占位符.proto文件,以便使用import public将所有导入转发到新位置。
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.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
protocol编译器使用-I
/--proto_path
标志在协议编译器命令行中指定的一组目录中搜索导入的文件。如果没有给出标志,则在调用编译器的目录中查找。一般来说,应该将--proto_path标志设置为项目的根目录。
嵌套类型
你可以在其他消息类型中定义和使用消息类型,如下面的例子所示——这里的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;
}
你可以多层嵌套信息。在下面的例子中,请注意两个名为Inner的嵌套类型是完全独立的,因为它们是在不同的消息中定义的:
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;
}
}
}
更新消息类型
如果现有的消息类型不再满足您的所有需求—例如,您希望消息格式有一个额外的字段—但您仍然希望使用用旧格式创建的代码,请不要担心!当您使用二进制连线格式时,更新消息类型非常简单,而不会破坏任何现有代码。
字段修改最佳实践和遵守规则
1.不要更改任何现有字段的字段号。“更改”字段号相当于删除该字段并添加具有相同类型的新字段。如果需要对字段重新编号,请参见删除字段的说明。
2.如果添加新字段,使用“旧”消息格式的代码序列化的任何消息仍然可以被新生成的代码解析。您应该保留这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。
3.只要在更新后的消息类型中不再使用字段号,即可删除字段。您可能想要重命名字段,可以添加前缀“OBSOLETE_”,或者保留字段编号,以便将来使用.proto的用户不会意外地重用该编号导致错误。
4. Int32、uint32、int64、uint64和bool都是兼容的,这意味着您可以将一个字段从这些类型中的一种更改为另一种,而不会破坏向前或向后兼容性。如果从字节流中解析的数字不适合相应的类型,那么您将获得与在c++中将该数字强制转换为该类型相同的效果(例如,如果将64位数字读取为int32,则将其截断为32位)。
5. sint32和sint64相互兼容,但与其他整数类型不兼容。
6. 只要字节是有效的UTF-8, string和bytes是兼容的。
7.如果字节包含消息的编码版本,则嵌入式消息与字节兼容。
8.fixed32与sfixed32兼容,fixed64与sfixed64兼容。
9. 对于字符串、字节和消息字段,可选与repeat兼容。给定一个repeat字段的序列化数据作为输入,如果该字段是基本类型字段,希望该字段是optional的客户端将接受最后一个输入值,如果是消息类型字段,则合并所有输入元素。请注意,这对于数值类型(包括bool和enum)通常不安全。数字类型的repeat字段可以以打包格式序列化,当期望使用optional字段时,将无法正确解析该格式。
10. enum兼容int32、uint32、int64和uint64(注意,不匹配的值会被截断)。但是,请注意,当消息被反序列化时,客户端代码可能会以不同的方式对待它们:例如,消息中将保留无法识别的proto3枚举类型,但是当消息被反序列化时,如何表示它取决于语言。
Any
Any消息类型允许您使用消息作为嵌入类型,而不需要它们的.proto定义。Any包含任意序列化的字节消息,以及作为该消息类型的全局唯一标识符并解析为该消息类型的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);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
Oneof
如果您的消息有许多字段,并且最多同时设置一个字段,则可以通过使用oneof特性强制执行此行为并节省内存。
除了Oneof共享内存中的所有字段之外,Oneof字段与常规字段类似,并且最多可以同时设置一个字段。设置其中的任何成员都会自动清除所有其他成员。您可以根据所选择的语言,使用special case()或whereoneof()方法检查其中的哪个值被设置(如果有的话)。
注意,如果设置了多个值,则由原型中的顺序决定的最后一个设定值将覆盖之前的所有设定值。
其中一个字段的字段号在封闭消息中必须是唯一的。
使用Oneof
要在.proto中定义oneof,你可以使用oneof关键字后跟oneof名称,在本例中为test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
将oneof字段添加到oneof定义中。您可以添加任何类型的字段,除了map字段和repeat字段。如果需要向其中一个添加重复字段,可以使用包含重复字段的消息。
Oneof特性
设置oneof字段将自动清除oneof字段的所有其他成员。因此,如果您设置了几个oneof字段,那么只有您设置的最后一个字段仍然具有值。
SampleMessage message;
message.set_name("name");
CHECK_EQ(message.name(), "name");
// Calling mutable_sub_message() will clear the name field and will set
// sub_message to a new instance of SubMessage with none of its fields set.
message.mutable_sub_message();
CHECK(message.name().empty());
1.如果解析器在网络上遇到相同的多个成员,则只在解析的消息中使用看到的最后一个成员。
2. oneof不能是repeated。
如果您正在使用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
向后兼容性问题
在添加或删除其中一个字段时要小心。如果检查oneof的值返回None/NOT_SET,这可能意味着oneof没有被设置,或者它已被设置为oneof的不同版本中的字段。
Maps
如果你想创建一个关联映射作为你的数据定义的一部分,protocol buffers提供了一个方便的快捷语法:
map<key_type, value_type> map_field = N;
其中key_type可以是任何整数或字符串类型(因此,任何标量类型,除了浮点类型和字节)。注意,enum和proto消息对key_type都无效。value_type可以是任何类型,除了另一个map。
因此,例如,如果你想创建一个项目映射,其中每个项目消息与一个字符串键相关联,你可以这样定义它:
map<string, Project> projects = 3;
Maps的特性
1.map字段不能是repeated。
2.在为.proto生成文本格式时,映射按key排序,数字key按数字排序。
3.在从连接进行解析或合并时,如果存在重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复键,解析可能会失败。
4.如果为映射字段提供键但没有值,则序列化字段时的行为依赖于语言。在c++、Java、Kotlin和Python中,该类型的默认值是序列化的,而在其他语言中则没有序列化。
5.符号FooEntry不能存在于map foo的同一作用域中,因为FooEntry已经被map的实现使用了。
向后兼容性
map语法在网络上相当于下面的语法,所以不支持map的protocol buffers实现仍然可以处理你的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Packages
您可以向.proto文件添加一个可选的包说明符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后,您可以在定义消息类型的字段时使用包说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
包说明符影响生成代码的方式取决于您选择的语言:
在c++中,生成的类被封装在一个c++命名空间中。例如,Open将位于名称空间foo::bar中。
建议为.proto文件指定包,否则它可能导致描述符中的命名冲突,并使proto无法移植到其他语言。
enum值选项
支持枚举值选项。您可以使用deprecated选项来指示不应该再使用某个值。
下面的示例显示了添加这些选项的语法:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
生成类
要生成需要处理.proto文件中定义的消息类型的Java、Kotlin、Python、c++、Go、Ruby、Objective-C或c#代码,您需要在.proto文件上运行protocol buffer编译器protoc。
编译器的调用方式如下:
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文件的目录。如果省略,则使用当前目录。可以通过多次传递——proto_path选项来指定多个导入目录;他们将被按顺序搜查。-I=_IMPORT_PATH_可以用作--proto_path的简写形式。
你可以提供一个或多个输出指令:
--cpp_out在DST_DIR中生成c++代码(其他生成对应的编程语言代码)。
如果DST_DIR以.zip或. jar结尾,编译器将把输出写到一个具有给定名称的zip格式的归档文件中。注意,如果输出存档已经存在,它将被覆盖。
您必须提供一个或多个.proto文件作为输入。可以一次指定多个.proto文件,尽管文件是相对于当前目录命名的,但每个文件必须在在import_path之中。
文件位置
最好不要将.proto文件放在与其他语言源文件相同的目录中。考虑在项目的根目录下为.proto文件创建子目录proto。