Protocol Buffer 基础知识:Dart

Dart 程序员使用 protocol buffers 的基础入门。

本教程为 Dart 程序员提供了使用 protocol buffers 的基础入门,使用 protocol buffers 语言的 proto3 版本。通过创建一个简单的示例应用程序,它将向您展示如何

  • 在 .proto 文件中定义消息格式。
  • 使用 protocol buffer 编译器。
  • 使用 Dart protocol buffer API 编写和读取消息。

这不是在 Dart 中使用 protocol buffers 的全面指南。有关更详细的参考信息,请参阅《Protocol Buffer 语言指南》、《Dart 语言之旅》、《Dart API 参考》、《Dart 代码生成指南》和《编码参考》。

问题域

我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以将人员的联系方式读取和写入文件。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话号码。

如何序列化和检索这样的结构化数据?有几种方法可以解决这个问题

  • 您可以发明一种临时方法将数据项编码为单个字符串 – 例如将 4 个整数编码为 “12:3:-23:67”。这是一种简单而灵活的方法,尽管它确实需要编写一次性的编码和解析代码,并且解析会带来少量运行时成本。这最适合编码非常简单的数据。
  • 将数据序列化为 XML。这种方法可能非常有吸引力,因为 XML 是(某种程度上)人类可读的,并且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。但是,XML 以空间密集而闻名,并且编码/解码它可能会对应用程序造成巨大的性能损失。此外,导航 XML DOM 树比通常在类中导航简单字段要复杂得多。

Protocol buffers 是解决这个问题的灵活、高效、自动化的解决方案。使用 protocol buffers,您可以编写要存储的数据结构的 .proto 描述。由此,protocol buffer 编译器会创建一个类,该类以高效的二进制格式实现 protocol buffer 数据的自动编码和解析。生成的类为构成 protocol buffer 的字段提供 getter 和 setter,并负责以单元形式读取和写入 protocol buffer 的详细信息。重要的是,protocol buffer 格式支持随着时间推移扩展格式的想法,以便代码仍然可以读取以旧格式编码的数据。

在哪里找到示例代码

我们的示例是一组命令行应用程序,用于管理使用 protocol buffers 编码的地址簿数据文件。命令 `dart add_person.dart` 将新条目添加到数据文件。命令 `dart list_people.dart` 解析数据文件并将数据打印到控制台。

您可以在 GitHub 存储库的 examples 目录中找到完整的示例。

定义您的协议格式

要创建您的地址簿应用程序,您需要从 .proto 文件开始。 .proto 文件中的定义很简单:您为您要序列化的每个数据结构添加一个消息,然后为消息中的每个字段指定名称和类型。在我们的示例中,定义消息的 .proto 文件是 addressbook.proto。

.proto 文件以包声明开头,这有助于防止不同项目之间的命名冲突。

syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

接下来,您有您的消息定义。消息只是一个包含一组类型化字段的聚合。许多标准的简单数据类型可用作字段类型,包括 bool、int32、float、double 和 string。您还可以通过使用其他消息类型作为字段类型,为您的消息添加更多结构。

message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    PHONE_TYPE_UNSPECIFIED = 0;
    PHONE_TYPE_MOBILE = 1;
    PHONE_TYPE_HOME = 2;
    PHONE_TYPE_WORK = 3;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

在上面的示例中,Person 消息包含 PhoneNumber 消息,而 AddressBook 消息包含 Person 消息。您甚至可以在其他消息内部定义嵌套的消息类型 – 正如您所看到的,PhoneNumber 类型是在 Person 内部定义的。如果您希望您的字段之一具有预定义值列表中的一个,您还可以定义枚举类型 – 在这里您想指定电话号码可以是 PHONE_TYPE_MOBILE、PHONE_TYPE_HOME 或 PHONE_TYPE_WORK 之一。

每个元素上的 “= 1”、“= 2” 标记标识该字段在二进制编码中使用的唯一“标签”。标签号 1-15 比更高的数字少用一个字节进行编码,因此作为优化,您可以决定将这些标签用于常用或重复的元素,而将标签 16 及更高的标签用于不太常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段特别适合这种优化。

如果未设置字段值,则使用默认值:数字类型为零,字符串为空字符串,布尔值为 false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其中没有设置任何字段。调用访问器以获取尚未显式设置的字段的值始终返回该字段的默认值。

如果字段是 repeated,则该字段可以重复任意次数(包括零次)。重复值的顺序将保留在 protocol buffer 中。可以将重复字段视为动态大小的数组。

您可以在《Protocol Buffer 语言指南》中找到编写 .proto 文件的完整指南 – 包括所有可能的字段类型。不过,不要寻找类似于类继承的工具 – protocol buffers 不这样做。

编译您的 Protocol Buffers

现在您有了 .proto,接下来您需要做的是生成读取和写入 AddressBook(以及 Person 和 PhoneNumber)消息所需的类。为此,您需要在您的 .proto 上运行 protocol buffer 编译器 protoc

  1. 如果您尚未安装编译器,请下载软件包并按照 README 中的说明进行操作。

  2. 按照其 README 中的描述安装 Dart Protocol Buffer 插件。可执行文件 bin/protoc-gen-dart 必须在您的 PATH 中,protocol buffer protoc 才能找到它。

  3. 现在运行编译器,指定源目录(您的应用程序源代码所在的位置 – 如果您不提供值,则使用当前目录)、目标目录(您希望生成的代码去的位置;通常与 $SRC_DIR 相同)以及您的 .proto 的路径。在这种情况下,您将调用

    protoc -I=$SRC_DIR --dart_out=$DST_DIR $SRC_DIR/addressbook.proto
    

    因为您想要 Dart 代码,所以您使用 --dart_out 选项 – 为其他受支持的语言提供了类似的选项。

