序列化工具protobuf初解

系列文章目录

提示:这里可以添加系列文章的所有文章的目录

文章目录

  • 系列文章目录
  • 前言
  • 使用 Protobuf2
    • 第一步,创建 .proto 文件,定义数据结构
    • 第二步,protoc 编译 .proto 文件生成读写接口
    • 第三步,调用接口实现序列化、反序列化以及读写
  • 总结

前言

网络通信和通用数据交换等应用场景中经常使用的技术是 JSON 、 XML、ProtoBuf。

protocol buffers 是一种语言无关平台无关、可扩展的二进制序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
Protocol Buffers 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序

序列化:将结构数据对象转换成能够被存储传输(例如网络传输)的格式,同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。

使用 Protobuf2

第一步,创建 .proto 文件,定义数据结构

// 例1: 在 xxx.proto 文件中定义 Example1 message
syntax = "proto2";
package tutorial; // 包声明,避免命名空间的冲突

message Person {        //person 消息
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

  enum PhoneType {    //枚举类型
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {    //多个person组成的addressbook
  repeated Person people = 1;
}

我们在上例中定义了一个名为 Example1 的 消息,语法很简单,message 关键字后跟上消息名称

message xxx {

}

之后我们在其中定义了 message 具有的字段,形式为:

message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
可以认为repeated字段代表 动态数组
// 类型:int32、int64、sint32、sint64、string、32-bit …
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字),
某个消息内,字段编号不能重复,1-15编码后占的空间更小
// 字段规则 类型 名称 = 字段编号;
repeated int32 repeatedInt32Val = 4;
}

required字段慎用,如果在某个时候停止写入或发送必填字段,并将该字段更改为可选字段会产生问题—旧的读者会认为没有此字段的消息不完整,并且可能会无意中拒绝或删除它们。在谷歌内部,required字段非常不受欢迎;在proto2语法中定义的大多数消息都使用可选的和重复的(Proto3根本不支持required字段。)

第二步,protoc 编译 .proto 文件生成读写接口

我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。

当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。 ProtoBuf 提供相应的接口代码。如何提供?答案就是通过 protoc 这个编译器。
可通过如下命令生成相应的接口代码:

// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码

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

在例1生成的文件

addressbook.pb.h 头文件
addressbook.pb.cc 代码实现

最终生成的代码将提供类似如下的接口:

1、序列化和解析接口
序列化工具protobuf初解_第1张图片 2、protoc 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;    //返回id
  inline void set_id(int32_t value);  //设置id

  // 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();    // 返回该字段指针

  // phones
  inline int phones_size() const;   //repeated 字段方法
  inline void clear_phones();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
  inline const ::tutorial::Person_PhoneNumber& phones(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phones(int index);
  inline ::tutorial::Person_PhoneNumber* add_phones();   //增加

3、消息的检查函数

  bool IsInitialized() const;:检查是否全部的required字段都被置(set)了值。

  string DebugString() const;:返回一个易读的消息表示形式,对调试特别有用。

  void CopyFrom(const Person& from);:用外部消息的值,覆写调用者消息内部的值。

  void Clear();:将所有项复位到空状态(empty state)。

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

第三步,调用接口实现序列化、反序列化以及读写

针对第一步中例1定义的 message,我们可以调用第二步中生成的接口,实现写消息,下面的这个程序从一个文件中读取AddressBook ,然后根据用户的输入向其中添加一个新的Person ,然后再将新的AddressBook 写回文件中。


#include 
#include 
#include 
#include "addressbook.pb.h"
using namespace std;

// 这个函数 填写一个人的基本信息.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(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);  //设置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_phones();// phone_number 动态数组指针
    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;

  {
    // 读入已存在的 address_book,地址是 argv[1]
    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;
    }
  }

  // 调用 最上面定义的函数,PromptForAddress,增加一个Person,可通过多次调用该接口增加联系人,add_people()会返回Person*指针    
  PromptForAddress(address_book.add_people());

  {
    // 写一个新的 address book 回硬盘.
    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:  删除所有的全局对象 by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

总结

Protocol Buffers的作用绝不仅仅是简单的数据存取以及序列化。

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

你可能感兴趣的:(远程调用RPC,编程语言,数据结构,protobuf)