[关闭]
@anboqing 2016-01-10T01:44:31.000000Z 字数 11311 阅读 3965

Google Protocol Buffers 简介

caffe .proto



什么是 protocol buffers ?

Protocol buffers 是一种灵活、高效的序列化结构数据的自动机制--想想XML,但是它更小,更快,更简单。你只需要把你需要怎样结构化你的数据定义一次,你就能使用特殊生成的代码来方便的用多种语言从一系列数据流中读写你的结构化数据。你甚至不需要中断你用"老"结构编译好的已经部署的程序来更新你的数据结构。


它是怎样工作的?

你在一个名为.proto的文件里用protocol buffer message 定义你需要序列化数据的结构。每个protocol buffer message 是一个小的信息逻辑记录,包含了一系列的name-value对。这里有一个简单的.proto例子,它定义了一个person的信息。

  1. message Person {
  2. required string name = 1;
  3. required int32 id = 2;
  4. optional string email = 3;
  5. enum PhoneType {
  6. MOBILE = 0;
  7. HOME = 1;
  8. WORK = 2;
  9. }
  10. message PhoneNumber {
  11. required string number = 1;
  12. optional PhoneType type = 2 [default = HOME];
  13. }
  14. repeated PhoneNumber phone = 4;
  15. }

就像你看到的,这条信息结构很简单---每条message type 都有一个或多个独特的属性,每个属性都有一个name和一个value类型,value类型可以是numbers ( 整数或浮点数),booleans,strings,raw bytes或者其他protocol buffer message types,允许你以嵌套结构组织你的结构。你可以指定optional,required、和repeated属性。你可以从 Protocol Buffer Language Guide找到更多的关于如何写.proto文件的信息。

一旦你定义了你的信息,你就可以运行protocol buffer 编译器来编译你的.proto文件来生成特定语言的数据访问类。这些类提供了简单的对属性的访问函数(例如 name()set_name() )和用来序列化整个结构到raw bytes和从raw bytes 解析出结构的函数。例如,假如你使用的是c++语言,用编译器编译上面那个person.proto文件会生成一个Person类。你可以在你的应用里用这个类来操纵Person类的对象。比如,你可能会写一些这样的代码:

  1. Person person;
  2. person.set_name("John Doe");
  3. person.set_id(1234);
  4. person.set_email("jdoe@example.com");
  5. fstream output("myfile", ios::out | ios::binary);
  6. person.SerializeToOstream(&output);

然后,你可以通过这样的代码来把你的message读进来:

  1. fstream input("myfile", ios::in | ios::binary);
  2. Person person;
  3. person.ParseFromIstream(&input);
  4. cout << "Name: " << person.name() << endl;
  5. cout << "E-mail: " << person.email() << endl;

你可以给你的message添加新的属性而不打破向后兼容性(backwards-compatibility);旧的二进制文件仅仅在编译的时候忽略那些新的属性。这样一来,如果你有一个通信协议使用了protocol buffers当做它传输的数据格式,你可以扩展你的通信协议而不用担心破坏现有的代码。

你可以在API Reference section找到完整的文档,并且你可以在Protocol buffer encoding找出关于protocol buffer 编码的更多信息.


为什么不用XML等其他技术?

Protocol buffers相对XML在序列化数据的时候有很多优势。protocol buffers :

例如,假若你要给person建模,它有nameemail属性。在XML里,你需要:

  1. <person>
  2. <name>John Doe</name>
  3. <email>jdoe@example.com</email>
  4. </person>

用protocol buffer message(在protocol buffer 的text format)是这样的

  1. # Textual representation of a protocol buffer.
  2. # This is *not* the binary format used on the wire.
  3. person {
  4. name: "John Doe"
  5. email: "jdoe@example.com"
  6. }

当上面这段代码被编译成binary format(上面那段text format只是为了方便人类读写编辑的)的时候,它可能只占28字节长,仅仅需要100~200纳秒就能编译。那个XML版本即使移除所有空白也至少需要69字节,并且需要5000~10000纳秒来编译。

