Protobuf使用规范

protobuf使用规范

翻译自 官网

一个简单示例

//指定语法为proto3
syntax = "proto3";
//定义一个名为SearchRequest的消息
message SearchRequest {
  optional string query = 1;
  required int32 page_number = 2;
  int32 results_per_page = 3;
}
// string,int32为字段类型,query等为字段名,等于右边数字为字段编号,范围为[1,536870911]。
// optional,required为字段标签

字段标签

  • required:必须的字段。没有字段标签时默认required。
  • optional :表示是一个可选字段 。
  • repeated:此字段类型可以在格式良好的消息中重复0次或多次。重复值的顺序将被保留。相当于数组。
  • map:配对的键/值字段类型。

字段类型

.proto Type C++ Type Java/Kotlin Type[1] Python Type[3] Go Type C# Type
double double double float float64 double
float float float float float32 float
int32 int32 int int int32 int
int64 int64 long int/long[4] int64 long
uint32 uint32 int[2] int/long[4] uint32 uint
uint64 uint64 long[2] int/long[4] uint64 ulong
sint32 int32 int int int32 int
sint64 int64 long int/long[4] int64 long
fixed32 uint32 int[2] int/long[4] uint32 uint
fixed64 uint64 long[2] int/long[4] uint64 ulong
sfixed32 int32 int int int32 int
sfixed64 int64 long int/long[4] int64 long
bool bool boolean bool bool bool
string string String str/unicode[5] string string
bytes string ByteString str (Python 2) bytes (Python 3) []byte ByteString

字段编号

  • 1到536870911之间

  • 在同一个message中字段编号必须唯一,即不能重复

  • 19000到19999是协议缓冲区保留的,不要使用

  • 不能使用任何先前保留的字段号或任何已分配给扩展的字段号

  • 定义好了不要随意更改

  • 优先使用1到15,只占一个字节,16到2047占两个字节

删除字段

如果操作不当,删除字段可能会导致严重的问题。
当不再需要字段,并且所有引用都已从客户端代码中删除时,可以从消息中删除字段定义。但是,您必须保留已删除的字段号。防止将来重用该编号。也应该保留字段名,以允许消息的JSON和TextFormat编码继续解析。

保留字段

将已删除的字段号添加到保留列表中。为了确保消息的JSON和TextFormat实例仍然可以被解析,还要将删除的字段名称添加到保留列表中。如果将来的开发人员试图使用这些保留的字段号或名称,协议缓冲区编译器将会报错。

message Foo {
  reserved 2, 15, 9 to 11;	//2,15,9,10,11被保留,新字段不能使用
  reserved "foo", "bar";	//foo,bar被保留
}

编译.proto生成了什么?

  • 对于c++,编译器从每个.proto生成.h和.cc文件,并为文件中描述的每种消息类型提供一个类。

  • 对于Java,编译器生成一个. Java文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊Builder类。

  • 对于Kotlin,除了Java生成的代码外,编译器还为每种消息类型生成一个.kt文件,其中包含一个DSL,可用于简化创建消息实例。

  • Python稍有不同——Python编译器在.proto中生成一个带有每种消息类型静态描述符的模块,然后与元类一起使用,在运行时创建必要的Python数据访问类。

  • 对于Go,编译器生成一个.pb。为文件中的每种消息类型设置一个类型。

字段默认值

对于字符串,默认值是空字符串。
对于字节,默认值为空字节。
对于bool,默认值为false。
对于数字类型,默认值为零。
对于枚举,默认值是第一个定义的枚举值,该值必须为0。
对于消息字段,没有设置该字段。它的确切值与语言有关。有关详细信息,请参阅生成的代码指南。
重复字段的默认值为空(通常是相应语言中的空列表)。

请注意,对于标量消息字段,一旦解析了消息,就无法判断字段是显式设置为默认值(例如布尔值是否设置为false)还是根本没有设置:在定义消息类型时应该记住这一点。例如,如果你不希望某些行为在默认情况下也发生,不要使用布尔值在设置为false时打开某些行为。还要注意,如果将标量消息字段设置为其默认值,则该值将不会在网络上序列化。

枚举

注意:使用枚举时查看对应编程语言的生成代码指南

enum Corpus {
  CORPUS_UNSPECIFIED = 0;	//第一个值必须为0
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4;
}

删除枚举

