概览

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

它就像 JSON,但更小更快,并且能生成原生语言绑定。你只需定义一次你想要的数据结构,然后就可以使用特殊生成的源代码,轻松地使用多种语言从各种数据流中读写你的结构化数据。

Protocol Buffers 是定义语言(在 .proto 文件中创建)、proto 编译器生成的用于与数据交互的代码、特定语言的运行时库、写入文件(或通过网络连接发送)的数据的序列化格式以及序列化数据的组合。

Protocol Buffers 解决了什么问题?

Protocol Buffers 为大小可达几兆字节的类型化结构化数据包提供了一种序列化格式。该格式既适用于临时网络流量,也适用于长期数据存储。Protocol Buffers 可以扩展新信息,而不会使现有数据失效或需要更新代码。

Protocol Buffers 是 Google 最常用的数据格式。它们广泛用于服务器间通信以及磁盘上数据的归档存储。Protocol buffer 的 *消息* 和 *服务* 由工程师编写的 .proto 文件描述。下面展示了一个 message 示例:

edition = "2023";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
}

在构建时,proto 编译器会对 .proto 文件进行调用,以生成各种编程语言(在本主题后面的 跨语言兼容性 中介绍)的代码,用于操作相应的 protocol buffer。每个生成的类都包含每个字段的简单访问器以及将整个结构序列化为原始字节和从原始字节解析的方法。下面向您展示一个使用这些生成方法的示例:

Person john = Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .build();
output = new FileOutputStream(args[0]);
john.writeTo(output);

由于 Protocol Buffers 在 Google 的各种服务中被广泛使用,并且其中的数据可能会持续存在一段时间,因此保持向后兼容性至关重要。Protocol Buffers 允许无缝支持变更,包括添加新字段和删除现有字段,而不会破坏现有服务。有关此主题的更多信息,请参阅本主题后面的 在不更新代码的情况下更新 Proto 定义

使用 Protocol Buffers 有什么好处?

在任何需要以语言无关、平台无关、可扩展的方式序列化结构化的、类似记录的、类型化数据的情况下,Protocol Buffers 都是理想的选择。它们最常用于定义通信协议(与 gRPC 一起)和数据存储。

使用 Protocol Buffers 的一些优点包括:

  • 紧凑的数据存储
  • 快速解析
  • 支持多种编程语言
  • 通过自动生成的类优化功能

跨语言兼容性

用任何受支持的编程语言编写的代码都可以读取相同的消息。你可以在一个平台上用 Java 程序从一个软件系统捕获数据,根据 .proto 定义将其序列化,然后在一个运行在另一个平台上的独立 Python 应用程序中从该序列化数据中提取特定值。

protocol buffers 编译器 protoc 直接支持以下语言:

以下语言由 Google 支持,但其项目源代码位于 GitHub 仓库中。protoc 编译器为这些语言使用插件:

其他语言并非由 Google 直接支持,而是由其他 GitHub 项目支持。这些语言在 Protocol Buffers 的第三方附加组件 中有所介绍。

跨项目支持

你可以在项目中通过定义位于特定项目代码库之外的 .proto 文件中的 message 类型来跨项目使用 protocol buffers。如果你定义的 message 类型或枚举预计会在你的直属团队之外被广泛使用,你可以将它们放在一个没有依赖项的独立文件中。

Google 内部广泛使用的 proto 定义的几个例子是 timestamp.protostatus.proto

在不更新代码的情况下更新 Proto 定义

软件产品向后兼容是标准做法,但向前兼容则不太常见。只要你在更新 .proto 定义时遵循一些简单的实践,旧代码将可以无问题地读取新消息,并忽略任何新添加的字段。对于旧代码来说,被删除的字段将具有其默认值,而被删除的重复字段将为空。有关“重复”字段的信息,请参阅本主题后面的 Protocol Buffers 定义语法

新代码也将透明地读取旧消息。新字段不会出现在旧消息中;在这些情况下,protocol buffers 提供了一个合理的默认值。

什么时候 Protocol Buffers 不是一个好的选择?

