C++中ProtoBuffer使用详解

作者:袖梨 2022-06-25


 一、为什么使用Protocol Buffer?

      在回答这个问题之前,我们还是先给出一个在实际开发中经常会遇到的系统场景。比如:我们的客户端程序是使用Java开发的,可能运行自不同的平台,如:Linux、Windows或者是Android,而我们的服务器程序通常是基于Linux平台并使用C++开发完成的。在这两种程序之间进行数据通讯时存在多种方式用于设计消息格式,如:
      1. 直接传递C/C++语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C++程序而言就非常方便了,仅需将接收到的数据按照结构体类型强行转换即可。事实上对于变长结构体也不会非常麻烦。在发送数据时,也只需定义一个结构体变量并设置各个成员变量的值之后,再以char*的方式将该二进制数据发送到远端。反之,该方式对于Java开发者而言就会非常繁琐,首先需要将接收到的数据存于ByteBuffer之中,再根据约定的字节序逐个读取每个字段,并将读取后的值再赋值给另外一个值对象中的域变量,以便于程序中其他代码逻辑的编写。对于该类型程序而言,联调的基准是必须客户端和服务器双方均完成了消息报文构建程序的编写后才能展开,而该设计方式将会直接导致Java程序开发的进度过慢。即便是Debug阶段,也会经常遇到Java程序中出现各种域字段拼接的小错误。
      2. 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的XML描述信息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整体运行性能明显下降。
      对于以上两种方式所产生的问题,Protocol Buffer均可以很好的解决,不仅如此,Protocol Buffer还有一个非常重要的优点就是可以保证同一消息报文新旧版本之间的兼容性。至于具体的方式我们将会在后续的博客中给出。

定义一个Protocol Buffer消息

使用实例

// 定义一个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;
}

细节解释
1、package declaration为了阻止不同工程间的naming conflicts,这儿的tutorial相当于namespace。

2、message是一个包含若干类型字段的集合,可以使用bool、int32、float、double和string类型。可以内嵌message集合,类似于struct。

3、“=1”、“2”记号标识在二进制编码中类型字段的独特Tag,表示不同的字段在序列化后的二进制数据中的布局位置。

Tag number 1-15相对于更高的数字,少用了一个字节,所以可以使用1-15的Tag作为commonly used的repeated elements,16或者更高的Tag留给less-commonly use留给optional elements。

4、每个字段都必须使用如下标示符

required:字段值必须被提供,否则消息会被认为uninitialized。
optional:字段值可选
repeated:字段也许会被重复任何次数(包括0次)。可以将repeated field看做动态大小数组。
5、enum是枚举类型定义的关键字,0和1表示枚举值所对应的实际整型值,和C/C++一样,可以为枚举值指定任意整型值,而无需总是从0开始定义。

6、可以在同一个.proto文件中定义多个message,这样便可以很容易的实现嵌套消息的定义。Protocol Buffer提供了另外一个关键字import,这样我们便可以将很多通用的message定义在同一个.proto文件中,而其他消息定义文件可以通过import的方式将该文件中定义的消息包含进来,如:

import "myproject/CommonMessages.proto"

限定符(required/optional/repeated)的基本规则
1、在每个消息中必须至少留有一个required类型的字段。

2、每个消息中可以包含0个或多个optional类型的字段。

3、repeated表示的字段可以包含0个或多个数据。

4、如果打算在原有消息协议中添加新的字段,同时还要保证老版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional或repeated。道理非常简单,老版本程序无法读取或写入新增的required限定符的字段。

Protocol Buffer消息升级原则
1、不要修改已经存在字段的标签号。

2、任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。

3、在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。

4、int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。

5、optional和repeated限定符也是相互兼容的。

编译你的Protocol Buffers

编译方法

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
注:$SRC_DIR为Source Directory,$DST_DIR为Destination Directory,编译后,Destination Directory将会有以下两个文件

addressbook.pb.h:头文件,声明产生的类
addressbook.pb.cc:cpp文件,实现产生的类。
ProtoBuffer API

经过编译后我们能够得到下面这些消息API函数


// 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 ();

标准Message方法

每个消息都会包含一系列其他方法,允许你检查或者操作整个消息:


bool IsInitialized () const ; // 检查是否所有required field被设置
string DebugString () const ; // 返回一个有关消息的可读描述,对于调试很有用
void CopyFrom (const Person & from ); // 用给定的消息值来重写消息
void Clear (); // 将所有元素清空到empty state
使用你的ProtocBuffer
解析和序列化(Parsing and Serialization)
每个Protocol buffer类都有若干函数,这些函数能使用Protocol buffer binary format,来写入和读取你所选择的信息。


bool SerializeToString (string * output ) const ; // serializes the message and stores the bytes in the given string. Note that the bytes are binary, not text; we only use the string class as a convenient container.
bool ParseFromString (const string & data ); //  parses a message from the given string.
bool SerializeToOstream (ostream * output ) const ; // writes the message to the given C++ ostream.
bool ParseFromIstream (istream * input ); // parses a message from the given C++ istream.
Writing A Message

#include
#include
#include
#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;
}
Reading A Message


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

相关文章

精彩推荐