同样,操作protocol buffer 更容易:

  1. cout << "Name: " << person.name() << endl;
  2. cout << "E-mail:" << person.email() << endl;

然而如果使用XML你需要这样做:

  1. cout << "Name: "
  2. << person.getElementsByTagName("name")->item(0)->innerText()
  3. << endl;
  4. cout << "E-mail: "
  5. << person.getElementsByTagName("email")->item(0)->innerText()
  6. << endl;

嗯~听起来能解决我的问题!我该怎样开始呢?

下载地址--这个包包含了完整的c++,python和java语言的编译器源代码,和I/O和测试的类。安装请参阅README。

一旦你安装好了,就可以跟着入门教程来学习了。


入门教程c++版

这个教程会带你走一遍使用protocol buffer的流程,创建一个简单的实例程序,学会基本的使用方法:


为什么使用protocol buffers?

在这个教程里我们要创建一个简单的“地址簿”程序来在文件里读写人们的联系人信息。每个人都有一个name,id,email address和一个联系电话。

你怎样序列化和读取这样一个结构数据呢?这里有三种方法:

protocol buffers 灵活高效,可以解决上述问题。你只需要编写一个.proto文件来描述你要使用的数据结构。protocol buffer 编译器可以把.proto文件编译成一个类似于ORM(object relation mapping)实现类的数据访问类,这个类可以把高效的用二进制文件方式存储的数据读写出来。更多的是,它提供了一种向后兼容的扩展机制,使你可以不用担心兼容性问题来扩展你的数据格式。


定义你的protocol Format

为了创建地址簿程序,你需要首先定义一个.proto文件。定义.proto文件十分简单: 你添加一个 message 给你想序列化的每个数据结构 ,然后指定一个 name和一个typemessage的每个属性。下面是一个.proto文件,定义了地址簿数据结构,addressbook.proto:

  1. package tutorial;
  2. message Person {
  3. required string name = 1;
  4. required int32 id = 2;
  5. optional string email = 3;
  6. enum PhoneType {
  7. MOBILE = 0;
  8. HOME = 1;
  9. WORK = 2;
  10. }
  11. message PhoneNumber {
  12. required string number = 1;
  13. optional PhoneType type = 2 [default = HOME];
  14. }
  15. repeated PhoneNumber phone = 4;
  16. }
  17. message AddressBook {
  18. repeated Person person = 1;
  19. }

protobuffer支持的内建数据类型包括bool,int32,float,double,string.
注意:message可以嵌套,比如 PhoneNumber 就定义在Person里。
“=1”,“=2”标记了每个元素的唯一“tag”,这是用在二进制编码里的。使用1-15可以在1个字节里表示这些tag,节省空间,一般把常用的需要大量重复的元素使用1-15来编码,把16以上的tag留给不常用的元素。

每个属性必须标记为下列修饰符之一:


编译你的protocol buffers文件

protoc -I=\$SRC_DIR --cpp_out=\$DST_DIR \$SRC_DIR/addressbook.proto


Protocol Buffer API

我们现在来看一些生成的code是什么样的,编译器为我们生成了什么类和函数呢?
如果我们打开tutorial.pb.h,我们会看到编译器给我们在.proto文件里定义的每一个message都生成了一个class,我们再看Person类,会发现编译器给message的每个属性都生成了getters和setters,例如,对于name,id,email,和phone属性,我们可以找到这些函数:

  1. // name
  2. inline bool has_name() const;
  3. inline void clear_name();
  4. inline const ::std::string& name() const;
  5. inline void set_name(const ::std::string& value);
  6. inline void set_name(const char* value);
  7. inline ::std::string* mutable_name();
  8. // id
  9. inline bool has_id() const;
  10. inline void clear_id();
  11. inline int32_t id() const;
  12. inline void set_id(int32_t value);
  13. // email
  14. inline bool has_email() const;
  15. inline void clear_email();
  16. inline const ::std::string& email() const;
  17. inline void set_email(const ::std::string& value);
  18. inline void set_email(const char* value);
  19. inline ::std::string* mutable_email();
  20. // phone
  21. inline int phone_size() const;
  22. inline void clear_phone();
  23. inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
  24. inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
  25. inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
  26. inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
  27. inline ::tutorial::Person_PhoneNumber* add_phone();

