Protocol Buffers V3 版本语法要点记录

Tags: protobuf 

目录

Protocol Buffers V3 vs V2

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

V3 删除的语法

删除 required

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;
}

删除数值数组显式声明 packed 的约束

proto2 因为历史原因,scalar numeric types 类型的数值需要明确指定 packed=true,才会用更高效的编码方式,如下:

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

proto3 中不需要进行显式配置,默认就是压缩的。

删除 default option

proto2 中通过 default option 设置默认值的方式被删除,proto3 不再支持。

// proto3 不支持 default option
optional int32 result_per_page = 3 [default = 10];

proto3 约定未赋值的 filed 默认为对应类型的零值:

  • For strings, the default value is the empty string.
  • For bytes, the default value is empty bytes.
  • For bools, the default value is false.
  • For numeric types, the default value is zero.
  • For enums, the default value is the first defined enum value, which must be 0.
  • For message fields, the field is not set. Its exact value is language-dependent.
  • For repeated fields, the default value is empty (generally an empty list in the appropriate language).

注意:如果一个 field 的值为 default,序列化时不会包含该字段。

V3 接口文件更新注意事项

如果要继续使用之前生成的代码(或存量系统依然再使用),更新接口文件时,需要注意以下几点,Updating A Message Type

  • 不要修改已有的 field 的 field num
  • 新代码解析旧代码生成的消息时,新增字段解析为为默认值;旧代码解析新代码生成的消息时,忽视不能辨认的新增字段
  • 可以删除不再使用的 field,但是 field 的 field num 不能再次被使用(可以给废弃字段增加OBSOLETE_前缀,或者用 reserved 保留曾被使用过的 field_num)
  • int32,uint32,int64,uint64,bool 是完全兼容的,可以互相切换,数值转换规则和 C++ 相同
  • sint32, sint64 互相兼容(和其它类型不兼容)
  • bytes 为 UTF-8 时,和 string 互相兼容
  • 嵌套定义的 field 和包含 encoded version 的 bytes 兼容
  • fixed32 和 sfixed32 兼容
  • fixed64 和 sfixed64 兼容
  • 非数值类型的 repeated 字段和对应的 singular 字段可以兼容,singular 字段取数组中的最后一个值(不适用于数值类型的数组,因为数值类型的数组会进行压缩传递)
  • enum 和 int32,uint32,int64,uint63 在序列化后的数值是兼容的,但是在生成的代码中可能是不兼容的类型
  • 把一个 optional field 或者 extension 挪入新定义的 oneof 字段,是安全的且二进制兼容。同理,把只包含一个 field 的 oneof 修改为一个 optional field 或者 extension 也是安全的
  • 把多个 fields 挪入新定义的 oneof 字段,只有在这些字段不会存在同时赋值的情况下,才是安全的
  • 把 field 挪入已经存在的 oneof 字段 是不安全的

V3 新增的语法或约束

syntax 声明

proto3 需要在文件开头声明语法版本为 proto3,如果不用 syntax 声明会被认为使用 proto2 语法。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

Enum 必须从 0 开始

proto3 中要求 enum 必须从 0 开始,并且 0 是默认枚举值

V3 的完整语法要点

syntax 声明

proto3 需要在文件开头声明语法版本为 proto3,如果不用 syntax 声明会被认为使用 proto2 语法。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

设置包名 - package

用 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。

配置项 - option

google/protobuf/descriptor.proto 列出了所有支持的 option 以及作用。

option 分为file-levelmessage-levelfield-level 三种级别,分别在不同的位置使用。

file-level options

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-level options

message 级别的 option 用法:

message Foo {
  option message_set_wire_format = true;
  extensions 4 to max;
}

field-level options

field 级别的 option 用法:

repeated int32 samples = 4 [packed = true];
optional int32 old_field = 6 [deprecated=true];

自定义 options

可以通过扩展 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!";
}

package 引用

用 import 导入目标文件:

import "myproject/other_protos.proto";

import public

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

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

引用嵌套定义 message

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;
}

引用 proto2 的定义

