proto3 比 proto2 新,所以一般选用proto3进行构建项目。
下面英文版可以在Language Guide (proto3)上看到。
假设您要定义搜索请求消息格式,其中每个搜索请求都有一个查询字符串。
syntax = "proto3";
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.
}
该文件的第一行指定您使用的是proto3语法:如果不这样做,协议缓冲区编译器将假定您正在使用proto2。 这必须是文件的第一个非空,非注释行。
在上述示例中,所有字段都是标量类型:两个整数(page_number和result_per_page)和一个字符串(查询)。
但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。
消息定义中的每个字段都有唯一的编号标签。这些标签用于以消息二进制格式标识字段,并且在使用消息类型后不应更改它们。
请注意,值范围为1到15的标签需要一个字节进行编码,包括标识号和字段的类型。16到2047范围内的标签需要两个字节。
为非常频繁出现的消息元素预留标签1到15。记住要为将来可能添加的频繁出现的元素留下一些空间。
您可以指定的最小标签号码是1,最大的标签号码是2的29次方减1(536,870,911)。您也不能使用号码19000到19999(FieldDescriptor :: kFirstReservedNumber到FieldDescriptor :: kLastReservedNumber),因为它们被保留用于协议缓冲区实现。
在proto3中,repeated 数字类型的重复字段默认使用压缩编码。
使用**"//"** 双斜杠进行注释
如果通过删除某个字段或者将这个字段注释掉,将来又使用了这个数字编号,并且使用的又是老版本.proto文件,会导致严重的问题(数据损坏,私有属性bugs等)。
解决方法:在"reserved"后指定已删除字段, 当以后使用这些字段的时候protobuffer解释器会自动报错。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意:您不能在相同的保留语句中混合添加字段名称和标签号。,也就是要不用编号,要不用字段名称。
当.proto运行协议编译器时,会根据选择的语言将消息序列化成输出流,并从输入流解析消息。
详细信息可以查看API Reference:
.proto | Notes | C++ | Java | Python | Go | Ruby | C# | PHP |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] |
uint32 | Uses variable-length encoding. | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer |
uint64 | Uses variable-length encoding. | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int[1] | int | uint32 | Fixnum or Bignum (as required) | uint | integer |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] |
sfixed32 | Always four bytes. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
sfixed64 | Always eight bytes. | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode[4] | string | String (UTF-8) | string | string |
bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
当消息被解析时,如果编码的消息不包含特定的单个元素,则解析对象中的相应字段将被设置为该字段的默认值。:
重复字段的默认值为空(通常为适当语言的空列表)。
注意:当message 属性被解析,就没办法明确设置值为默认值(例如布尔值是否设置为false,或者根本不设置,例如,如果您不希望默认情况下也会发生这种行为,那么在设置为false时,不要使用布尔值来切换某些行为。
另外如果当message属性被解析设置为默认值,那么这个值就不会被序列化。
在下面的示例中,我们添加了一个名为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;
}
枚举初始化常量是0, 每个枚举都必须包含一个常量。定义为0为第一个元素。
必须有一个零值,所以我们可以使用0作为数字默认值。
可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将allow_aliasoptions设置为true,否则协议编译器将在找到别名时生成错误消息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
枚举器常量必须在32位整数的范围内。由于枚举值在线上使用varint编码,所以负值无效,因此不推荐使用。
还可以使用语法MessageType.EnumType在一个消息中声明的枚举类型作为不同消息中的字段的类型。
在反序列化期间,消息中将保留无法识别的枚举值,尽管消息反序列化的方式与语言有关。在任一情况下,如果消息被序列化,则无法识别的值仍将被序列化为消息。
可以在一个消息中包含其他消息
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
在上述示例中,Result消息类型与SearchResponse相同的文件中定义,如果要用作字段类型的消息类型已经在另一个.proto文件中定义了
您可以通过导入来自其他.proto文件的定义。
import "myproject/other_protos.proto";
默认情况下,您只能使用直接导入的.proto文件中的定义。 但是,有时您可能需要将.proto文件移动到新的位置。 而不是直接移动.proto文件,并在一次更改中更新所有调用站点,现在可以在旧位置放置一个虚拟.proto文件,以使用导入公开概念将所有导入转发到新位置。 任何导入包含导入公开声明的proto的任何进口公共依赖都可以被过渡性地依赖。 例如:
新proto:
// new.proto
// All definitions are moved here
旧proto:
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
使用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消息类型并在proto3消息中使用它们,反之亦然。 然而,proto2枚举不能直接在proto3语法中使用(如果导入的proto2消息使用它们就可以了)。
可以任意嵌套层次(不过使用中还是少嵌套)
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;
}
}
}
比如需要添加额外字段,但仍然想使用与哪有创建的代码,这个时候就可以使用更新。
表示解析器无法识别的字段。
Proto3实现可以成功地解析具有未知字段的消息。实现支持或不支持未知字段。 未知字段在proto3中运行时间不可访问,并在反序列化时间被遗忘和遗忘。 这对于proto2是不同的行为,其中未知的字段总是与消息一起保留和序列化。
Any类型可以包含任意序列化的消息作为内容。充当唯一标示符并解析为该消息类型的URL.
使用Any类型需要导入google/protobuf/any.proto .
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
默认给的消息类型是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>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
(不太理解这个方法,后续了解后补充)
如果您有一个包含多个字段的消息,并且最多可以同时设置一个字段,则可以通过使用该功能强制执行此行为并节省内存。
Oneof字段就像常规字段,除了一个共享内存中的所有字段,最多可以同时设置一个字段。
设置任何成员自动清除所有其他成员。 您可以根据您选择的语言检查使用 case() or WhichOneof()方法设置一个值(如果有)。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Oneof特点
map<key_type, value_type> map_field = N;
其中key_type可以是任何整数或字符串类型(因此,除浮点类型和字节之外的任何标量类型)。 value_type可以是任何类型。
map<string, Project> projects = 3;
map等同于下面的内容,不支持map的任然可以解析
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
可以添加包,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
用的时候
message Foo {
...
foo.bar.Open open = 1;
...
}
使用取决于选择的语言
解析从内向外进行解析,也就是内部包到其父包。
如果要使用RPC系统的消息类型,可以定义一个RPC服务接口。
如果要使用一个使用SearchRequest并返回SearchResponse的方法定义RPC服务,可以在.proto文件中定义它,如下所示:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
可以使用grpc或自定义rpc进行使用
Proto3支持JSON中的规范编码,使得更容易在系统之间共享数据。
如果JSON编码数据中缺少值,或者如果其值为空,则在解析为协议缓冲区时将被解释为适当的默认值。 如果某个字段在协议缓冲区中具有默认值,则默认情况下将在JSON编码数据中省略该节点以节省空间。
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fBar": v, "g": null, …} | Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. null is accepted and treated as the default value of the corresponding field type. |
enum | string | "FOO_BAR" | The name of the enum value as specified in proto is used. |
map<K,V> | object | {"k": v, …} | All keys are converted to strings. |
repeated V | array | [v, …] | null is accepted as the empty list []. |
bool | true, false | true, false | |
string | string | "Hello World!" | |
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" | |
int32, fixed32, uint32 | number | 1, -10, 0 | JSON value will be a decimal number. Either numbers or strings are accepted. |
int64, fixed64, uint64 | string | "1", "-10" | JSON value will be a decimal string. Either numbers or strings are accepted. |
float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" | JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. |
Any | object | {"@type": "url", "f": v, … } | If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy}. Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type. |
Timestamp | string | "1972-01-01T10:00:20.021Z" | Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. |
Duration | string | "1.000340012s", "1s" | Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision. Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision. |
Struct | object | { … } | Any JSON object. See struct.proto. |
Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, … | Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer. |
FieldMask | string | "f.fooBar,h" | See fieldmask.proto. |
ListValue | array | [foo, bar, …] | |
Value | value | Any JSON value | |
NullValue | null | JSON null |
.proto文件中的单独声明可以使用多个options进行注释。 options不会更改声明的整体含义,但可能会影响其在特定上下文中的处理方式。 可用options的完整列表在google/protobuf/descriptor.proto中定义。
1.java_package (file option):要用于生成的Java类的包。 如果在.proto文件中没有提供明确的java_package选项,则默认情况下将使用proto包(使用.proto文件中的“package”关键字指定)。 然而,由于原始软件包不期望以反向域名开始,因此原始软件包通常不会生成好的Java软件包。 如果不生成Java代码,则此选项不起作用。
option java_package = "com.example.foo";
2.java_multiple_files (file option):导致在包级别定义顶级消息,枚举和服务,而不是以.proto文件命名的外部类。
option java_multiple_files = true;
3.java_outer_classname (file option): 要生成的最外层Java类的类名(因此是文件名)。 如果在.proto文件中没有指定明确的java_outer_classname,则通过将.proto文件名转换为camel-case来构造类名称(因此foo_bar.proto变为FooBar.java)。 如果不生成Java代码,则此选项不起作用。
option java_outer_classname = "Ponycopter";
4.optimize_for (file option):可以设置为SPEED,CODE_SIZE或LITE_RUNTIME。 这将影响C ++和Java代码生成器(以及可能的第三方生成器),方法如下:
option optimize_for = CODE_SIZE;
5.cc_enable_arenas (file option):Enables arena allocation for C++ generated code.
6.objc_class_prefix (file option):将Objective-C类前缀设置为所有Objective-C生成的类和该.proto的枚举前缀。 没有默认值。 您应该使用Apple推荐的3-5个大写字母之间的前缀。 请注意,所有2个字母的前缀都由Apple保留。
7.deprecated (field option):如果设置为true,则表示该字段已被弃用,不应由新代码使用。 在大多数语言中,这没有实际的效果。 在Java中,这将成为@Deprecated注释。 将来,其他特定于语言的代码生成器可能会在该字段的访问器上生成废弃注释,这将在编译尝试使用该字段的代码时产生警告。 如果任何人不使用该字段,并且您想要阻止新用户使用该字段,请考虑将字段声明替换为保留语句。
int32 old_field = 6 [deprecated=true];
允许您定义和使用您自己的options。 这是大多数人不需要的高级功能。 如果您认为您需要创建自己的options,请参阅“Proto2语言指南”了解详细信息。 请注意,创建自定义options使用的扩展名只能在proto3中的自定义options中使用。
要生成Java,Python,C ++,Go,Ruby,JavaNano,Objective-C或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 --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
作为一个额外的便利,如果DST_DIR以.zip或.jar结尾,编译器将把输出写入具有给定名称的单个ZIP格式归档文件。 .jar输出也将被给予Java JAR规范要求的清单文件。
注意,如果输出存档已经存在,它将被覆盖; 编译器不够聪明,无法将文件添加到现有存档。
proto2 定义消息体有点不一样,接下来只会将不一样的写出来一样的请看上面。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
方法定义规则
由于历史原因,标量数字类型的重复字段没有尽可能高效地编码。 新代码应该使用特殊选项[packed = true]来获得更有效的编码。 例如:
repeated int32 samples = 4 [packed=true];
required是永远您应该非常小心根据需要标记字段。 如果您希望停止写入或发送required字段,则将该字段更改为可选字段将是有问题的.
已经被废弃不要再使用
扩展名是一个字段的占位符,其类型未由原始.proto文件定义。 这允许其他.proto文件通过使用这些数字标签定义一些或所有字段的类型来添加到您的消息定义。
message Foo {
// ...
extensions 100 to 199;
}
这表示Foo中的字段数字[100,199]的范围被保留用于扩展。 其他用户现在可以在自己的.proto文件中向Foo添加新的字段,该文件导入.proto,使用指定范围内的标签,例如:
extend Foo {
optional int32 bar = 126;
}
这将将一个名为bar的字段添加到Foo的原始定义中。
编译后格式和原油格式相同,但是在访问扩展字段和常规字段有所不同,需要使用特殊访问器。如c++中设置bar值
Foo foo;
foo.SetExtension(bar, 15);
类似地,Foo类定义了模板访问器HasExtension(), ClearExtension(), GetExtension(), MutableExtension(), and AddExtension()。
注意,扩展可以是任何字段类型,包括消息类型,但不能是oneofs或map。
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
c++使用
Foo foo;
foo.SetExtension(Baz::bar, 15);
换句话说,唯一的效果是在Baz范围内定义了该条
声明嵌套在消息类型中的扩展块并不意味着外部类型和扩展类型之间的任何关系
上述示例并不意味着Baz是Foo的任何类别的子类。 这意味着符号栏被声明在Baz的范围内; 它只是一个静态成员。(意味着这样用不好呗,那么不要嵌套使用咯)
一个常见的模式是在扩展字段类型的范围内定义扩展名,例如,这是Baz类型的Foo扩展名,扩展名定义为Baz的一部分和下面这种其实是一样的。
message Baz {
...
}
// This can even be in a different file.
extend Foo {
optional Baz foo_baz_ext = 127;
}
实际上,这种语法可能是首选,以避免混淆。 如上所述,嵌套语法经常被误认为尚不熟悉扩展名的用户进行子类化。
确保两个用户不使用相同的数字标签添加相同消息类型的扩展非常重要
如果您的编号约定可能涉及具有非常大数字的扩展名作为标签,则可以使用max关键字指定扩展范围达到最大可能的字段数:
message Foo {
extensions 1000 to max;
}
max是2的29次方减1, or 536,870,911.(同时要避免内部使用的号码 19000 though 19999)。
这是大多数人不需要的高级功能。
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
这里我们通过扩展MessageOptions定义了一个新的消息级选项。 当我们使用该选项时,选项名称必须用括号括起来表示它是一个扩展名。 我们现在可以用C ++读取my_option的值:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
这里,My Message::descriptor()->options()返回MyMessage的Message Options协议消息。 从它读取自定义选项就像阅读任何其他扩展名。
Java 读取
String value = MyProtoFile.MyMessage.getDescriptor().getOptions().getExtension(MyProtoFile.myOption);
Python:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions().Extensions[my_proto_file_pb2.my_option]
可以为Protocol Buffers语言中的每种构造定义自定义选项。 这是一个使用各种选项的例子:
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50006;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
请注意,如果要在除了定义的包之外的包中使用自定义选项,则必须在选项名称前加上包名称,就像对类型名称一样。 例如:
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
最后一件事:由于自定义选项是扩展名,因此必须为任何其他字段或扩展名分配字段编号。 在上面的例子中,我们使用的范围是50000-99999。 此范围仅供个别组织内部使用,因此您可以自由使用本范围内的数字进行内部应用。 但是,如果您打算在公共应用程序中使用自定义选项,那么重要的是确保您的字段数字是全球唯一的。 要获取全球唯一的字段编号,请发送一个请求到protobuf-global-extension-registry@google.com。 只需提供您的项目名称(例如Object-C插件)和您的项目网站(如果有的话)。 通常你只需要一个分机号码。 您可以通过将它们放入子消息来声明只有一个分机号码的多个选项:
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// usage:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// alternative aggregate syntax (uses TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
PS: 觉得不错的请点个赞吧!! (ง •̀_•́)ง