正如你所能看到的那样,getters和小写属性名一样,setters以set_开头。还有has_开头的判断是否设置了值的函数。还有clear_开头的函数用于清空设置的值。

不同类型的属性方法不尽相同,例如 id只有基本的getter,setter方法,而name,email等字符串类型的属性多了一个mutable_开头的getter,和一个多出来的setter。即使还没有设置email仍然可以调用mutable_email。它可以自动初始化为一个空字符串。

repeated属性同样有些特别的方法,例如phone属性:

更多关于编译器生成函数的信息请参看C++ generated code reference


枚举和嵌套类

生成的代码包含了一个PhoneType枚举对应你的.proto文件里的enum.你可以通过Person::PhoneType来使用这个枚举,和它的值Person::MOBILE,Person::HOME,Person::WORK(具体实现很复杂,但我们不需要了解它)

编译器同样生成了一个嵌套类Person::PhoneNumber。如果查看代码,会发现实际的类是叫做Person_PhoneNumber,但是使用了一个typedef来重命名了它,唯一的区别是当你想在另一个文件里前向声明这个类的时候,必须使用Person_PhoneNumber来前向声明它。


标准 Message方法

每个message类还包含了一些其他方法来使你能检查或者操作整个message,包括:


解析和序列化

最终,每个protocol buffer class使用读写方法来解析和序列化message到二进制文件里,这些方法包括:

查看Message API获取更详细内容.


写一个Message

现在,让我们试着使用编译器为我们生成的类。我们让我们的地址簿程序做的第一件事情是把一个人的个人信息写到地址簿文件里。我们需要生成一个该类的实例然后把它写入到输出流里。

这里有一个实例程序:

  1. #include <iostream>
  2. #include <fstream>
  3. #include <string>
  4. #include "addressbook.pb.h"
  5. using namespace std;
  6. // This function fills in a Person message based on user input.
  7. void PromptForAddress(tutorial::Person* person) {
  8. cout << "Enter person ID number: ";
  9. int id;
  10. cin >> id;
  11. person->set_id(id);
  12. cin.ignore(256, '\n');
  13. cout << "Enter name: ";
  14. getline(cin, *person->mutable_name());
  15. cout << "Enter email address (blank for none): ";
  16. string email;
  17. getline(cin, email);
  18. if (!email.empty()) {
  19. person->set_email(email);
  20. }
  21. while (true) {
  22. cout << "Enter a phone number (or leave blank to finish): ";
  23. string number;
  24. getline(cin, number);
  25. if (number.empty()) {
  26. break;
  27. }
  28. tutorial::Person::PhoneNumber* phone_number = person->add_phone();
  29. phone_number->set_number(number);
  30. cout << "Is this a mobile, home, or work phone? ";
  31. string type;
  32. getline(cin, type);
  33. if (type == "mobile") {
  34. phone_number->set_type(tutorial::Person::MOBILE);
  35. } else if (type == "home") {
  36. phone_number->set_type(tutorial::Person::HOME);
  37. } else if (type == "work") {
  38. phone_number->set_type(tutorial::Person::WORK);
  39. } else {
  40. cout << "Unknown phone type. Using default." << endl;
  41. }
  42. }
  43. }
  44. // Main function: Reads the entire address book from a file,
  45. // adds one person based on user input, then writes it back out to the same
  46. // file.
  47. int main(int argc, char* argv[]) {
  48. // Verify that the version of the library that we linked against is
  49. // compatible with the version of the headers we compiled against.
  50. GOOGLE_PROTOBUF_VERIFY_VERSION;
  51. if (argc != 2) {
  52. cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
  53. return -1;
  54. }
  55. tutorial::AddressBook address_book;
  56. {
  57. // Read the existing address book.
  58. fstream input(argv[1], ios::in | ios::binary);
  59. if (!input) {
  60. cout << argv[1] << ": File not found. Creating a new file." << endl;
  61. } else if (!address_book.ParseFromIstream(&input)) {
  62. cerr << "Failed to parse address book." << endl;
  63. return -1;
  64. }
  65. }
  66. // Add an address.
  67. PromptForAddress(address_book.add_person());
  68. {
  69. // Write the new address book back to disk.
  70. fstream output(argv[1], ios::out | ios::trunc | ios::binary);
  71. if (!address_book.SerializeToOstream(&output)) {
  72. cerr << "Failed to write address book." << endl;
  73. return -1;
  74. }
  75. }
  76. // Optional: Delete all global objects allocated by libprotobuf.
  77. google::protobuf::ShutdownProtobufLibrary();
  78. return 0;
  79. }