proto3 可以引用 proto2 中的的定义,但是不能直接引用 proto2 中的 enum。

RPC Service 接口定义

rpc 接口定义方法:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

protoc 不会为 RPC Service 生成对应代码,需要由选用的 RPC 服务框架提供生成方法。RPC 服务框架推荐使用 Google 开源的 gRPC 框架,和 protobuf 原生配套。

stream 类型输入和输出

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 定义

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

field 的编号:

  1. field 编号发布后不能再更改
  2. 编号 1~15 用一个字节描述,编号 16~2047 用两个字节
  3. 编号最小值是 1,最大 2^29-1
  4. 19000~19999 用于内部实现,不能使用

注意事项 :

  1. 不要修改 field 的编号
  2. field 可以删除,但是 field num 不能再次分配给其它 field

extensions 预留用于扩展的 field 编号

extensions 用来预留可以被第三方使用的字段编号:

message Foo {
  // ...
  extensions 100 to 199;
}

// 另一个 proto 文件,扩展 Foo:
extend Foo {
  optional int32 bar = 126;     // 扩展的 field 不能是 oneof、map
}

标准数值类型 Scalar Value Types

支持的类型: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:

reserved

repeated

field 类型:

  1. repeated 修饰的 field,会保证参数顺序

枚举类型 - enum

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 别名

enum EnumAllowingAlias {
  // 枚举值存在重复时,如果不指定 allow_alias=true,会报错
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum 保留值

可以保留部分枚举数值,max 表示最大值:

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;   // 用 max 表示最大值
  reserved "FOO", "BAR";                // 同一行中数值和名称不能混用
}

嵌套类型 - Nested Types

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;
    }
  }
}

未知字段 - Uknown Fields

旧代码接收到新代码生成的消息时,新增的 field 不可辨认为 unknown fields。

任意类型 - Any

Any 用于装载任意类型的 message,它是 protbuf 内置的类型,位于 google/protobuf/any.proto 文件:

message Any {
  string type_url = 1;
  bytes value = 2;
}

Any 有两个 filed:

  • type_url 是 message 的类型标识符,默认为 type.googleapis.com/packagename.messagename
  • value 是序列化后值

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

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 特性:

  • 如果代码多次设置 oneof 中不同 field 的值,只会保留最后一次设置的值
  • 如果反序列化时遇到多个 value,选用最后一个
  • oneof 不能用 repeated 修饰
  • 把其它 field 移入已有的 oneof 或者从已有的 oneof 移出,会导致数据丢失

哈希类型 - map

map<key_type, value_type> map_field = N;

map 的注意事项:

  • key_type 不能是 float 和 bytes
  • value_type 不能是 map
  • map 不能用 repeated, optional, required 修饰
  • map 内的 key-value 顺序是不保证的

map 等价于下面的定义:

message MapFieldEntry {
  optional key_type key = 1;
  optional value_type value = 2;
}

repeated MapFieldEntry map_field = N;

JSON Mapping

Proto3 开始支持标准的 JSON 编码,即将 proto 中的定义序列化成 JSON 格式以及反序列化 JSON 字符串,Proto3 JSON Mapping

google.golang.org/protobuf/encoding 提供了 protocol buffer message、json format、textproto format 间的转换函数。

参考

  1. 李佶澳的博客
  2. Protocol Buffer imports not recognized in Intellij
  3. PLanguage Guide(proto2)
  4. Language Guide (proto3)
  5. API Reference
  6. why messge type remove ‘required,optional’?
  7. Updating A Message Type
  8. how to use protobuf.any in golang
  9. gRPC
  10. Proto3 JSON Mapping
  11. google/protobuf/descriptor.proto
  12. Protocol Buffers API Reference
  13. google.golang.org/protobuf/encoding
  14. Protocol Buffers Version 3 Language Specification
  15. gRPC Service definition
  16. Streaming Multiple Messages
  17. gRPC Go: Basics tutorial

推荐阅读

Copyright @2011-2019 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: [email protected],备注网站合作

友情链接:  系统软件  程序语言  运营经验  水库文集  网络课程  微信网文  发现知识星球