目录

ProtoBuf笔记

ProtoBuf笔记

官方文档 中文文档

尽量少抄文档吧。。。感觉抄文档的笔记没有一点意义,真要查些什么都直接查文档了。

定义:Protocol Buffers 是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。

看起来有点懵,举个例子。我们随便定义一个结构体,在不同语言中定义方法是不同的:

struct Person
{
    char name[50];
    int age;
};

struct Person {
    std::string name;
    int age;
};
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
type Person struct {
    Name string
    Age  int
}

而protobuf提供了一种通用的定义方式,它可以通过工具转换成任意一种语言。

message Person {
  string name = 1;
  int32 age = 2;
}

protobuf本质上是一种序列化技术。什么是序列化?这里不扯定义,简单粗暴地说,就是把数据转换成另一种更方便更通用的形式,方便进行存储和传输。你在程序里有个结构体,你可以把它当参数在这个程序里面传来传去。但如果你要把它存到磁盘,可能就得存一个txt或json,这个过程你就可以理解为序列化。你用另一个程序把这个txt或json读到程序里的结构体,然后就可以自由操作这个结构体了,这个过程就可以理解为反序列化。上面讲的是存储,而传输也是一样的道理。json就是序列化技术之一,而protobuf也是类似的。而相比json的轻量级标记,protobuf则有着更接近编程语言的语法和工具链支持。

protobuf常用于跨语言,跨项目的通信协议和数据存储,如RPC等。具体什么是RPC这里不予赘述。

protobuf生成go代码时,需要使用 go_package 选项指定生成代码的包的导入路径。包名为最后一个路径组件。如下,包名为tutorialpb

option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";

使用protobuf生成go代码,需要额外安装插件protoc-gen-go,如果是gRPC开发还需安装protoc-gen-go-grpc

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

编译器插件 protoc-gen-go 将安装在 $GOBIN 中,默认为 $GOPATH/bin。 需要将其添加到$PATH 中,以便协议编译器 protoc 能够找到它。

Linux添加PATH

export PATH="$PATH:$(go env GOPATH)/bin"

编译命令中使用--go_out--go-grpc_out指定输出目录。

protoc --go_out=proto --go-grpc_out=proto .\proto\test.proto

说是不要抄文档,但语法部分还是当备忘录抄一下。毕竟平时用的不多老忘。抄一下主要的部分。

  • optional: 显示指定字段是否可选。如果没有这个标记,将无法区分字段未设置还是设置了零值。对于Go语言,使用了optional后字段类型将变成指针,同时提供HasXxx()方法判断是否设置,GetXxx()方法安全获取值(未设置返回零值)。

  • repeated: 此字段可以重复零次或多次,重复值的顺序将被保留。相当于数组/列表。

  • map: 键值对字段类型。相当于repeated字段的一种语法糖。即如下两种写法是基本是等价的,区别是map序列化后顺序不会保留。

    // map
    message Test1 {
        map<string, int32> g = 7;
    }
    
    // repeated
    message Test2 {
        message g_Entry {
            string key = 1;
            int32 value = 2;
        }
        repeated g_Entry g = 7;
    }
  • 优先使用 C/C++/Java 风格的行尾注释 //,放在 .proto 代码元素的前一行。
  • 也接受 C 风格的内联/多行注释 /* ... */
    • 使用多行注释时,推荐使用 * 作为边距行。
/**
 * SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response.
 */
message SearchRequest {
  string query = 1;

  // Which page number do we want?
  int32 page_number = 2;

  // Number of results to return per page.
  int32 results_per_page = 3;
}

删除字段后建议保留字段编号和字段名。重用字段编号可能会导致严重问题,而重用旧字段名通常是安全的,但仍建议保留。示例如下:

message Foo {
  reserved 2, 15, 9 to 11, 40 to max;;
  reserved "foo", "bar";
}

protobuf中提供的标量类型如下:

Proto 类型说明
double使用 IEEE 754 双精度格式。
float使用 IEEE 754 单精度格式。
int32使用可变长度编码。对于编码负数效率低下——如果你的字段可能包含负值,请改用 sint32。
int64使用可变长度编码。对于编码负数效率低下——如果你的字段可能包含负值,请改用 sint64。
uint32使用可变长度编码。
uint64使用可变长度编码。
sint32使用可变长度编码。有符号整数值。这些比常规的 int32 更高效地编码负数。
sint64使用可变长度编码。有符号整数值。这些比常规的 int64 更高效地编码负数。
fixed32总是四个字节。如果值经常大于 228,比 uint32 更高效。
fixed64总是八个字节。如果值经常大于 256,比 uint64 更高效。
sfixed32总是四个字节。
sfixed64总是八个字节。
bool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,且长度不能超过 232。
bytes可包含任意字节序列,长度不超过 232。
  • 对于字符串,默认值是空字符串。
  • 对于字节,默认值是空字节。
  • 对于布尔值,默认值是 false。
  • 对于数值类型,默认值是零。
  • 对于消息字段,该字段未设置。其确切值取决于语言。详情请参阅生成的代码指南
  • 对于枚举,默认值是第一个定义的枚举值,它必须为 0。请参阅枚举默认值
  • 对于 repeated 字段,默认值是空的(通常是相应语言中的空列表)。
  • 对于 map 字段,默认值是空的(通常是相应语言中的空 map)。

可以使用enum定义枚举值。示例如下:

enum BookStatus {
  option allow_alias = true;
  BOOK_STATUS_UNSPECIFIED = 0;
  BOOK_STATUS_AVAILABLE = 1;
  BOOK_STATUS_ONSHELF = 1;
  BOOK_STATUS_UNAVAILABLE = 2;
  BOOK_STATUS_OFFSHELF = 2;
}