注意代码中的GOOGLE_PROTOBUF_VERIFY_VERSION宏,在使用c++ Protocol Buffer 之前执行这个宏是一个好的习惯(尽管不是强制要求的)。它会验证你是否链接了正确的库,防止你链接版本不匹配的库。

注意代码中的ShutdownProtobufLibrary(),它会清楚所有protocol buffer libarary分配的全局对象。通常这是不需要的,因为这个进程总是会退出,系统会接管剩下的内存。但是,如果你使用了一个内存泄露检查工具,比如valgrand之类的,这类工具会要求你把所有分配的内存释放掉,或者你在写一个库文件,这个库文件会被同一个进程加载和卸载多次,这两种情况你就需要清理所有东西。


读取一个Message

这是一个从二进制文件读取地址簿的例子:

  1. #include <iostream>
  2. #include <fstream>
  3. #include <string>
  4. #include "addressbook.pb.h"
  5. using namespace std;
  6. // Iterates though all people in the AddressBook and prints info about them.
  7. void ListPeople(const tutorial::AddressBook& address_book) {
  8. for (int i = 0; i < address_book.person_size(); i++) {
  9. const tutorial::Person& person = address_book.person(i);
  10. cout << "Person ID: " << person.id() << endl;
  11. cout << " Name: " << person.name() << endl;
  12. if (person.has_email()) {
  13. cout << " E-mail address: " << person.email() << endl;
  14. }
  15. for (int j = 0; j < person.phone_size(); j++) {
  16. const tutorial::Person::PhoneNumber& phone_number = person.phone(j);
  17. switch (phone_number.type()) {
  18. case tutorial::Person::MOBILE:
  19. cout << " Mobile phone #: ";
  20. break;
  21. case tutorial::Person::HOME:
  22. cout << " Home phone #: ";
  23. break;
  24. case tutorial::Person::WORK:
  25. cout << " Work phone #: ";
  26. break;
  27. }
  28. cout << phone_number.number() << endl;
  29. }
  30. }
  31. }
  32. // Main function: Reads the entire address book from a file and prints all
  33. // the information inside.
  34. int main(int argc, char* argv[]) {
  35. // Verify that the version of the library that we linked against is
  36. // compatible with the version of the headers we compiled against.
  37. GOOGLE_PROTOBUF_VERIFY_VERSION;
  38. if (argc != 2) {
  39. cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
  40. return -1;
  41. }
  42. tutorial::AddressBook address_book;
  43. {
  44. // Read the existing address book.
  45. fstream input(argv[1], ios::in | ios::binary);
  46. if (!address_book.ParseFromIstream(&input)) {
  47. cerr << "Failed to parse address book." << endl;
  48. return -1;
  49. }
  50. }
  51. ListPeople(address_book);
  52. // Optional: Delete all global objects allocated by libprotobuf.
  53. google::protobuf::ShutdownProtobufLibrary();
  54. return 0;
  55. }

扩展一个Protocol Buffer

当一段时间之后你需要在你发布使用你的protocol buffer后改进你的protocol buffer定义。如果你希望你的新buffer能够向前兼容,而你的老buffer能向后兼容,那么你就需要遵守下面这几个规则:


添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注