protobuf的简单使用


    接触protobuf是因为公司业务需要,从而又了解了Google给程序猿提供的又一杀器。

在上家公司,接口使用socket通讯,双方定义接口时,偶尔会因为一些字节序问题导致数据错乱。当时,我们是自己针对给定的接口,自己写的结构体,然后遇到变长字符串,就用char[1]这种数组结构支持字符串。这是种很原生的实现方式,需要在发送和接收的实现方法中,加上对大小端字节序的处理,很麻烦。如果当时能接触到protobuf,或许能省下很多事儿。

    好了,废话不多说,让我们开始protobuf的使用。在学习任何一种东西之前,我们要先了解它的诞生原因,这样才能方便我们更好的知道,它有什么用,为什么要使用它。

protobuf的产生

    protobuf,全称protocol buffers,最早出现在Google公司中,简单说来,它是一种保存结构化数据的格式,类似xml,和json。后来Google开源,我们这些平凡普通的程序猿才能享受这个成果。so,感谢开源。感谢Google。

    为什么使用 Protocol Buffers?

在回答这个问题之前,我还有个问题要问,你是怎么序列化和反序列化结构化的数据?不必回答,我来告诉你

  • 原始的内存中的结构化数据,可以以二进制形式发送或者保存。随着时间的推移,这是一种脆弱的方法。因为接收/读取的代码必须以相同的内存形式、字节顺序等等编译。因为文件以原始格式积累数据,以及与原始格式相关联的软件副本的传播,导致很难扩展格式。
  • 你也可以创造一种特别指定的方式,将数据样式编码成一个单独的字符串,比如将4个整型数据12,3,-23,67编码成"12:3:-23:67"。这是一个简单并且灵活的方法,尽管它要求写一次性编码和解码的代码,并且解析还强加一小部分的时间花费。这种方法适合编码一些非常简单的数据。
  • 序列化数据成XML。这个方法很有吸引力,因为XML是人类可读的,并且很多语言都有对应的库提供。这是一个好的选择如果你想和其他程序共享数据的话。然而,XML是众所周知的空间紧凑,并且编码和解码都强加了大量的性能压力给程序。并且,导航一个XML的DOM树比导航类中的简单字段更复杂。

Protocol buffers是灵活的,有效率的,自动化的方案,针对于解决这些具体的问题。通过Protocol buffers,你只要写一个.proto文件,描述你要保存的数据的结构。然后,protocol buffer编译器创建一个类,包含自动解码和编码协议数据的实现,通过一个有效率的二进制格式。重要的是,protocol buffer格式支持格式的扩展,随着时间的推移,代码依然能读取以旧格式编码成的数据。

    以上内容翻译自 https://developers.google.com/protocol-buffers/docs/cpptutorial 中的why use protocol buffers,读者们可以自行查阅,能力有限,有翻译的不周到的地方,请批评指正。

    上面的内容,说明了protocol buffer的两个优点:效率高,代码自动生成。而从protocol buffer的名称,我们可以知道,这种结构常用与协议通讯。

protobuf编程

    现在,我们可以开始protobuf有关的编程了。首先,要介绍的是protobuf的代码生成程序。

Protocol buffer compiler的安装

    我们可以选择release版安装和源码安装,在这里,介绍的是源码安装。   

release版下载地址:https://github.com/google/protobuf/releases

源码github地址:https://github.com/google/protobuf

    读者可以自行去查看protobuf/src/README.md 下的介绍文档,在这里,我简单说下这个文档介绍的安装过程。

如果是使用github的源码安装,那么以下工具是需要的:

  • autoconf
  • automake
  • libtool
  • curl (used to download gmock)
  • make
  • g++
  • unzip
开始执行脚本;

$ ./autogen.sh
接下来动作相同:
$ ./configure
$ make
$ make check
$ sudo make install
$ sudo ldconfig # refresh shared library cache.
就像描述说的,最后一步ldconfig,是刷新生成的protocol buffer的共享库。可以看到,$这个符号,这说明,是以非root用户登录系统的,虽然非root操作有点不便利,但为了安全起见,还是建议大家使用非root用户登录服务器。而sudo 就是为了获取这个命令的操作权限。

    默认的,安装包会安装在/usr/local目录下,然而,在许多平台上,/usr/local/lib并不是LD_LIBRARY_PATH的一部分,当然,你也可以手动把它加进去,但更简单的方法就是我们指定安装在/usr目录下。这样的话,就要这么调用configure程序

./configure --prefix=/usr
如果你之前用的是不一样的前缀,在重新build前,确保你调用了make clean。

    怎么检验我们安装成功了呢?

protoc
如果提示 -bash: protoc: command not found,则说明安装失败。如果提示 Missing input file. 则说明安装成功。

    那么这个命令的用法是什么?

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
.proto文件就是我们描述结构化数据格式的文件

定义protocol 格式

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 phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

