Protocol Buffer Basics(转)

ProtocolBuffer是用于结构化数据串行化的灵活、高效、自动的方法。有点象XML,不过它更小、更快、也更简单。这是 Google Protocol Buffer Basics的C++中文翻译,由CSDN的learnhard提供,感谢learnhard!

译文来自 learnhard的CSDN博客:http://blog.csdn.net/learnhard/
原文链接:code.google.com/intl/zh-CN/apis/protocolbuffers/docs/cpptutorial.html

本教程提供了面向C++程序员的protocol buffers的基本介绍。通过创建一个简单的示例程序,它教你如何:

  1. 定义.proto文件的消息格式。
  2. 使用protocol buffer的编译器。
  3. 使用protoco buffer的C++ API来读写消息。

本文并不是关于protocol buffers的C++使用的全面教程。要查看更详细的参考资料,请阅如下文章:Protocol Buffer Language GuideC++ API ReferenceC++ Generated Code Guide,以及 Encoding Reference
示例代码包含在源代码包中的“examples”目录下。点击此 处下载

要创建你的地址薄应用程序,你需要从编写一个.proto文件开 始。.proto文件的定义是比较简单的:为每一个你需要序列化的数据 结构添加一个消息(message),然后为消息(message)中的每一个字段(field)指定一个名字和一个类型。下面就是一个定义你的多个消息 (messages)的文件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;
}



正如你所看到的一样,该语法类似于C++或Java的语法。让我们依次来看看文件的每一部分的作用。
.proto文件以一个package声明开始。这个声明是为了防止不同项目之间的命名冲突。对应到C++中去,你用这个.proto

再往下看,就是若干消息(message)定义了。一个消息就是某些类型的字段的集合。许多标准的、简单的数据类型都可以用作字段类 型,包括bool,
int32,float,double,以及string。你也可以使用其他的消息(message)类型来作为你的字段类型——在上面的例子中,消息

在每一项后面的、类似于“= 1”,“=
2”的标志指出了该字段在二进制编码中使用的唯一“标识(tag)”。标识号1~15编码所需的字节数比更大的标识号使用的字节数要少1个,所以,如果你

想寻求优化,可以为经常使用或者重复的项采用1~15的标识(tag),其他经常使用的optional项采用≥16的标识(tag)。在重复的字段中,

每一个字段都必须用以下之一的修饰符来修饰:

  • required:必 须提供字段值,否则对应的消息就会被认为是“未初始化的”。如果libprotobuf是以debug模式编译的,序列化一个未初始 化的消息(message)将会导致一个断言错误。在优化过的编译情况下(译者注:例如release),该检查会被跳过,消息会被写入。然而,解析一个 未初始化的消息仍然会失败(解析函数会返回false)。除此之外,一个required的字段与一个optional的字段就没有区别了。
  • optional:字 段值指定与否都可以。如果没有指定一个optional的字段值,它就会使用默认值。对简单类型来说,你可以指定你自己的默认值,就 像我们在上面的例子中对phone number的type字段所做的一样。如果你不指定默认值,就会使用系统默认值:数据类型的默认值为0,string的默认值为空字符串,bool的默 认值为false。对嵌入式消息(message)来说,其默认值总是消息的“默认实例”或“原型”,即:没有任何一个字段是指定了值的。调用访问类来取 一个未显式指定其值的optional(或者required)的字段的值,总是会返回字段的默认值。
  • repeated:字 段会重复N次(N可以为0)。重复的值的顺序将被保存在protocol buffer中。你只要将重复的字段视为动态大小的数组就可以了。

Required是永久性的: 在把一个字段标识为required的时候,你应该特别小心。如果在某些情况下你不想写入或者发送一个 required的字 段,那么将该字段更改为optional可能会遇到问题——旧版本的读者(译者注:即读取、解析消息的一方)会认为不含该字段的消息(message)是 不完整的,从而有可能会拒绝解析。在这种情况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google的一些工程师得出了一个结论: 使用required弊多于利;他们更愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。

你可以在Protocol Buffer Language Guide一文中找到编写.proto文件的完整指南 (包括所有可能的字段类型)。但是,不要想在里面找到与类继承相似的特性,因为protocol buffers不是拿来做这个的。

在得到了一个.proto文件之后,下一步你就要生成可以读写 AddressBook消息(当然也就包括了Person以及 PhoneNumber消息) 的类了。此时你需要运行protocol buffer编译器来编译你的.proto文件:

  • 如果你还没有安装该编译器,下载安装 包并参照README文件中的说明来安装。
  • 安装了之后,就可以运行编译器了。指定源目录(即你的应用程序源代码所在的目录——如果不指定的话,就使用当前目录)、目标目录 (即生成的代码放置的目 录,通常与$SRC_DIR是一样的),以及你的.proto文件所在的目录。在我们这里,可以这样用:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

因为需要生成的是C++类,所以使用了--cpp_out选项参数—— protocol buffers也为其他支持的语言提供了类似的选项参数。这样就可以在你指定的目标目录下生成如下文件:

  • addressbook.pb.h声明你生成的类的头文件。
  • addressbook.pb.cc你生成的类的实现文件。