message Book {
  int64 id = 1;
  string title = 2;
  string author = 3;
  BookStatus status = 4;
}

需要注意的是,枚举定义中定义的第一个值必须为零,并且应该命名为ENUM_TYPE_NAME_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。这是因为

  • 必须有一个零值,以便我们可以将 0 用作数字默认值。
  • 零值需要是第一个元素,以便与 proto2 语义兼容,其中除非明确指定了不同的值,否则第一个枚举值是默认值。

可以将 allow_alias 选项设置为 true来运行别名。

oneof 中的所有字段共享内存,并且一次最多只能设置一个字段。果设置了多个值,则由 proto 中的顺序决定的最后一个设置的值将覆盖所有先前的值。

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

如果想将的消息类型与 RPC(远程过程调用)系统一起使用,您可以在 .proto 文件中定义一个 RPC 服务接口,protocol buffer 编译器将以选择的语言生成服务接口代码和存根 (stub)。

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

提供了标量类型以外的预定义类型,位于google.protobuf包,源码在protocolbuffers/protobufsrc/google/protobuf目录下。

场景推荐类型
需要精确时间点Timestamp
需要时间间隔Duration
需要表示空响应Empty
需要区分 0/null/未设置包装类型(Int32Value 等)
需要动态 JSON 数据Struct
需要部分更新FieldMask
需要动态类型Any
需要二进制数据bytes
不需要值区分直接使用基础类型(int32, string 等)
特性StructAnyValue
本质JSON 对象类型擦除的消息JSON 值
数据格式Map + 动态类型任意 Protobuf 消息任意 JSON 类型
类型安全弱(运行时检查)强(需显式类型 URL)弱(运行时检查)
跨语言JSON 兼容Protobuf 消息JSON 兼容
典型用途动态配置、元数据插件系统、多态任意值存储

选择的关键在于:是需要存储 JSON 数据(用 Struct/Value),还是需要存储类型安全的 Protobuf 消息(用 Any)。

这些包装类型主要用于兼容proto2,解决未设置和设置为默认值的问题。proto3中加入了optional,就不需要用到包装类型了。

Duration 表示一个带符号的固定长度时间跨度,以秒和纳秒分辨率的秒分数表示。它独立于任何日历以及“天”或“月”等概念。它与 Timestamp 相关,因为两个 Timestamp 值之间的差是 Duration,并且可以从 Timestamp 中添加或减去。范围大约为 ±10,000 年。

Timestamp 表示一个独立于任何时区或日历的时间点,以 UTC 纪元时间中的秒和纳秒分辨率的秒分数表示。Timestamp 类型以 RFC 3339 格式编码为字符串:“{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z”。

字段名Type描述
secondsint64自 Unix 纪元 1970-01-01T00:00:00Z 以来 UTC 时间的秒数。必须在 0001-01-01T00:00:00Z 到 9999-12-31T23:59:59Z 之间(含)。
nanosint32纳秒分辨率的非负小数秒。带有小数的负秒值仍必须具有向前计时的非负纳秒值。必须在 0 到 999,999,999 之间(含)。

FieldMask 表示一组符号字段路径。该字段掩码用于指定应由获取操作返回(一个 投影)或由更新操作修改的字段子集。字段掩码也有自定义的 JSON 编码(见下文)。

// 字段掩码
paths: "f.a"
paths: "f.b.d"

// 应用前
f {
  a : 22
  b {
    d : 1
    x : 2
  }
  y : 13
}
z: 8

// 应用后
f {
  a : 22
  b {
    d : 1
  }
}

代码示例:

// protobuf
mask {
  paths: "user.display_name"
  paths: "photo"
}
// json
{
  mask: "user.displayName,photo"
}
字段名Type描述
pathsstring字段掩码路径集。

Any 包含一个任意序列化消息以及描述序列化消息类型的 URL。

package google.profile;
message Person {
  string first_name = 1;
  string last_name = 2;
}
{
  "@type": "type.googleapis.com/google.profile.Person",
  "firstName": <string>,
  "lastName": <string>
}
字段名Type描述
type_urlstring一个 URL/资源名称,其内容描述了序列化消息的类型。
valuebytes必须是上述指定类型的有效序列化数据。

Struct 表示一个结构化数据值,由映射到动态类型值的字段组成。在某些语言中,Struct 可能由原生表示支持。

字段名Type描述
fieldsmap<string, Value>动态类型值的映射。

Value 表示一个动态类型的值,可以是 null、数字、字符串、布尔值、递归结构体值或值列表。值的生成者应设置其中一种变体,缺少任何变体表示错误。

Value 的 JSON 表示是 JSON 值。

Value为联合字段,以下只有一个:

字段名Type描述
null_valueNullValue表示一个空值。
number_valuedouble表示一个双精度值。
string_valuestring表示一个字符串值。
bool_valuebool表示一个布尔值。
struct_valueStruct表示一个结构化值。
list_valueListValue表示一个重复的 Value。

ListValue 是一个包含重复值字段的包装器。

ListValue 的 JSON 表示是 JSON 数组。

字段名Type描述
valuesValue动态类型值的重复字段。

NullValue 是一个单例枚举,表示 Value 类型联合的空值。

NullValue 的 JSON 表示是 JSON null。

枚举值描述
NULL_VALUE空值。

对于原生protoc生态,可以使用protoc-gen-validate(PGV)插件。而对于新项目/buf生态,更推荐使用protovalidate,支持CEL表达式,有着更完善强大的验证功能。可以在CEL by Example快速学习了解cel在protovalidate中的使用。