Protobuf3 基础(不是翻译)
基础语法
syntax = "proto3"; //指定protobuf版本
package foo.bar; //包声明
import "myproject/other_protos.proto"; //导入依赖
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message Open { ... }
说明:
注释同c/c++语法:单行以 // 注释;多行以:
/* 和 */
注释。每行以分号 ; 结束,类似于c/c++语法。
文件的第一个非空非注释行需要指定protobuf版本,如:
syntax = "proto3";
如果不指定,默认为protobuf2。如果使用gRPC,推荐使用protobuf3。如果需要使用的消息已经在其他proto文件中定义,那么可以使用
import
将其导入。如:import "myproject/other_protos.proto";
package包声明是可选的,可以防止不同的消息类型有命名冲突。在proto转为响应代码时,package也会做和语法相关的处理:
对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中
对于Java,包声明符会变为java的一个包,除非在 .proto 文件中明确提供选项 java_package,否则该包将用作 Java 包
对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的
对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中
对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open会在Foo::Bar名称空间中
对于javaNano包会使用Java包,除非你在你的文件中显式的提供一个option java_package
对于C#包可以转换为PascalCase后作为名称空间,除非你在文件中显式的提供一个option csharp_namespace,例如,Open会在Foo.Bar名称空间中
字段类型
message Demo {
int32 id = 1; //整形
string name = 2; //字符串
enum Gender { //此处定义enmu类型
UNKNOW = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 3; //枚举类型
map<string, Project> projects = 4; //map映射
}
说明:
- 字段类型可以时标量类型(整形,浮点型,布尔,字符串,bytes);也可以是合成类型(“结构体/对象”,枚举,map)
- 关于标量类型proto和其他主流语言的对应关系:
.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 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
uint32 | 使用变长编码 | uint32 | int | int/long | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
uint64 | 使用变长编码 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | uint32 | int | int | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 总是4个字节 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sfixed64 | 总是8个字节 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | string | String | str/unicode | string | String (UTF-8) | string | string |
bytes | 可能包含任意顺序的字节数据。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
fix和double固定长度,int,uint,sint为变长整形,当value包含负数时,使用sint编码效率比int更高。
标量默认值:
- 对于strings,默认是一个空string
- 对于bytes,默认是一个空的bytes
- 对于bools,默认是false
- 对于数值类型,默认是0
- 对于枚举,默认是第一个定义的枚举值,必须为0
- 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的
关于枚举类型 enum:
必须有有一个0值,我们可以用这个0值作为默认值。
这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。
可以通过将不同的枚举常量指定位相同的值。如果这样做你需要将allow_alias设定位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位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
枚举类型可以在 一个消息定义的内部定义,也可以在消息外部单独定义。
如果需要使用另外一个消息中定义的枚举,需要使用形如:MessageType.EnumType的语法。
字段标识号
message Demo {
int32 id = 1; //标识号为1,是Demo消息中的第一个字段
string name = 2; //标明是第二个字段
enum Gender {
UNKNOW = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 3;
map<string, Project> projects = 4;
}
说明:
- 如上面的代码所示,在消息定义中,每个字段都有唯一的一个数字标识符。
- 这些标识符是用来在消息的二进制格式中指定各个字段的位置,一旦设定就不能够再改变。
- [1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。要为将来有可能添加的、频繁出现的标识号预留一些标识号。
- 最小的标识号可以从1开始,最大到536870911(2^29 - 1),其中的[19000-19999]为预留标识号,不可以使用。
字段规则
所指定的消息字段修饰符必须是如下之一:
- singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。
- repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
- 在proto3中,repeated的标量域默认情况虾使用packed。
- 如果不指定字段规则,proto3默认字段规则为: singular。
关于保留字段:
如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.proto文件这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标识符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。
其他人是这样翻译的,我说下自己的理解吧:
如果消息中的某个字段你不想使用了,通常情况下,我们会在代码里注释或者删除掉。但是这种变更其他人可能不知道,仍然继续在使用变更前的消息定义,如此就会出现数据编解码上的问题(字段顺序/数据类型/其他)。所以建议的方法是:对这个不再需要的字段进行标注,这样从发起修改的一方看,原有字段只是不再使用,在编解码时仍然不会忽略掉,如此一来编解码的另一方就不会受到影响。
message Foo { reserved 2, 15, 9 to 11; //可以直接通过标识号指定,需要进行保留的字段 reserved "foo", "bar"; //也可以通过字段名指定 } //注:不要在同一行reserved声明中同时声明域名字和标识号
proto文件编译后的结果
- 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
- 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
- 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
- 对Go来说,编译器会位每个消息类型生成了一个.pd.go文件。
- 对于Ruby来说,编译器会为每个消息类型生成了一个.rb文件。
- 对于javaNano来说,编译器输出类似域java但是没有Builder类
- 对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
- 对于C#来说,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。
Any
Any类型消息允许你在没有指定他们的.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入import 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()
方法。目前,用于Any类型的动态库仍在开发之中,如果你已经很熟悉proto2语法,使用Any替换拓展
Oneof
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存。
Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用case()
或者WhichOneof()
方法检查哪个oneof字段被设置, 看你使用什么语言了。
- 使用Oneof
为了在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字, 比如下面例子的test_oneof:
然后你可以增加oneof字段到 oneof 定义中。你可以增加任意类型的字段, 但是 不能使用repeated 关键字。message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; } }
在产生的代码中, oneof字段拥有同样的 getters 和setters, 就像正常的可选字段一样.。也有一个特殊的方法来检查到底那个字段被设置.。你可以在相应的语言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 字段有效。
- 如果使用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()
两个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字段时一定要小心. 如果检查oneof的值返回
None/NOT_SET
, 它意味着oneof字段没有被赋值或者在一个不同的版本中赋值了。 你不会知道是哪种情况,因为没有办法判断如果未识别的字段是一个oneof字段。Tage 重用问题:
- 将字段移入或移除oneof:在消息被序列号或者解析后,你也许会失去一些信息(有些字段也许会被清除)
- 删除一个字段或者加入一个字段:在消息被序列号或者解析后,这也许会清除你现在设置的oneof字段
- 分离或者融合oneof:行为与移动常规字段相似。
Map(映射)
如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:
map<key_type, value_type> map_field = N;
其中key_type
可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type
可以是任意类型。
例如,如果你希望创建一个project的映射,每个Projecct
使用一个string作为key,你可以像下面这样定义:
map<string, Project> projects = 3;
Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。
生成map的API现在对于所有proto3支持的语言都可用了,你可以从API指南找到更多信息。
向后兼容性问题:map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的:
message MapFieldEntry { key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N;
定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最直观的使用protocol buffer的RPC系统是gRPC一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接为您从.proto文件中产生相关的RPC代码。
如果你不想使用gRPC,也可以使用protocol buffer用于自己的RPC实现,你可以从proto2语言指南中找到更多信息
还有一些第三方开发的PRC实现使用Protocol Buffer。参考第三方插件wiki查看这些实现的列表。
JSON 映射
Proto3 支持JSON的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。
如果JSON编码的数据丢失或者其本身就是``,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在protocol buffer中表示为默认值,体会在转化成JSON的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。
proto3 | JSON | JSON示例 | 注意 |
---|---|---|---|
message | object | {“fBar”: v, “g”: null, …} | 产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值 |
enum | string | “FOO_BAR” | 枚举值的名字在proto文件中被指定 |
map | object | {“k”: v, …} | 所有的键都被转换成string |
repeated V | array | [v, …] | null被视为空列表 |
bool | true, false | true, false | |
string | string | “Hello World!” | |
bytes | base64 string | “YWJjMTIzIT8kKiYoKSctPUB+” | |
int32, fixed32, uint32 | number | 1, -10, 0 | JSON值会是一个十进制数,数值型或者string类型都会接受 |
int64, fixed64, uint64 | string | “1”, “-10” | JSON值会是一个十进制数,数值型或者string类型都会接受 |
float, double | number | 1.1, -10.0, 0, “NaN”, “Infinity” | JSON值会是一个数字或者一个指定的字符串如”NaN”,”infinity”或者”-Infinity”,数值型或者字符串都是可接受的,指数符号也可以接受 |
Any | object | {“@type”: “url”, “f”: v, … } | 如果一个Any保留一个特上述的JSON映射,则它会转换成一个如下形式:{"@type": xxx, "value": yyy} 否则,该值会被转换成一个JSON对象,@type 字段会被插入所指定的确定的值 |
Timestamp | string | “1972-01-01T10:00:20.021Z” | 使用RFC 339,其中生成的输出将始终是Z-归一化啊的,并且使用0,3,6或者9位小数 |
Duration | string | “1.000340012s”, “1s” | 生成的输出总是0,3,6或者9位小数,具体依赖于所需要的精度,接受所有可以转换为纳秒级的精度 |
Struct | object | { … } | 任意的JSON对象,见struct.proto |
Wrapper types | various types | 2, “2”, “foo”, true, “true”, null, 0, … | 包装器在JSON中的表示方式类似于基本类型,但是允许nulll,并且在转换的过程中保留null |
FieldMask | string | “f.fooBar,h” | 见fieldmask.proto |
ListValue | array | [foo, bar, …] | |
Value | value | 任意JSON值 | |
NullValue | JSON null |
选项
在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。
如下就是一些常用的选择:
java_package
(文件选项) :这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名。当然了,默认方式产生的 java包名并不是最好的方式,按照应用名称倒序方式进行排序的。如果不需要产生java代码,则该选项将不起任何作用。如:option java_package = "com.example.foo";
java_outer_classname
(文件选项): 该选项表明想要生成Java类的名称。如果在.proto文件中没有明确的java_outer_classname定义,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),如果不生成java代码,则该选项不起任何作用。如:option java_outer_classname = "Ponycopter";
optimize_for (文件选项): 可以被设置为 SPEED, CODE_SIZE,或者LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
SPEED (default)
: protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。CODE_SIZE
: protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。LITE_RUNTIME
: protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。option optimize_for = CODE_SIZE;
cc_enable_arenas
(文件选项):对于C++产生的代码启用arena allocationobjc_class_prefix
(文件选项):设置Objective-C类的前缀,添加到所有Objective-C从此.proto文件产生的类和枚举类型。没有默认值,所使用的前缀应该是苹果推荐的3-5个大写字符,注意2个字节的前缀是苹果所保留的。deprecated
(字段选项):如果设置为true
则表示该字段已经被废弃,并且不应该在新的代码中使用。在大多数语言中没有实际的意义。在java中,这回变成@Deprecated
注释,在未来,其他语言的代码生成器也许会在字标识符中产生废弃注释,废弃注释会在编译器尝试使用该字段时发出警告。如果字段没有被使用你也不希望有新用户使用它,尝试使用保留语句替换字段声明。int32 old_field = 6 [deprecated=true];
自定义选项
ProtocolBuffers允许自定义并使用选项。该功能应该属于一个高级特性,对于大部分人是用不到的。如果你的确希望创建自己的选项,请参看 Proto2 Language Guide。注意创建自定义选项使用了拓展,拓展只在proto3中可用。
生成访问类
可以通过定义好的.proto文件来生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代码,需要基于.proto文件运行protocol buffer编译器protoc。如果你没有安装编译器,下载安装包并遵照README安装。对于Go,你还需要安装一个特殊的代码生成器插件。你可以通过GitHub上的protobuf库找到安装过程
通过如下方式调用protocol编译器:
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
IMPORT_PATH
声明了一个.proto文件所在的解析import具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以多次调用--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 中产生Go代码,参考正在制作中。--javanano_out
在目标目录DST_DIR中生成JavaNano,JavaNano代码生成器有一系列的选项用于定制自定义生成器的输出:你可以通过生成器的README查找更多信息,JavaNano参考正在制作中。--objc_out
在目标目录DST_DIR中产生Object代码,可以在Objective-C代码生成参考中查看更多。--csharp_out
在目标目录DST_DIR中产生Object代码,可以在C#代码生成参考中查看更多。--php_out
在目标目录DST_DIR中产生Object代码,可以在PHP代码生成参考中查看更多。
作为一个方便的拓展,如果DST_DIR以.zip或者.jar结尾,编译器会将输出写到一个ZIP格式文件或者符合JAR标准的.jar文件中。注意如果输出已经存在则会被覆盖,编译器还没有智能到可以追加文件。
- 你必须提议一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 irvin.em@live.com。
文章标题:Protobuf3 基础(不是翻译)
文章字数:6.2k
本文作者:dino
发布时间:2019-11-17, 17:02:47
最后更新:2019-11-17, 17:02:47
原始链接:https://blog.walkbc.com/2019/11/17/aboutProtobuf3/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。
QQ交流群:273078549