protocol-buffer3语言指南-02

Any

Any 消息类型可以让你使用消息作为嵌入类型而不必持有他们的.proto定义. Any把任意序列化后的消息作为bytes包含, 带有一个URL, 工作起来类似一个全局唯一的标识符. 为了使用Any类型, 需要导入google/protobuf/any.proto.
参考文档

Oneof

  1. 如果你有一个有很多字段的消息, 而同一时间最多只有一个字段会被设值, 你可以通过使用oneof特性来强化这个行为并节约内存.

Oneof 字段和常见字段类似, 除了所有字段共用内存, 并且同一时间最多有一个字段可以设值. 设值oneof的任何成员都将自动清除所有其他成员. 可以通过使用特殊的case()或者WhichOneof()方法来检查oneof中的哪个值被设值了(如果有), 取决于你选择的语言.

  1. 使用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中找到更多信息.

  1. 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的成员.
  1. 标签重用问题
  • 移动字段进出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消息定义和他们对应的类保持一致并容易阅读.

  1. 消息和字段名
    消息名使用驼峰法 - 例如, 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"`
  1. 枚举
    枚举类型名使用驼峰法(首字母大写), 值的名字使用大写加下划线分隔:
enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

每个枚举值以分号(;)结束, 不要用逗号(,).
生成结果:

type Foo int32
const (
	Foo_FIRST_VALUE  Foo = 0
	Foo_SECOND_VALUE Foo = 1
)
  1. 服务
    如果.proto文件定义RPC服务, 服务名和任何rpc方法应该用驼峰法(首字母大写):
service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

编码

这封文档描述protocol buffer消息的二进制格式. 在应用中使用protocol buffer不需要理解这些, 但是它对于了解不同的protocol buffer格式对编码消息的大小的影响非常有用.

  1. 简单消息
    假设你有下面这个非常简单的消息定义:
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倍多

技巧

  1. 大数据集
    Protocol Buffer 并非设计用来处理巨型消息. 作为一个常用规则, 如果你处理每个都大于1M的消息, 是时候考虑使用交替策略了(alternate strategy).

这里说的是, Protocol Buffer 非常善于处理在一个大数据库集下的单个消息. 通常, 大数据集仅仅是小片段的集合, 这里每个小片段是数据的一个结构化的片段. 虽然Protocol Buffer不能一次性的处理整体, 但是使用Protocol Buffer来编码每个片段可以极大的简化问题: 现在需要做的是处理字节字符串的集合而不是结构体的集合.

Protocol Buffer 没有包括任何内建的对大数据集的支持, 因为不同的情况需要不同的解决方案. 某些时候一个简单的记录列表就可以搞定, 而其他时候可能想要更多东西比如数据库. 每个解决方案应该作为独立类库来开发, 这样仅仅是那些需要使用他们的人才需要付出代价.

protobuf-go官方文档
protobuf-go API文档
proto3语言规范

posted @ 2022-09-21 16:59  专职  阅读(95)  评论(0编辑  收藏  举报