如果你通过完全删除一个枚举项或注释掉它来更新一个枚举类型,将来的用户可以在对该类型进行更新时重用这个数值。如果他们后来加载相同的 .proto 的旧版本,这可能会导致严重的问题,包括数据损坏、隐私漏洞等等。确保这种情况不发生的一种方法是指定删除项的数值(和/或名称,这也会导致JSON序列化的问题)是保留的。如果将来有任何用户试图使用这些标识符,协议缓冲区编译器会发出警告。你可以使用max关键字指定保留的数值范围上升到最大可能值 。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;	
  reserved "FOO", "BAR";
}

消息嵌套

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

导入定义

使用其他.proto文件的消息

import "myproject/other_protos.proto";

如何避免被导入的.proto移动到其他位置导致导入失败

// new.proto
// 新位置
// old.proto
// 旧位置,只放import的.proto文件,用于给其他.proto导入使用
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
//

使用proto2消息

proto3中可以使用proto2消息,但是不能直接使用proto2的枚举,如果是proto2消息使用了proto2的枚举就没有关系。

嵌套类型

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

在其他消息中使用上面的Result

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

可以随意嵌套,Inner在不同的消息中,是完全独立的。

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

更新消息类型

  • 不要更改任何现有字段的字段号
  • 如果添加新字段,使用“旧”消息格式的代码序列化的任何消息仍然可以被新生成的代码解析。
  • 只要在更新后的消息类型中不再使用字段号,即可删除字段。
  • Int32、uint32、int64、uint64和bool都是兼容的这意味着您可以将一个字段从这些类型中的一种更改为另一种,而不会破坏向前或向后兼容性。
  • Sint32和sint64相互兼容,但与其他整数类型不兼容。
  • 只要字节是有效的UTF-8, string和bytes是兼容的。
  • 如果字节包含消息的编码版本,则嵌入式消息与字节兼容。
  • Fixed32与sfixed32兼容,fixed64与sfixed64兼容。
  • 对于字符串、字节和消息字段,optional与repeat兼容。
  • Enum在wire格式上兼容int32、uint32、int64和uint64(注意,不匹配的值会被截断)
  • 将单个可选字段或扩展名更改为新字段或扩展名的成员是二进制兼容的,但是对于某些语言(特别是Go),生成的代码的API将以不兼容的方式更改。
  • 将单个字段更改为可选字段或扩展名也是安全的。
  • 在映射<K, V>和相应的重复消息字段之间更改字段是二进制兼容的

未知字段

新版本新增一个字段,旧版本解释新版本的数据时,新增的字段就是未知字段。最初,proto3消息总是在解析过程中丢弃未知字段。在3.5及更高版本中,在解析过程中保留未知字段并包含在序列化输出中。

Any类型

Any类型类似接口一样可以代指所有类型

import "google/protobuf/any.proto";
message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

oneof

用于多选一,一个消息字段包含多个字段,但一次只会设置其中一个。这个设计是为了节约空间,由于多个字段只会设置一个,那么只需使用一份内存即可,其他未设置的字段会被清空,相当于没有传输其他字段。

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

拿到解析后的数据需要通过api去判断被设置的是哪一个值。

Maps

//语法: map<key_type, value_type> map_field = N;key不能为枚举,不能嵌套map
map<string, Project> projects = 3;
  • 映射字段标签不能是repeat
  • map是无序的
  • 在为.proto生成文本格式时,映射按键排序。数字键按数字排序。
  • 重复键后解析的会覆盖先解析的
  • When parsing from the wire or when merging, if there are duplicate map keys the last key seen is used. When parsing a map from text format, parsing may fail if there are duplicate keys.
  • 如果为映射字段提供键但没有值,则序列化字段时的行为依赖于语言。在c++、Java、Kotlin和Python中,该类型的默认值是序列化的,而在其他语言中则没有序列化。
  • map向后兼容,不支持map的protobuf实现任然可以处理map(本质是message)

service

定义接口

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

JSON Mapping

protobuf和json互转

