Google Protocol Buffers简介

什么是 protocol buffers ?

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

 

它是怎样工做的?

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

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
  repeated PhoneNumber phone = 4;
}

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

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

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

而后,你能够经过这样的代码来把你的message读进来:c++

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

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

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

 

为何不用XML等其余技术?

Protocol buffers相对XML在序列化数据的时候有不少优点。protocol buffers :数组

  • 更简单
  • 比XML小3到10倍
  • 比XML快20到100倍
  • 更少歧义
  • 能够生成方便编程的数据访问类

例如,倘若你要给person建模,它有nameemail属性。在XML里,你须要:数据结构

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

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

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

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

一样,操做protocol buffer 更容易:

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

然而若是使用XML你须要这样作:

cout << "Name: "
       << person.getElementsByTagName("name")->item(0)->innerText()
       << endl;
cout << "E-mail: "
       << person.getElementsByTagName("email")->item(0)->innerText()
       << endl;

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

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

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

入门教程c++版

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

  • .proto文件里定义信息格式
  • 使用protocol buffer编译器
  • 使用c++ protocol buffer API来读写信息

为何使用protocol buffers?

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

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

  • 内存中原始的字节数据结构能够存储为2进制形式。这种方法很脆弱,由于读取代码必须用一样的内存布局编译,还要考虑使用相同的内存大小端等等。当文件积累了不少数据以后,拷贝处处都是,扩展结构就很困难了。
  • 你能够发明一个点对点的方式来把数据编码为一个简单的字符串---例如编码4个整数为"12:3:-23:67".这是一个简单且灵活的方法,尽管它须要你编写一次性的读写代码,读取须要一些运行时间。这种方法适用于编码十分简单的数据。
  • 序列化数据到XML文件。若是你须要和其余程序共享数据,那么这将是个好方法。然而,XML占内存已经臭名昭著了,解析编码它会形成程序性能大幅降低。在XML DOM tree里巡弋也远比在类里查找属性复杂的多。

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


 

定义你的protocol Format

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

package tutorial;
message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
  repeated PhoneNumber phone = 4;
}
message AddressBook {
  repeated Person person = 1;
}

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

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

    • required : 故名思议就是必须提供值的属性,当你把属性设置为required的时候要当心,由于若是之后想修改成其余类型,老的读取类就不兼容新的数据了。
    • optional: 就是能够不提供值的属性,若是没有提供值,会设置为默认值。默认值能够本身提供,若是没有本身提供默认值,会设置为系统默认值:numeric类型会置为0,字符串置为空串,bool置为false;对于内嵌类型,默认值永远是空实例。
    • repeated:就是可能重复任意次(包含0次).重复值的顺序会在二进制文件保存下来,能够把重复的属性看作动态大小的数组。注意,因为历史缘由,repeated数值属性不能有效的被编码成二进制,新的代码可使用[packed=true]来得到更好的编码效率 

 

例如: repeated int32 samples = 4 [packed=true];

编译你的protocol buffers文件

protoc -I=\$SRC_DIR --cpp_out=\$DST_DIR \$SRC_DIR/addressbook.proto
  • addressbook.pb.h
  • addressbook.pb.cc

Protocol Buffer API

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

// name
  inline bool has_name() const;
  inline void clear_name();
  inline const ::std::string& name() const;
  inline void set_name(const ::std::string& value);
  inline void set_name(const char* value);
  inline ::std::string* mutable_name();
  // id
  inline bool has_id() const;
  inline void clear_id();
  inline int32_t id() const;
  inline void set_id(int32_t value);
  // email
  inline bool has_email() const;
  inline void clear_email();
  inline const ::std::string& email() const;
  inline void set_email(const ::std::string& value);
  inline void set_email(const char* value);
  inline ::std::string* mutable_email();
  // phone
  inline int phone_size() const;
  inline void clear_phone();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
  inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
  inline ::tutorial::Person_PhoneNumber* add_phone();

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

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

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

  • 能够查看_size(这我的有多少个电话号码)
  • 能够经过index访问一个特定的值
  • 能够添加一个新值(经过add_方法)

更多关于编译器生成函数的信息请参看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,包括:

  • bool IsInitialized() const;: checks if all the required fields have been set.
  • string DebugString() const;: returns a human-readable representation of the message, particularly useful for debugging.
  • void CopyFrom(const Person& from);: overwrites the message with the given message's values.
  • void Clear();: clears all the elements back to the empty state.

 

解析和序列化

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

  • bool SerializeToString(string* output) const;: 序列化一个message而且把字节文件存储到string里,这里使用string仅仅是为了把它当作一个方便的容器.
  • bool ParseFromString(const string& data);: 从指定的string里解析message
  • bool SerializeToOstream(ostream* output) const;: 把message写到指定的c++`ostream`里。
  • bool ParseFromIstream(istream* input);: 从指定的c++istream读取message

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


 

写一个Message

如今,让咱们试着使用编译器为咱们生成的类。咱们让咱们的地址簿程序作的第一件事情是把一我的的我的信息写到地址簿文件里。咱们须要生成一个该类的实例而后把它写入到输出流里。

这里有一个实例程序:

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, '\n');
  cout << "Enter name: ";
  getline(cin, *person->mutable_name());
  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }
  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }
    tutorial::Person::PhoneNumber* phone_number = person->add_phone();
    phone_number->set_number(number);
    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}
// Main function:  Reads the entire address book from a file,
//   adds one person based on user input, then writes it back out to the same
//   file.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook address_book;
  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  // Add an address.
  PromptForAddress(address_book.add_person());
  {
    // Write the new address book back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }
  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

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

注意代码中的ShutdownProtobufLibrary(),它会清楚全部protocol buffer libarary分配的全局对象。一般这是不须要的,由于这个进程老是会退出,系统会接管剩下的内存。可是,若是你使用了一个内存泄露检查工具,好比valgrand之类的,这类工具会要求你把全部分配的内存释放掉,或者你在写一个库文件,这个库文件会被同一个进程加载和卸载屡次,这两种状况你就须要清理全部东西。


 

读取一个Message

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

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
  for (int i = 0; i < address_book.person_size(); i++) {
    const tutorial::Person& person = address_book.person(i);
    cout << "Person ID: " << person.id() << endl;
    cout << "  Name: " << person.name() << endl;
    if (person.has_email()) {
      cout << "  E-mail address: " << person.email() << endl;
    }
    for (int j = 0; j < person.phone_size(); j++) {
      const tutorial::Person::PhoneNumber& phone_number = person.phone(j);
      switch (phone_number.type()) {
        case tutorial::Person::MOBILE:
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:
          cout << "  Work phone #: ";
          break;
      }
      cout << phone_number.number() << endl;
    }
  }
}
// Main function:  Reads the entire address book from a file and prints all
//   the information inside.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook address_book;
  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  ListPeople(address_book);
  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

扩展一个Protocol Buffer

当一段时间以后你须要在你发布使用你的protocol buffer后改进你的protocol buffer定义。若是你但愿你的新buffer可以向前兼容,而你的老buffer能向后兼容,那么你就须要遵照下面这几个规则:

  • 不要修改tag数字
  • 不要增删任何required属性
  • 能够删除repeated或者optional属性
  • 能够添加 repeated或者optional属性,可是必须使用新tag number

转自:https://zybuluo.com/anboqing/note/263467

参考:http://www.cnblogs.com/shitouer/archive/2013/04/08/google-protocol-buffers-overview.html

相关文章
相关标签/搜索