这会在您指定的目标目录中生成 addressbook.pb.dart 。

Protocol Buffer API

生成 addressbook.pb.dart 为您提供以下有用的类型

  • 一个 AddressBook 类,带有 List<Person> get people getter。
  • 一个 Person 类,带有 name、id、email 和 phones 的访问器方法。
  • 一个 Person_PhoneNumber 类,带有 number 和 type 的访问器方法。
  • 一个 Person_PhoneType 类,带有 Person.PhoneType 枚举中每个值的静态字段。

您可以在《Dart 代码生成指南》中阅读有关生成的具体内容的更多详细信息。

编写消息

现在让我们尝试使用您的 protocol buffer 类。您希望您的地址簿应用程序能够做的第一件事是将个人详细信息写入您的地址簿文件。为此,您需要创建并填充您的 protocol buffer 类的实例,然后将它们写入输出流。

这是一个程序,它从文件中读取 AddressBook,根据用户输入向其中添加一个新的 Person,并将新的 AddressBook 写回文件。直接调用或引用协议编译器生成的代码的部分已突出显示。

import 'dart:io';

import 'dart_tutorial/addressbook.pb.dart';

// This function fills in a Person message based on user input.
Person promptForAddress() {
  Person person = Person();

  print('Enter person ID: ');
  String input = stdin.readLineSync();
  person.id = int.parse(input);

  print('Enter name');
  person.name = stdin.readLineSync();

  print('Enter email address (blank for none) : ');
  String email = stdin.readLineSync();
  if (email.isNotEmpty) {
    person.email = email;
  }

  while (true) {
    print('Enter a phone number (or leave blank to finish): ');
    String number = stdin.readLineSync();
    if (number.isEmpty) break;

    Person_PhoneNumber phoneNumber = Person_PhoneNumber();

    phoneNumber.number = number;
    print('Is this a mobile, home, or work phone? ');

    String type = stdin.readLineSync();
    switch (type) {
      case 'mobile':
        phoneNumber.type = Person_PhoneType.PHONE_TYPE_MOBILE;
        break;
      case 'home':
        phoneNumber.type = Person_PhoneType.PHONE_TYPE_HOME;
        break;
      case 'work':
        phoneNumber.type = Person_PhoneType.PHONE_TYPE_WORK;
        break;
      default:
        print('Unknown phone type.  Using default.');
    }
    person.phones.add(phoneNumber);
  }

  return person;
}

// Reads the entire address book from a file, adds one person based
// on user input, then writes it back out to the same file.
main(List arguments) {
  if (arguments.length != 1) {
    print('Usage: add_person ADDRESS_BOOK_FILE');
    exit(-1);
  }

  File file = File(arguments.first);
  AddressBook addressBook;
  if (!file.existsSync()) {
    print('File not found. Creating new file.');
    addressBook = AddressBook();
  } else {
    addressBook = AddressBook.fromBuffer(file.readAsBytesSync());
  }
  addressBook.people.add(promptForAddress());
  file.writeAsBytes(addressBook.writeToBuffer());
}

读取消息

当然,如果您无法从中获取任何信息,地址簿将毫无用处!此示例读取由上述示例创建的文件并打印其中的所有信息。

import 'dart:io';

import 'dart_tutorial/addressbook.pb.dart';
import 'dart_tutorial/addressbook.pbenum.dart';

// Iterates though all people in the AddressBook and prints info about them.
void printAddressBook(AddressBook addressBook) {
  for (Person person in addressBook.people) {
    print('Person ID: ${ person.id}');
    print('  Name: ${ person.name}');
    if (person.hasEmail()) {
      print('  E-mail address:${ person.email}');
    }

    for (Person_PhoneNumber phoneNumber in person.phones) {
      switch (phoneNumber.type) {
        case Person_PhoneType.PHONE_TYPE_MOBILE:
          print('   Mobile phone #: ');
          break;
        case Person_PhoneType.PHONE_TYPE_HOME:
          print('   Home phone #: ');
          break;
        case Person_PhoneType.PHONE_TYPE_WORK:
          print('   Work phone #: ');
          break;
        default:
          print('   Unknown phone #: ');
          break;
      }
      print(phoneNumber.number);
    }
  }
}

// Reads the entire address book from a file and prints all
// the information inside.
main(List arguments) {
  if (arguments.length != 1) {
    print('Usage: list_person ADDRESS_BOOK_FILE');
    exit(-1);
  }

  // Read the existing address book.
  File file = new File(arguments.first);
 AddressBook addressBook = new AddressBook.fromBuffer(file.readAsBytesSync());
  printAddressBook(addressBook);
}

扩展 Protocol Buffer

迟早,在您发布使用 protocol buffer 的代码后,您无疑会想要“改进”protocol buffer 的定义。如果您希望您的新缓冲区向后兼容,并且您的旧缓冲区向前兼容 – 您几乎肯定希望这样做 – 那么您需要遵循一些规则。在新版本的 protocol buffer 中

  • 不得更改任何现有字段的标签号。
  • 可以删除字段。
  • 可以添加新字段,但您必须使用新的标签号(即从未在此 protocol buffer 中使用过的标签号,甚至不是已删除字段使用的标签号)。

(这些规则有一些例外,但很少使用。)

如果您遵循这些规则,旧代码将很高兴地读取新消息,并简单地忽略任何新字段。对于旧代码,已删除的单数字段将仅具有其默认值,而已删除的重复字段将为空。新代码也将透明地读取旧消息。

但是,请记住,新字段不会出现在旧消息中,因此您需要对默认值做一些合理的事情。使用特定于类型的默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为 false。对于数字类型,默认值为零。