proto3 JSON JSON example Notes
message object {"fooBar": v, "g": null, ...} 生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了' json_name '字段选项,则将使用指定的值作为键。解析器接受lowerCamelCase名称(或由' json_name '选项指定的名称)和原始原型字段名称。' null '是所有字段类型的可接受值,并被视为相应字段类型的默认值。但是,' null '不能用于' json_name '值。有关原因的更多信息,请参见对json_name进行更严格的验证
enum string "FOO_BAR" 使用proto中指定的enum值的名称。解析器接受枚举名和整数值。
map<K,V> object {"k": v, ...} 所有键都转换为字符串。
repeated V array [v, ...] ' null '被接受为空列表'[]'。
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON值将是使用带填充的标准base64编码编码为字符串的数据。标准或url安全的base64编码(带/不带填充)都可以接受。
int32, fixed32, uint32 number 1, -10, 0 JSON值将是一个十进制数。接受数字或字符串。
int64, fixed64, uint64 string "1", "-10" JSON值将是一个十进制字符串。接受数字或字符串。
float, double number 1.1, -10.0, 0, "NaN", "Infinity" JSON值将是一个数字或特殊字符串值“NaN”,“Infinity”和“-Infinity”之一。接受数字或字符串。指数表示法也被接受。-0被认为等于0
Any object {"@type": "url", "f": v, ... } 如果' Any '包含一个具有特殊JSON映射的值,则将其转换为如下格式:' {"@type": xxx, "value": yyy} '。否则,该值将被转换为JSON对象,并插入“@type”字段以指示实际的数据类型。
Timestamp string "1972-01-01T10:00:20.021Z" 使用RFC 3339,其中生成的输出将始终是z规范化的,并使用0、3、6或9个小数。除“Z”以外的偏移量也可以接受。
Duration string "1.000340012s", "1s" 生成的输出总是包含0、3、6或9个小数,具体取决于所需的精度,后面跟着后缀“s”。接受任何小数(也不接受),只要它们符合纳秒精度,并且需要后缀“s”。
Struct object { ... } 任何JSON对象。看“struct.proto”。
Wrapper types various types 2, "2", "foo", true, "true", null, 0, ... 包装器在JSON中使用与包装的原语类型相同的表示,除了在数据转换和传输期间允许并保留' null '。
FieldMask string "f.fooBar,h" 看“field_mask.proto”。
ListValue array [foo, bar, ...]
Value value 任何JSON值。具体请查看google.protobuf.Value
NullValue null JSON null
Empty object {} An empty JSON object

JSON Options

proto3 JSON实现可能提供以下选项:

Emit fields with default values :proto3 JSON输出默认省略带有默认值的字段。实现可以提供一个选项来覆盖此行为并使用其默认值输出字段。
Ignore unknown fields :Proto3 JSON解析器应该默认拒绝未知字段,但可能会提供在解析中忽略未知字段的选项。
Use proto field name instead of lowerCamelCase name :默认情况下,proto3 JSON打印机应该将字段名转换为lowerCamelCase并使用它作为JSON名称。实现可能会提供使用原型字段名作为JSON名称的选项。Proto3 JSON解析器需要接受转换后的lowerCamelCase名称和原型字段名称。
Emit enum values as integers instead of strings :在JSON输出中默认使用枚举值的名称。可以提供一个选项来使用枚举值的数值。

选项

option java_package = "com.example.foo";	//生成Java/Kotlin包,不指定使用`package`关键字
option java_outer_classname = "Ponycopter"; //生成Java类名
option java_multiple_files = true;			//为每个Java类型生成单独的.java文件
option optimize_for = CODE_SIZE;

// 是否生成service代码
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
repeated int32 samples = 4 [packed = false];	//使重复字段代码更紧凑
int32 old_field = 6 [deprecated = true];		//该字段已弃用,枚举字段可用该选项

自定义选项

Option Retention(选项保留)

Option Targets

生成代码

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
  • --proto_path等价于-I

  • --cpp_out在DST_DIR中生成 C++ 代码

  • --java_out在DST_DIR中生成 Java 代码

  • --kotlin_out在DST_DIR中生成 Kotlin 代码

  • --python_out在DST_DIR中生成 Python 代码

  • --go_out在DST_DIR中生成 Go 代码

  • --ruby_out在DST_DIR中生成 Ruby 代码

  • --objc_out在DST_DIR中生成 Objective-C 代码

  • --csharp_out在DST_DIR中生成 C# 代码

  • --php_out在DST_DIR中生成 PHP 代码

文件位置

不要讲.proto文件和其他语言源文件放在相同的目录下,考虑在项目的根包下为.proto文件创建子包proto。

posted @ 2023-11-23 18:24  longan55  阅读(504)  评论(0)    收藏  举报