Protocol Buffers V3 版本和 V2 相比有了比较大的变化,删除了 V2 的一些特性,建议直接使用 V3 版本。 以下是 V3 版本的语法要点,源自 Language Guide (proto3) ,语法手册见 Protocol Buffers Version 3 Language Specification。
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
支持的编程语言、代码生成方法和代码映射关系参阅:Protocol Buffers API Reference。
proto3 删除了 required
,改变了原先的 optional
的语义,原因说明 why messge type remove ‘required,optional’?。
proto3 中可以直接增加新的 field,不需要在新字段前增加 optional 修饰,这种没有修饰的常规字段默认为 singular 类型。 服务端收到的消息体中,如果 singular 类型的字段为默认值,无法判断是客户端设置了默认值,还是未赋值导致的默认值。
optional
在 protov2 中语义原本为可以缺失的字段,在 proto3 中转变为可以判断出是否有赋值的字段,用于弥补 singular 的不足。
message Result {
// 如果需要增加字段,直接添加即可,不需要用 optional 修饰
string url = 1;
string name = 2;
//可以判断出是否有赋值
optional string title = 3;
//repeated 语义不变
repeated string snippets = 4;
}
proto2 因为历史原因,scalar numeric types 类型的数值需要明确指定 packed=true,才会用更高效的编码方式,如下:
repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];
proto3 中不需要进行显式配置,默认就是压缩的。
proto2 中通过 default option 设置默认值的方式被删除,proto3 不再支持。
// proto3 不支持 default option
optional int32 result_per_page = 3 [default = 10];
proto3 约定未赋值的 filed 默认为对应类型的零值:
注意:如果一个 field 的值为 default,序列化时不会包含该字段。
如果要继续使用之前生成的代码(或存量系统依然再使用),更新接口文件时,需要注意以下几点,Updating A Message Type:
新定义的 oneof 字段
,是安全的且二进制兼容。同理,把只包含一个 field 的 oneof 修改为一个 optional field 或者 extension 也是安全的新定义的 oneof 字段
,只有在这些字段不会存在同时赋值的情况下,才是安全的已经存在的 oneof 字段
是不安全的proto3 需要在文件开头声明语法版本为 proto3,如果不用 syntax 声明会被认为使用 proto2 语法。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
proto3 中要求 enum 必须从 0 开始,并且 0 是默认枚举值
proto3 需要在文件开头声明语法版本为 proto3,如果不用 syntax 声明会被认为使用 proto2 语法。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
用 package 声明当前 proto 文件的包名,声明包名后,其它 proto 文件通过 package name 引用包内的定义。
package foo.bar;
message Open { ... }
其它 proto 文件通过 package name 引用:
message Foo {
required foo.bar.Open open = 1;
}
没有用 option go_package = “XXX” 指定生成的 Go 代码所在路径时,默认使用 package name 作为生成代码的 package。
google/protobuf/descriptor.proto 列出了所有支持的 option 以及作用。
option 分为file-level
、message-level
和 field-level
三种级别,分别在不同的位置使用。
file 级别的 options 用法:
option go_package = "google.golang.org/protobuf/types/descriptorpb";
option java_package = "com.google.protobuf";
option java_outer_classname = "DescriptorProtos";
option csharp_namespace = "Google.Protobuf.Reflection";
option objc_class_prefix = "GPB";
option cc_enable_arenas = true;
// descriptor.proto must be optimized for speed because reflection-based
// algorithms don't work during bootstrapping.
option optimize_for = SPEED;
message 级别的 option 用法:
message Foo {
option message_set_wire_format = true;
extensions 4 to max;
}
field 级别的 option 用法:
repeated int32 samples = 4 [packed = true];
optional int32 old_field = 6 [deprecated=true];
可以通过扩展 google.protobuf.XXXOptions 增加自定义的 option:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234; // 自定义 message-level option
}
message MyMessage {
option (my_option) = "Hello world!";
}
用 import 导入目标文件:
import "myproject/other_protos.proto";
A 文件通过 import public
引用 B 文件后,只需引用 A 文件就可以直接使用 B 文件中的定义。
import public 不支持 java,生成 java 代码时不能使用该功能。
// 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
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
// 引用 SearchResponse 中定义的 Result
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
proto3 可以引用 proto2 中的的定义,但是不能直接引用 proto2 中的 enum。
rpc 接口定义方法:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
protoc 不会为 RPC Service 生成对应代码,需要由选用的 RPC 服务框架提供生成方法。RPC 服务框架推荐使用 Google 开源的 gRPC 框架,和 protobuf 原生配套。
Language Guide (proto3) 中没有提到 stream 的用法,V3 Specification 语法显示 rpc 接口的输入参数和返回数据可以用 stream 修饰。
service = "service" serviceName "{" { option | rpc | emptyStatement } "}"
rpc = "rpc" rpcName "(" [ "stream" ] messageType ")" "returns" "(" [ "stream" ]
messageType ")" (( "{" {option | emptyStatement } "}" ) | ";")
gRPC Service definition 对 stream 的用法做了简单介绍,stream 表示支持 a sequence of messages
,按照作用位置可以把 rpc 接口分为四类。
// 请求和响应都是一个 message
rpc SayHello(HelloRequest) returns (HelloResponse);
// 请求是一个 message,响应是多个 message
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
// 请求是多个 message,响应是一个 message
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
// 请求和响应都是多个 message
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
Streaming Multiple Messages 中提到, protobuf 在协议上没发区分消息的,需要由发送者/接收者自行根据 message size 进行分割。gRPC 框架能够自动生成 stream 相关的处理代码,参考 Basics tutorial 中的例子以及 grpc 的实现代码。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
field 的编号:
注意事项 :
extensions 用来预留可以被第三方使用的字段编号:
message Foo {
// ...
extensions 100 to 199;
}
// 另一个 proto 文件,扩展 Foo:
extend Foo {
optional int32 bar = 126; // 扩展的 field 不能是 oneof、map
}
支持的类型:Scalar Value Types
double
float
int32
int64
uint32
uint64
sint32: 对负数的编码更高效
sint64: 对负数的编码更高效
fixed32:永远 4 字节,对应大于 2^28 的数值编码更高效
fixed64:永远 8 字节,对应大于 2^56 的数值编码更高效
sfixed32: 永远 4 字节
sfixed64: 永远 8 字节
bool:
string:UTF-8 或者 7-bit ASCII Text
bytes:
field 类型:
enum 使用 32bit,使用 varint 编码,对负数编码效率低,不建议使用负数。
enum 可以独立定义,也可以在 message 内部定义:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
enum EnumAllowingAlias {
// 枚举值存在重复时,如果不指定 allow_alias=true,会报错
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
可以保留部分枚举数值,max 表示最大值:
enum Foo {
reserved 2, 15, 9 to 11, 40 to max; // 用 max 表示最大值
reserved "FOO", "BAR"; // 同一行中数值和名称不能混用
}
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
可以多层嵌套,位于不同层中的 message 可以重名:
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;
}
}
}
旧代码接收到新代码生成的消息时,新增的 field 不可辨认为 unknown fields。
Any 用于装载任意类型的 message,它是 protbuf 内置的类型,位于 google/protobuf/any.proto 文件:
message Any {
string type_url = 1;
bytes value = 2;
}
Any 有两个 filed:
how to use protobuf.any in golang 中提到,Go 可以用 “google.golang.org/protobuf/types/known/anypb” 中的 MarshalFrom() /UnmarshalTo()/UnmarshalNew() 将其它 message 转换成 Any 类型以及在转回。
import "testing"
import "proto_code_go/proto_gen/demo"
import "google.golang.org/protobuf/types/known/anypb"
import "google.golang.org/protobuf/proto"
func TestPackAny(t *testing.T) {
person := &demo.Person{
Name: "lijiaocn.com",
}
any := &anypb.Any{
TypeUrl: "",
Value: nil,
}
if err := anypb.MarshalFrom(any, person, proto.MarshalOptions{}); err != nil {
t.Errorf("anypb.MarshalFrom fail: err=%v", err)
} else {
t.Logf("%v", any)
}
anotherPersion, err := anypb.UnmarshalNew(any, proto.UnmarshalOptions{})
if err != nil {
t.Errorf("anypb.UnMarshalNew fail: err=%v", err)
} else {
t.Logf("%v", anotherPersion.ProtoReflect().Descriptor().FullName())
}
}
oneof 中不可以直接使用 map 和 repeated,在 proto2 中还要求不能使用 requried、optional。
message SampleMessage {
oneof test_oneof {
// 不可以直接使用 map 和 repeated
string name = 4;
// SubMessage 内部的 field 可以用 requried、optional、repeated 修饰
SubMessage sub_message = 9;
}
}
oneof 特性:
map<key_type, value_type> map_field = N;
map 的注意事项:
map 等价于下面的定义:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Proto3 开始支持标准的 JSON 编码,即将 proto 中的定义序列化成 JSON 格式以及反序列化 JSON 字符串,Proto3 JSON Mapping。
google.golang.org/protobuf/encoding 提供了 protocol buffer message、json format、textproto format 间的转换函数。