概览

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

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

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

Protocol Buffers 解决什么问题?

Protocol Buffers 提供了一种序列化格式,用于处理大小可达几兆字节的、带类型、结构化的数据包。这种格式适用于瞬时网络流量和长期数据存储。无需使现有数据失效或要求更新代码,即可使用新信息扩展 Protocol Buffers。

Protocol Buffers 是 Google 最常用的数据格式。它们被广泛用于服务器间通信以及磁盘上的数据归档存储。Protocol Buffer 的 消息(message)服务(service) 由工程师编写的 .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 可以无缝支持对任何 Protocol Buffer 的更改,包括添加新字段和删除现有字段,而不会破坏现有服务。有关此主题的更多信息,请参阅本主题后面的 无需更新代码即可更新 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 定义时遵循一些简单实践,旧代码就可以毫无问题地读取新消息,并忽略任何新添加的字段。对于旧代码来说,已删除的字段将具有其默认值,而已删除的 repeated 字段将为空。有关“repeated”字段的信息,请参阅本主题后面的Protocol Buffers 定义语法

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

Protocol Buffers 何时不适用?

Protocol Buffers 不适用于所有数据。特别地

  • Protocol Buffers 倾向于假设整个消息可以一次加载到内存中,且大小不超过一个对象图。对于超过几兆字节的数据,请考虑使用其他解决方案;处理更大的数据时,由于序列化副本的存在,你实际上可能会得到数据的多个副本,这可能导致内存使用量出现令人意外的峰值。
  • Protocol Buffers 序列化后,相同的数据可以有许多不同的二进制序列化形式。你无法在完全解析它们之前比较两个消息是否相等。
  • 消息未压缩。虽然消息可以像其他文件一样进行 zip 或 gzip 压缩,但对于适当类型的数据,JPEG 和 PNG 使用的专用压缩算法会生成小得多的文件。
  • 对于许多涉及大型多维浮点数数组的科学和工程用途,Protocol Buffer 消息在大小和速度方面效率都不是最高的。对于这些应用,FITS 等类似格式的开销更少。
  • Protocol Buffers 在科学计算中流行的非面向对象语言(如 Fortran 和 IDL)中支持不佳。
  • 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 中,将字段设置为 optional 会将其从隐式存在更改为显式存在

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

字段还可以是

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

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

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

设置基数和数据类型后,选择字段的名称。设置字段名称时需要记住一些事项

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

为字段分配名称后,分配字段编号。字段编号不能被重新用于其他用途或重复使用。如果删除一个字段,应保留其字段编号,以防止有人意外地重用该编号。

额外的数据类型支持

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

历史

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

Protocol Buffers 开源理念

Protocol Buffers 于 2008 年开源,旨在为 Google 以外的开发者提供与我们在内部获得的相同好处。我们通过定期更新语言来支持开源社区,以满足我们的内部需求。虽然我们接受外部开发者的一些精选拉取请求,但我们无法总是优先处理不符合 Google 特定需求的功能请求和 bug 修复。

开发者社区

要获取 Protocol Buffers 的未来变更通知,并与 protobuf 开发者和用户联系,请加入 Google Group

额外资源