让我们看一下生成的代码,了解一下编译器为你创建了什么样的类和函数。如果你看了tutorial.pb.h文件,就会发现你得到了一 个类,它对应于 tutorial.proto文件中写的每一个消息(message)。更深入一步,看看Person 类:编译器为每一个字段生成了读写函数。例如,对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(); 

正如你所看到的,getter函数具有与字段名一模一样的名字,并且是小写的,而setter函数都是以set_前缀开头。此外,还有 has_前缀的函 数,对每一个单一的(required或optional的)字段(译者注:此处估计是“非repeated字段”的意思)来说,如果字段被置(set) 了值,该函数会在返回true。最后,每一个字段还有一个clear_前缀的函数,用来将字段重置(un-set)到空状态(empty state)。

然而,数值类型的字段id就只有如上所述的基本读写函数,name和email字段则有一些额外的函数,因为它们是string——前 缀为 mutable_的函数返回string的直接指针(direct pointer)。除此之外,还有一个额外的setter函数。注意:你甚至可以在email还没有被置(set)值的时候就调用 mutable_email(),它会被自动初始化为一个空字符串。在此例中,如果有一个单一消息字段,那么它也会有一个mutable_ 前缀的函数,但是没有一个set_ 前缀的函数。

重复的字段也有一些特殊的函数——如果你看一下重复字段phone 的那些函数,就会发现你可以:

  • 得到重复字段的_size(换句话说,这个Person关联了多少个电 话号码)。
  • 通过索引(index)来获取一个指定的电话号码。
  • 通过特定的索引(index)来更新一个已经存在的电话号码。
  • 向消息(message)中添加另一个电话号码,然后你可 以编辑它(重复的标量类型有一个add_前缀的函数,允许你传新值进 去)

关于编译器如何生成特殊字段的更多信息,请查看文章C++ generated code reference


Enums and Nested Classes 枚举和嵌套类

生成的代码中包含了一个PhoneType 枚举,它对应于.proto文件中的那个枚举。你可以把这个类型当作Person::PhoneType,其值为Person::MOBILE, Person::HOME和Person::WORK(实现的细节稍微复杂了点,但是没关系,不理解它也不会影响你使用该枚举)。

编译器还生成了一个名为Person::PhoneNumber的嵌套类。如果你看看代码,就会发现“真实的”类实际上是叫做 Person_PhoneNumber,只不过Person 内部的一个typedef允许你像一个嵌套类一样来对待它。这一点所造成的唯一一个区别就是:如果你想在另一个文件中对类进行前向声明(forward- declare)的话,你就不能在C++中对嵌套类型进行前向声明了,但是你可以对Person_PhoneNumber进行前向声明。

Standard Message Methods 标准消息函数

每一个消息(message)还包含了其他一系列函数,用来检查或管理整个消息, 包括:

  • bool IsInitialized() const;:检查是否全部的required字段都被置(set)了值。
  • string DebugString() const;:返回一个易读的消息表示形式,对调试特别有用。
  • void CopyFrom(const Person& from);:用外部消息的值,覆 写调用者消息内部的值。
  • void Clear();:将所有项复位到空状态(empty state)。

这些函数以及后面章节将要提到的I/O函数实现了Message 的接口,它们被所有C++ protocol buffer类共享。更多信息,请查看文章 complete API documentation for Message

Parsing and Serialization 解析&序列化

最后,每一个protocol buffer类都有读写你所选择的消息类型的函数。它们包括:

  • bool SerializeToString(string* output) const;:将消息序列化并储存在指定的string中。注意里面的内容是二进制的,而不是文本;我们只是使用string 作为一个很方便的容器。
  • bool ParseFromString(const string& data);:从给定的 string解析消息。
  • bool SerializeToOstream(ostream* output) const;:将消息写入到给定的C++ ostream中。
  • bool ParseFromIstream(istream* input);:从给定的C++ istream解析消息。

protocol buffers和面向对象的设计 protocol buffer类通常只是纯粹的数据存储器(就像C++中的结构体一样);它们在对象模型中并不是一等公民。如果你想向生成的类中添加更丰富的行为,最好的 方法就是在应用程序中对它进行封装。如果你无权控制.proto文件的设计的话,封装protocol buffers也是一个好主意(例如,你从另一个项目中重用一个.proto文件)。在那种情况下,你可以用封装类来设计接口,以更好地适应你的应用程序 的特定环境:隐藏一些数据和方法,暴露一些便于使用的函数,等等。但是你绝对不要 通过继承生成的类来添加行为。这样做的话,会破坏其内部机制,并且不是一 个好的面向对象的实践。

Writing A Message 写消息

现在让我们尝试使用你的protocol buffer类。你想让 你的address book程序完成的第一件事情就是向你的address book文件写入详细的个人信息。要实现这一点,你需要创建protocol buffer类的实例并将它们写入到一个输出流(output stream)中。
下面的这个程序从一个文件中读取AddressBook ,然后根据用户的输入向其中添加一个新的Person ,然后再将新的AddressBook 写回文件中。由protocol buffer编译器生成的代码或者直接调用的代码都被突出显示了。