Protocol Buffers 并不适用于所有数据。特别是:

  • Protocol Buffers 倾向于假设整个消息可以一次性加载到内存中,并且不大于一个对象图。对于超过几兆字节的数据,请考虑其他解决方案;在处理较大数据时,由于序列化副本的存在,你可能最终会得到数据的多个副本,这可能会导致内存使用量出现惊人的激增。
  • 当 Protocol Buffers 被序列化时,相同的数据可以有许多不同的二进制序列化形式。在没有完全解析的情况下,你无法比较两个消息是否相等。
  • 消息不被压缩。虽然消息可以像其他文件一样被 zip 或 gzip 压缩,但对于特定类型的数据,像 JPEG 和 PNG 使用的专用压缩算法会产生小得多的文件。
  • 对于许多涉及大型、多维浮点数数组的科学和工程用途,Protocol Buffer 消息在大小和速度上都不是最优的。对于这些应用,FITS 和类似的格式开销更小。
  • 在科学计算中流行的非面向对象语言(如 Fortran 和 IDL)中,Protocol Buffers 的支持并不好。
  • Protocol Buffer 消息本身不描述其数据,但它们有一个完全反射的模式,你可以用它来实现自描述。也就是说,没有其对应的 .proto 文件,你无法完全解释一个消息。
  • Protocol Buffers 不是任何组织的正式标准。这使得它们不适合在有法律或其他要求必须基于标准构建的环境中使用。

谁在使用 Protocol Buffers?

许多项目使用 Protocol Buffers,包括以下这些:

Protocol Buffers 是如何工作的?

下图展示了如何使用 protocol buffers 处理数据。

Compilation workflow showing the creation of a proto file, generated code, and compiled classes
图 1. Protocol Buffers 工作流程

protocol buffers 生成的代码提供了实用方法,用于从文件和流中检索数据、从数据中提取单个值、检查数据是否存在、将数据序列化回文件或流,以及其他有用的功能。

以下代码示例向您展示了在 Java 中的此流程。如前所示,这是一个 .proto 定义:

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
}

编译此 .proto 文件会创建一个 Builder 类,您可以用它来创建新实例,如下面的 Java 代码所示:

Person john = Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .build();
output = new FileOutputStream(args[0]);
john.writeTo(output);

然后,你可以使用 protocol buffers 在其他语言(如 C++)中创建的方法来反序列化数据:

Person john;
fstream input(argv[1], ios::in | ios::binary);
john.ParseFromIstream(&input);
int id = john.id();
std::string name = john.name();
std::string email = john.email();

Protocol Buffers 定义语法

在定义 .proto 文件时,你可以指定字段基数(单一或重复)。在 proto2 和 proto3 中,你还可以指定字段是否为可选的。在 proto3 中,将字段设置为可选会将其从隐式存在更改为显式存在

设置字段的基数后,你需要指定数据类型。Protocol Buffers 支持常见的原始数据类型,如整数、布尔值和浮点数。完整列表请参阅标量值类型

字段也可以是:

  • 一个 message 类型,这样你可以嵌套部分定义,例如用于重复的数据集。
  • 一个 enum 类型,这样你可以指定一个可选值的集合。
  • 一个 oneof 类型,当一个消息有许多可选字段且最多只有一个字段会同时被设置时,你可以使用它。
  • 一个 map 类型,用于向你的定义中添加键值对。

消息可以允许通过 **extensions** 在消息自身之外定义字段。例如,protobuf 库的内部消息模式允许为自定义的、特定于用途的选项提供扩展。

有关可用选项的更多信息,请参阅 proto2proto3edition 2023 的语言指南。

设置基数和数据类型后,你需要为字段选择一个名称。在设置字段名称时,需要记住一些事情:

  • 在生产环境中使用后,更改字段名称有时会很困难,甚至不可能。
  • 字段名称不能包含破折号。有关字段名称语法的更多信息,请参阅消息和字段名称
  • 对重复字段使用复数名称。

为字段分配名称后,你需要分配一个字段编号。字段编号不能被重新调整用途或重用。如果删除一个字段,应保留其字段编号,以防止有人意外重用该编号。

额外的数据类型支持

Protocol Buffers 支持许多标量值类型,包括使用可变长度编码和固定大小的整数。你还可以通过定义消息来创建自己的复合数据类型,这些消息本身就是可以分配给字段的数据类型。除了简单和复合值类型外,还发布了几种常用类型

历史

要了解 protocol buffers 项目的历史,请参阅 Protocol Buffers 的历史

Protocol Buffers 开源理念

Protocol Buffers 于 2008 年开源,旨在为 Google 之外的开发者提供我们内部从中获得的同样好处。我们通过定期更新语言来支持开源社区,因为我们进行这些更改以支持我们的内部需求。虽然我们接受外部开发者的部分拉取请求,但我们不能总是优先处理不符合 Google 特定需求的功能请求和错误修复。

开发者社区

要了解 Protocol Buffers 即将发生的变更,并与 protobuf 开发者和用户联系,请加入 Google Group

其他资源