protocol-buffer3语言指南-02
Any
Any 消息类型可以让你使用消息作为嵌入类型而不必持有他们的.proto定义. Any把任意序列化后的消息作为bytes包含, 带有一个URL, 工作起来类似一个全局唯一的标识符. 为了使用Any类型, 需要导入google/protobuf/any.proto.
参考文档
Oneof
- 如果你有一个有很多字段的消息, 而同一时间最多只有一个字段会被设值, 你可以通过使用oneof特性来强化这个行为并节约内存.
Oneof 字段和常见字段类似, 除了所有字段共用内存, 并且同一时间最多有一个字段可以设值. 设值oneof的任何成员都将自动清除所有其他成员. 可以通过使用特殊的case()或者WhichOneof()方法来检查oneof中的哪个值被设值了(如果有), 取决于你选择的语言.
- 使用Oneof
使用oneof关键字来在.proto中定义oneof, 后面跟oneof名字, 在这个例子中是test_oneof:
message SubMessage {
int32 age = 1;
}
message SampleMessage {
oneof test_oneof {
string name = 1;
SubMessage sub_message = 2;
}
}
然后再将oneof字段添加到oneof定义. 可以添加任意类型的字段, 但是不能使用重复(repeated)字段.
在生成的代码中, oneof字段和普通字段一样有同样的getter和setter方法. 也会有一个特别的方法用来检查哪个值(如果有)被设置了. 可以在所选语言的oneof API中找到更多信息.
- Oneof特性
- 设置一个oneof字段会自动清除所有其他oneof成员. 所以如果设置多次oneof字段, 只有最后设置的字段依然有值.
案例:
func main() {
sm := proto.SampleMessage{}
sm.TestOneof = &proto.SampleMessage_Name{Name: "王五"}
sm.TestOneof = &proto.SampleMessage_SubMessage{SubMessage: &proto.SubMessage{Age: 18}}
sn, ok := sm.TestOneof.(*proto.SampleMessage_Name)
if ok {
fmt.Println(*sn)
} else {
fmt.Println("sn断言成Name失败")
}
fmt.Println(*sm.TestOneof.(*proto.SampleMessage_SubMessage))
}
输出结果:
sn断言成Name失败
{age:18}
- 如果解析器遇到同一个oneof的多个成员, 只有看到的最后一个成员在被解析的消息中被使用.
- oneof不能是重复字段
- Reflection APIs work for oneof fields. (反射api可以工作于oneof字段?)
- 当添加或者删除oneof字段时要小心. 如果检查oneof的值返回None/NOT_SET, 这意味着这个oneof没有被设置或者它被设置到一个字段, 而这个字段是在不同版本的oneof中. 没有办法区分这个差别, 因此没有办法知道一个未知字段是否是oneof的成员.
- 标签重用问题
- 移动字段进出oneof: 消息被序列化和解析后, 可能丢失部分信息(某些字段可能被清除).
- 删除oneof的一个字段加回来: 消息被序列化和解析后, 可能清除你当前设置的oneof字段
- 拆分或者合并oneof: 和移动普通字段一样有类似问题
maps
如果想常见一个关联的map作为数据定义的一部分, protocol buffers 提供方便的快捷语法:
map<key_type, value_type> map_field = N;
key_type可以是任意整型或者字符类型(因此, 除了floating point和bytes外任何简单类型). value_type可以是任意类型.
因此, 例如, 如果你想创建一个projects的map, 每个Project消息都关联到一个string key, 可以这样定义:
map<string, Project> projects = 3;
包
可以在.proto文件中增加一个可选的包标记来防止protocol消息类型之间的名字冲突.
package foo.bar;
message Open { ... }
在定义消息类型的字段时可以这样使用包标志:
message Foo {
...
foo.bar.Open open = 1;
...
}
包标志对生成代码的影响依赖所选的语言:
- 在c++中, 生成类包裹在c++ namespace中. 例如, Open将在namespace foo::bar中.
- 在java中, 包被作为java package使用, 除非你在.proto文件中显式提供java_package选项.
- 在Python中, 这个包指令将被忽略, 因为Python模块是根据他们在文件系统中的位置被组织的.
- 在Go中, 包被作为Go package使用, 除非你在.proto文件中显式提供go_package选项.
- 在Ruby中, 生成的代码被包裹在内嵌的Ruby namespaces, 转换为要求的Ruby capitalization风格(第一个字符大写;如果第一个字符不是字母则加一个PB_前缀). * 例如, Open将在namespace Foo::Bar中.
- 在JavaNano中, 包被作为java package使用, 除非你在.proto文件中显式提供java_package选项.
定义服务
如果想在RPC (Remote Procedure Call) 系统中使用消息类型, 可以在.proto文件中定义RPC服务接口, 然后protocol buffer编译器会生成所选语言的服务接口代码和桩(stubs). 因此, 例如, 如果想定义一个RPC服务,带一个方法处理SearchRequest并返回SearchResponse, 可以在.proto文件中如下定义:
syntax = "proto3";
package proto;
option go_package = "my_proto/proto";
message SearchRequest {
string name = 1;
}
message SearchResponse {
string res = 1;
}
service SearchService {
rpc search(SearchRequest) returns (SearchResponse);
}
使用protocol buffers最直接的RPC系统是gRPC: 一个Google开发的语言和平台无关的开源RPC系统. gRPC 可以非常好和protocol buffers一起工作并使用特别的protocol buffer编译器插件从.proto文件直接生成对应的RPC代码.
如果不想用gRPC, 也可以在自己的RPC实现中使用protocol buffers.
json映射
Proto3支持JSON格式的标准编码, 让在系统之间分享数据变得容易. 编码在下面的表格中以type-by-type的基本原则进行描述.
如果一个值在json编码的数据中丢失或者它的值是null, 在被解析成protocol buffer时它将设置为对应的默认值.如果一个字段的值正好是protocol buffer的默认值, 这个字段默认就不会出现在json编码的数据中以便节约空间.具体实现应该提供选项来在json编码输出中出现带有默认值的字段.
详细表格见原文
选项
.proto文件中的个别声明可以被一定数据的选项(option)注解. 选项不改变声明的整体意义, 但是在特定上下文会影响它被处理的方式. 可用选项的完整列表定义在google/protobuf/descriptor.proto.
有些选项是文件级别, 意味着他们应该写在顶级范围, 而不是在任何消息,枚举,或者服务定义之内. 有些选项时消息级别, 意味着他们应该写在消息定义内. 有些选项是字段级别, 意味着他们应该写在字段定义内. 选项也可以写在枚举类型, 枚举值, 服务定义和服务方法上. 但是, 目前没有任何有用的选项存在这些地方.
这里有一些最常用的选项:
- packed (文件选项): 如果在重复字段或者基本数字类型上设置为true, 会使用一个更加紧凑的编码. 使用这个选项没有副作用. 但是, 注意在2.3.0版本之前, 接收到意外packed数据的解析器会忽略它. 因此, 将现有字段修改为packed格式是不可能不打破兼容性的. 在2.3.0和之后的版本中, 这个修改是安全的, 因为对于可压缩的字段解析器总是可以接收两个格式(压缩和不压缩), 但是在处理使用老版本protobuf的老程序请小心.
message SearchRequest {
repeated int32 age = 1 [packed = true];
}
deprecated (字段选项): 如果设置为true, 表明这个字段被废弃, 新代码不应该再使用. 在大多数语言中这不会有实质影响. 在Java中, 这将会变成一个@Deprecated标签. 未来, 其他特定语言的代码生成器可能在字段的访问器上生成废弃标签, 在编译试图使用这个字段的代码时会生成警告. 如果这个字段不再被任何人使用而你想阻止新用户使用它, 可以考虑将字段声明替换为保留字段.
string name = 2 [deprecated = true];
生成类
为了生成Java, Python, C++, Go, Ruby, JavaNano, Objective-C, 或者 C# 代码, 需要处理定义在.proto文件中的消息类型, 需要在.proto文件上运行protocol buffer编译器protoc. 如果你没有安装这个编译器, 下载包并遵循README的指示. 对于Go, 需要为编译器安装特别的代码生成插件: 在github上的golang/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文件, 当解析导入命令时. 缺省使用当前目录. 多个导入命令可以通过传递多次—proto_path选项来指定.这些路径将按照顺序被搜索. -I=IMPORT_PATH是—proto_path的缩写形式.
- 可以提供一个或多个.proto文件作为输入. 多个.proto文件可以一次指定. 虽然文件被以当前目录的相对路径命名, 每个文件必须位于一个IMPORT_PATH路径下, 以便编译器可以检测到它的标准名字.
风格指南
这个文档为.proto文件提供风格指南. 通过遵循下列约定, 可以让protocol buffer消息定义和他们对应的类保持一致并容易阅读.
- 消息和字段名
消息名使用驼峰法 - 例如, SongServerRequest. 字段名使用下划线分隔 - 例如, song_name.
message SongServerRequest {
string song_name = 1;
}
为字段名使用这种命名约定可以得到如下的访问器:
MySchool string `protobuf:"bytes,3,opt,name=my_school,json=mySchool,proto3" json:"my_school,omitempty"`
- 枚举
枚举类型名使用驼峰法(首字母大写), 值的名字使用大写加下划线分隔:
enum Foo {
FIRST_VALUE = 0;
SECOND_VALUE = 1;
}
每个枚举值以分号(;)结束, 不要用逗号(,).
生成结果:
type Foo int32
const (
Foo_FIRST_VALUE Foo = 0
Foo_SECOND_VALUE Foo = 1
)
- 服务
如果.proto文件定义RPC服务, 服务名和任何rpc方法应该用驼峰法(首字母大写):
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
编码
这封文档描述protocol buffer消息的二进制格式. 在应用中使用protocol buffer不需要理解这些, 但是它对于了解不同的protocol buffer格式对编码消息的大小的影响非常有用.
- 简单消息
假设你有下面这个非常简单的消息定义:
message Test1 {
int32 a = 1;
}
在应用中, 创建一个Test1消息并设置a为150. 然后序列化这个消息到输出流. 如果你可以检查编码后的消息, 你会看到3个字节:
func main() {
t := myProto.Test1{A: 150}
data, err := proto.Marshal(&t)
fmt.Println(data, err)
var t2 myProto.Test1
if err = proto.Unmarshal(data, &t2); err != nil {
fmt.Println(err)
}
}
输出:
[8 150 1] <nil>
如此的小 - 我们测试protobuf比json压缩效率和速度对比:
func main() {
t1 := myProto.Test1{A: 150}
start1 := time.Now()
for i := 0; i < 1000000; i++ {
v1, _ := proto.Marshal(&t1)
if i == 0 {
fmt.Println(v1)
}
var t2 myProto.Test1
_ = proto.Unmarshal(v1, &t2)
}
d1 := time.Now().Sub(start1)
start2 := time.Now()
for i := 0; i < 1000000; i++ {
v2, _ := json.Marshal(&t1)
if i == 0 {
fmt.Println(v2)
}
var t3 myProto.Test1
_ = json.Unmarshal(v2, &t3)
}
d2 := time.Now().Sub(start2)
fmt.Println("d1 =", d1)
fmt.Println("d2 =", d2)
fmt.Println("d2 / d1 =", float64(d2)/float64(d1))
}
执行结果:
[8 150 1] // proto
[123 34 97 34 58 49 53 48 125] // json
d1 = 453.7118ms // proto
d2 = 1.4455396s // json
d2 / d1 = 3.186030427244784 // proto比json快3倍多
技巧
- 大数据集
Protocol Buffer 并非设计用来处理巨型消息. 作为一个常用规则, 如果你处理每个都大于1M的消息, 是时候考虑使用交替策略了(alternate strategy).
这里说的是, Protocol Buffer 非常善于处理在一个大数据库集下的单个消息. 通常, 大数据集仅仅是小片段的集合, 这里每个小片段是数据的一个结构化的片段. 虽然Protocol Buffer不能一次性的处理整体, 但是使用Protocol Buffer来编码每个片段可以极大的简化问题: 现在需要做的是处理字节字符串的集合而不是结构体的集合.
Protocol Buffer 没有包括任何内建的对大数据集的支持, 因为不同的情况需要不同的解决方案. 某些时候一个简单的记录列表就可以搞定, 而其他时候可能想要更多东西比如数据库. 每个解决方案应该作为独立类库来开发, 这样仅仅是那些需要使用他们的人才需要付出代价.