#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, ' '); 
  
    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库之前执行该宏。它会检查你是不是在无意中链接到了与你使用的头文件不兼容的protocol buffer库。如果检测到了不匹配情况,程序会中止运行下去。注意:每一个.pb.cc文件在开始的时候都会自动调用该宏。

另外还需要注意的是程序结尾处调用的ShutdownProtobufLibrary()函数。该函数所做的所有工作就是删除由Protocol Buffer库分配的全局对象。在大多数程序中,这都是没有必要的,因为进程一退出,操作系统就回收了它的内存。然而,如果你使用了内存检查工具(译者 注:例如valgrind)来检查你的程序的话(内存检查工具要求每一个对象最后都要被释放),或者你写了一个可能会在一个进程中多次被加载、卸载的库, 那么你可能就需要强制Protocol Buffer来清理一切了。

Reading A Message 读消息

当然,如果你不能从一个address book中取出信息的话,那么它也就没什么用了!下面的例子展示了如何读取上面的程序创建的文件,并将读到的所有信息打印出来。

#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; 
} 

Extending a Protocol Buffer 扩展一个protocol buffer

无论或早或晚,在你放出你那使用protocol buffer的代码之后,你必定会想“改进”protocol buffer的定义。如果你想让你的新buffer向后兼容(backwards-compatible),并且旧的buffer能够向前兼容 (forward-compatible)——你一定希望如此——那么你在新的protocol buffer中就要遵守其他的一些规则了:

  • 对已存在的任何字段,你都不能更改其标识(tag)号。
  • 你绝对不能添加或删除任何required的字段。
  • 你可以添加新的optional或repeated的字段,但是你必须使用新的标识(tag)号(例如,在这个protocol buffer中从未使用过的标识号——甚至于已经被删除过的字段使用过的标识号也不行)。

(有一些例外情况,但是它们很少使用。)

如果你遵守这些规则,老的代码将能很好地解析新的消息(message),并忽略 掉任何新的字段。对老代码来说,已经被删除的optional字段将被赋 予默认值,已被删除的repeated字段将是空的。新的代码也能够透明地读取旧的消息。但是,请牢记心中:新的optional字段将不会出现在旧的消 息中,所以你要么需要显式地检查它们是否由has_前缀的函数置(set)了值,要么在你的.proto文件中,在标识(tag)号的后面用 [default = value]提供一个合理的默认值。如果没有为一个optional项指定默认值,那么就会使用与特定类型相关的默认值:对string来说,默认值是空 字符串。对boolean来说,默认值是false。对数值类型来说,默认值是0。还要注意:如果你添加了一个新的repeated字段,你的新代码将无 法告诉你它是否被留空了(被新代码),或者是否从未被置(set)值(被旧代码),这是因为它没有has_标志。

Optimization Tips 优化小技巧

Protocol Buffer 的C++库已经做了极度优化。但是,正确的使用方法仍然会提高很多性能。下面是一些小技巧,用来提升protocol buffer库的最后一丝速度能力:

  • 如果有可能,重复利用消息(message)对象。即使被清除掉,消息(message)对象也会尽量保存所有被分配来重用的内存。这样 的话,如果你正在 处理很多类型相同的消息以及一系列相似的结构,有一个好办法就是重复使用同一个消息(message)对象,从而使内存分配的压力减小一些。然而,随着时 间的流逝,对象占用的内存也有可能变得越来越大,尤其是当你的消息尺寸(译者注:各消息内容不同,有些消息内容多一些,有些消息内容少一些)不同的时候, 或者你偶尔创建了一个比平常大很多的消息(message)的时候。你应该自己监测消息(message)对象的大小——通过调用SpaceUsed函数 ——并在它太大的时候删除它。
  • 在多线程中分配大量小对象的内存的时候,你的操作系统的内存分配器可能优化得不够好。在这种情况下,你可以尝试用一下Google's tcmalloc

Advanced Usage 高级使用

Protocol Buffers的作用绝不仅仅是简单的数据存取以及序列化。请阅读C++ API reference全文来看看你还能用它来做什么。

protocol消息类所提供的一个关键特性就是反射。你不需要编写针对一个特殊 的消息(message)类型的代码,就可以遍历一个消息的字段,并操纵 它们的值,就像XML和JSON一样。“反射”的一个更高级的用法可能就是可以找出两个相同类型的消息之间的区别,或者开发某种“协议消息的正则表达 式”,利用正则表达式,你可以对某种消息内容进 行匹配。只要你发挥你的想像力,就有可能将Protocol Buffers应用到一个更广泛的、你可能一开始就期望解决的问题范围上。

“反射”是由Message::Reflection interface提供的。

 

posted on 2011-02-28 21:47  cvbnm  阅读(4230)  评论(1编辑  收藏  举报

导航