正像你看到的,这里面,有像Java的语法,如package,也有像c++的语法。现在让我们分析上面的.proto文件.

    .proto文件以package的声明开始,这个有利于解决不同工程间的命名冲突。而在生成的c++代码中,namespace的名称对应于package的名称。

    然后,是一系列消息的定义。消息只是一个包含一组类型字段的聚合。许多标准的简单类型在protocol buffer中都能找到。包含bool,int32,float,double和string.你也可以在消息中增加另外个消息类型作为一种类型字段。在上面的例子中,Person消息包含了PhoneNumber消息,AddressBook消息包含了Person消息。你甚至可以在消息内部嵌套消息类型,就像上面例子中的PhoneNumber定义在Person消息内。你也可以定义enum枚举类型,如果你的数据中包含一些特定的值。

                表1-protocol buffer数据类型与c++类型的映射关系

protobuf 数据类型

描述

打包

C++语言映射

bool

布尔类型

1字节

bool

double

64位浮点数

N

double

float

32为浮点数

N

float

int32

32位整数、

N

int

uin32

无符号32位整数

N

unsigned int

int64

64位整数

N

__int64

uint64

64为无符号整

N

unsigned __int64

sint32

32位整数,处理负数效率更高

N

int32

sing64

64位整数 处理负数效率更高

N

__int64

fixed32

32位无符号整数

4

unsigned int32

fixed64

64位无符号整数

8

unsigned __int64

sfixed32

32位整数、能以更高的效率处理负数

4

unsigned int32

sfixed64

64为整数

8

unsigned __int64

string

只能处理 ASCII字符

N

std::string

bytes

用于处理多字节的语言字符、如中文

N

std::string

enum

可以包含一个用户自定义的枚举类型uint32

N(uint32)

enum

message

可以包含一个用户自定义的消息类型

N

object of class


        N 表示打包的字节并不是固定。而是根据数据的大小或者长度。例如int32,如果数值比较小,在0~127时,使用一个字节打包。

    接下来要说的就是 字段名称后面的 "=1","=2"标记,用以唯一声明消息内的每个元素,我们称为字段编码值。编码值的取值范围为1-2^32,其中,1-15的编码,在时间和空间上效率都是最高的,编码值越大,其编码的时间和空间效率就越低。注意,1900-2000编码值为google protobuf内部保留值,建议不要使用,另外,消息中的字段编码值无需连续,只要是合法的,并且唯一的。    

    每个字段必须用以下几个修饰符修饰:

  • required: 字段的值必须被提供,否则消息会被视为"uninitialized",即未初始化。如果libprotobuf库以debug模式编译,那么序列化一个未初始化的消息,会导致断言失败。而在优化的版本中,检查会被忽略,消息会被写入,但是,解析一个未初始化的消息总是会失败。
  • optional: 字段值可以设置,也可以不设置。如果字段没有设置,则一个默认的值会被使用。对于简单的类型(非消息类型),你可以指定默认值,正像例子中我们设置的PhoneNumber消息中的type字段。否则,系统会默认:整型类型默认0,字符串类型默认空字符串,bool类型默认false,对于嵌入的消息,默认值就是默认的实例,在消息中,没有一个值被设置。
  • repeated:字段的值可以重复任意次(包含0次)。值的重复顺序会被保存在protocol buffer中,将repeated字段视为动态数组即可。

编译protocol buffer文件

    即编译上文我们定义的.proto文件,命令如下

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

使用protocol buffer

    编译完.proto文件后,我们得到*.pb.h和*.pb.cpp,之后,我们就像操作普通的.h和.cpp文件一样的使用它们。首先,这些文件是定义了一些消息类,那么现在,就到了我们使用这些消息的时候。我们可以通过它,解析收到的数据,也可以通过它,序列化要发送的数据。

    我们继续以上述的proto文件为例。

序列化消息

    序列化消息的过程,是将一个消息对象结构,转换成一个字节数组的过程,是写的过程。下面是一个包含了写消息和读消息的例子。

先上代码:

#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;
}
以上示例,来自于protocol buffer的开发者指南。我们看到,PromptForAddress函数内,将用户输入的数据,写入我们定义的Person消息中。

设置字段的值:

    1.set_yyy(value);设置yyy字段的值,其中yyy即为字段的名称,当yyy为required或optional时使用。

    2.add_yyy(value); 设置yyy字段的值,其中yyy为字段的名称,当且仅当yyy为repeated时使用.

    main中,首先通过ParseFromIstream,初始化消息数据,然后添加address数据,最后,通过SerializeToOstream,序列化消息,输出至文件中。

序列化和解析的API接口

  • 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.  序列化消息成c++输出流
  • bool ParseFromIstream(istream* input); :  parses a message from the given C++ istream.  解析消息,数据来源是c++输入流

解析消息

    序列化数据的过程,是将一个字节数组,转换成消息对象的过程,是读的过程。下面是一个包含了读消息的例子。

#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.phones(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;
}

现在我们分析代码,ListPeople函数的功能是遍历打印AddressBook消息的。

    判断字段是否被设置:has_xxx();  //xxx为字段名称

    获取字段的值: xxx();  //xxx为字段名称

基础用法就介绍到这里了。

本博客部分内容,来源于网站:https://developers.google.com/protocol-buffers/docs/cpptutorial


评论

发表评论