Protobuf 是一种 IDL

Protobuf 是一种语言。更准确地说,它是一种接口描述语言(IDL)。做出这样的区分很重要,因为正如我们将在后面详细讨论的那样,在 Protobuf 中,我们并不编写像编程语言中那样的逻辑代码,而是编写数据模式,这些模式是用于序列化的契约,并在反序列化时得到满足。因此,在解释我们需要遵循的 .proto 文件编写规则以及序列化和反序列化的细节之前,我们需要首先了解什么是 IDL,以及这种语言的目标是什么。

IDL 是接口描述语言(Interface Description Language)的缩写,顾名思义,它包含三个部分。第一部分是 接口(Interface),它描述了两个或多个应用程序之间的代码,这些代码隐藏了实现的复杂性。因此,我们不需要对应用程序运行的硬件、操作系统以及编写它们的编程语言做出任何假设。这个接口的设计是硬件、操作系统和编程语言无关的。这对于 Protobuf 和其他许多序列化数据模式非常重要,因为它允许开发者一次编写代码,然后可以跨多个项目使用。

第二部分是 描述(Description),它建立在接口的基础上。我们的接口描述了两个应用程序可以预期接收什么,预期向对方发送什么。这包括描述一些类型及其属性,这些类型之间的关系,以及这些类型如何被序列化和反序列化。为了更直观地理解这一点,来看一个 Protobuf 示例。如果我们想创建一个名为 Account 的类型,它包含 ID、用户名和账户的权限,我们可以编写如下内容:

syntax = "proto3";

enum AccountRight {
  ACCOUNT_RIGHT_UNSPECIFIED = 0;
  ACCOUNT_RIGHT_READ = 1;
  ACCOUNT_RIGHT_READ_WRITE = 2;
  ACCOUNT_RIGHT_ADMIN = 3;
}

message Account {
  uint64 id = 1;
  string username = 2;
  AccountRight right = 3;
}

如果我们忽略一些在此阶段不重要的细节,可以看到我们定义了以下内容:

  • 一个枚举列出了所有可能的权限,并添加了一个额外的角色 ACCOUNT_RIGHT_UNSPECIFIED

  • 一个消息(相当于类或结构体)列出了 Account 类型应具有的三个属性。

再次强调,不深入细节,代码是可读的,并且 AccountAccountRight 之间的关系也很容易理解。

最后,第三部分是 语言(Language)。这是在说,像所有语言一样——无论是计算机语言还是其他语言——我们需要遵循规则,以便其他人或编译器能够理解我们的意图。在 Protobuf 中,我们编写代码以满足编译器(protoc),然后它会为我们做所有繁重的工作。它会读取我们的代码,并生成我们所需语言的代码,然后我们的用户代码将与生成的代码进行交互。下面是我们之前定义的 Account 类型在 Go 语言中的简化输出:

type AccountRight int32

const (
  AccountRight_ACCOUNT_RIGHT_UNSPECIFIED AccountRight = 0
  AccountRight_ACCOUNT_RIGHT_READ AccountRight = 1
  AccountRight_ACCOUNT_RIGHT_READ_WRITE AccountRight = 2
  AccountRight_ACCOUNT_RIGHT_ADMIN AccountRight = 3
)

type Account struct {
  Id      uint64 `protobuf:"varint,1,…"`
  Username string `protobuf:"bytes,2,…"`
  Right   AccountRight `protobuf:"varint,3,…"`
}

在这段代码中,有几个重要的点需要注意。我们将代码拆解如下:

type AccountRight int32

const (
  AccountRight_ACCOUNT_RIGHT_UNSPECIFIED AccountRight = 0
  AccountRight_ACCOUNT_RIGHT_READ AccountRight = 1
  AccountRight_ACCOUNT_RIGHT_READ_WRITE AccountRight = 2
  AccountRight_ACCOUNT_RIGHT_ADMIN AccountRight = 3
)

我们的 AccountRight 枚举被定义为类型为 int32 的常量。每个枚举值的名称以枚举名称为前缀,每个常量都有一个与 Protobuf 代码中等号后面设置的值相对应的值。这些值被称为 字段标签(field tags),我们将在本章后面介绍。

接下来,看看下面的代码:

type Account struct {
  Id      uint64 `protobuf:"varint,1,…"`
  Username string `protobuf:"bytes,2,…"`
  Right   AccountRight `protobuf:"varint,3,…"`
}

这里,我们的 Account 消息被转译为一个结构体,包含 IdUsernameRight 导出的字段。每个字段的类型从 Protobuf 类型转换为 Go 类型。在我们这里的示例中,Go 类型和 Protobuf 类型的名称是相同的,但需要注意的是,在某些情况下,类型会有所不同。例如,Protobuf 中的 double 类型会转换为 Go 中的 float64 类型。最后,我们看到了字段标签,它们在字段后面以元数据的形式引用。它们的具体含义将在本章稍后解释。

总结一下,IDL 是位于不同应用程序之间的代码,描述了对象及其关系,并遵循某些定义的规则。在 Protobuf 中,这个 IDL 会被读取,并用于生成另一种语言的代码。之后,用户代码将使用生成的代码来序列化和反序列化数据。