C++ Protobuf の学習と使用

導入

プロトコル バッファーは、言語、プラットフォームに依存せず、構造化データをシリアル化するための拡張可能な方法であり、(データ)通信プロトコルデータ ストレージなどに使用できます。通信中に送信される情報は、Protobuf によって定義されたメッセージ データ構造を通じてパッケージ化され、送信または保存のためにバイナリ コード ストリームにコンパイルされます。

プロトコル バッファーは、柔軟で効率的で自動化された構造化データのシリアル化方法であり、XML に匹敵しますが、XML よりも小さく (3 ~ 10 倍)、高速 (20 ~ 100 倍) で、シンプルです。

Protobuf を使用する場合は、IDL (インターフェイス記述言語) ファイルを作成し、その中でデータ構造を定義する必要があります。シリアル化および逆シリアル化できるのは、事前定義されたデータ構造のみです。このうち、オブジェクトをバイナリデータに変換するのがシリアル化、バイナリデータをオブジェクトに変換するのがデシリアライズです。

チュートリアル


このチュートリアルでは、基本的な C++ プログラマー向けにプロトコル バッファーの使用方法を紹介します。簡単なサンプル アプリケーションを作成することで、次のことを行う方法がわかります。

  • .proto ファイルでメッセージ形式を定義します。
  • プロトコルバッファコンパイラを使用します。
  • C++ プロトコル バッファー API を使用してメッセージの書き込みと読み取りを行います。

これは、C++ でプロトコル バッファーを使用するための包括的なガイドではありません。詳細なリファレンス情報については、「プロトコル バッファー言語ガイド (proto2)」、「プロトコル バッファー言語ガイド (proto3)」、「C++ API リファレンス」、「C++ 生成コード ガイド」、および「エンコーディング リファレンス」を参照してください。


私たちが使用する例は、ユーザーの連絡先の詳細をファイルに読み書きする、非常に単純な「アドレス帳」アプリケーションです。各個人は、アドレス帳に名前、ID、電子メール アドレス、連絡先電話番号を持っています。

このような構造化データをシリアル化して取得するにはどうすればよいですか? この問題を解決するには、いくつかの方法があります。

  • 生メモリ内のデータ構造はバイナリ形式で送信/保存できます受信/読み取りコードはまったく同じメモリ レイアウト、エンディアンなどでコンパイルする必要があるため、これは脆弱なアプローチです。さらに、ファイルは元の形式でデータを蓄積し、ソフトウェアのコピーはこの形式でネットワーク上を移動するため、形式を拡張することが困難です。
  • データ項目を単一の文字列としてエンコードするアドホックな方法を考案することもできます。たとえば、4 つの int を「12:3:-23:67」としてエンコードすることもできます。これはシンプルで柔軟なアプローチですが、一度エンコードと解析コードを作成する必要があり、解析にはわずかなランタイム コストがかかります。これは、非常に単純なデータをエンコードする場合に最適です。
  • データを XML にシリアル化します。XML は(ある意味)人間が可読であり、多くの言語を備えているため、このアプローチは非常に魅力的です。他の人とデータを共有したい場合、これは良いアプリケーション/プロジェクトです。ただし、XML はスペースを大量に消費することで知られており、XML のエンコード/デコードによってアプリケーションのパフォーマンスが大幅に低下する可能性があります。また、クラス内の単純なフィールドをナビゲートするよりも、XMLDOM ツリーをナビゲートする方が多くの場合行われます。

これらのオプションの代わりに Protobuf を使用できます。プロトコル バッファーは、この問題に正確に対処する、柔軟で効率的かつ自動化されたソリューションです。Protobuf を使用すると、.proto保存したいデータ構造の記述を記述することができます。Protobuf コンパイラは、Protobuf データの自動エンコードと効率的なバイナリ形式の解析を実装するクラスを作成します。生成されたクラスは、プロトコル バッファを構成するフィールドであり、プロトコル バッファを単位として読み取る役割を果たします。重要なのは、プロトコル バッファー形式は、コードが古い形式でエンコードされたデータを読み取れるように、時間の経過とともに形式を拡張するというアイデアをサポートしていることです。

サンプルコードは、ソースコードパッケージの 「examples」ディレクトリに含まれています。

プロトコル形式の定義

アドレス帳アプリケーションを作成するには、.proto開始ファイルが必要です。.protoファイル内の定義は単純です。シリアル化するデータ構造ごとにメッセージを追加し、メッセージ内の各フィールドの名前と型を指定します。以下はメッセージを定義する.protoファイル ですaddressbook.proto

syntax = "proto2";

package tutorial;

message 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 {
  repeated Person people = 1;
}


ご覧のとおり、その構文は C++ または Java に似ています。各部分を見て、それが何をするのかを見てみましょう。


.protoファイルはパッケージ宣言で始まります。これは、異なるプロジェクト間の名前の競合を防ぐのに役立ちます。C++ では、生成されたクラスはパッケージ名と一致する名前空間に配置されます。

次に、メッセージを定義します。メッセージには、一連の型付きフィールドが含まれますboolint32、 、などfloatdoublestringフィールド タイプとして使用できる標準的な単純なデータ タイプが多数あります他のメッセージ タイプを使用して、さらに構造フィールド タイプをメッセージに追加することもできます。上の例では、Personmessage contains message contains messageです。他のメッセージ内にネストされたメッセージ タイプを定義することもできます。ご覧のとおり、 タイプ定義はの中にあります。フィールドの 1 つに事前定義された値リストのいずれかを含める場合は、タイプを定義することもできます。ここでは、電話番号が次の電話タイプのいずれかであることを指定します: またはPhoneNumber AddressBookPersonPhoneNumberPersonenumMOBILEHOMEWORK

各要素の「=1」、「=2」フラグは、バイナリ エンコードで使用するフィールドを識別しますフィールド番号 1 ~ 15 は、それより大きい番号よりもエンコードに必要なバイトが 1 バイト少ないため、最適化として、フィールド番号 16 と 18 を残して、これらの一般的に使用される要素番号または繰り返し使用される要素番号を使用することを決定することもできます。あまり使用されないオプション要素の場合は高くなります。の各要素フィールドはフィールド番号を再コード化する必要があるため、特にフィールドが繰り返される場合、これは最適化する良い方法です。

各フィールドには、次のいずれかの修飾子を使用して注釈を付ける必要があります。

  • optional:フィールドは設定することも、設定しないこともできますオプションのフィールド値の場合は、デフォルト値を使用します。単純なタイプの場合は、例の電話番号の場合typeと同様に、独自のデフォルト値を指定できます。それ以外の場合は、システムのデフォルト値が使用されます。数値型の場合は 0、空の文字列の場合は string、ブール型の場合は false です。埋め込みメッセージの場合、デフォルト値は常に、フィールドが設定されていないメッセージの「デフォルト インスタンス」または「プロトタイプ」です。常に明示的に設定されるとは限らないオプション (または必須) フィールドに対してアクセサーを呼び出すと、そのフィールドのデフォルト値が返されます。
  • repeated:フィールドは任意の回数(ゼロを含む) 繰り返すことができます。繰り返される値の順序はプロトコル バッファーに保存されます。繰り返されるフィールドを、動的にサイズ変更される配列として扱います
  • required:フィールドの値を指定する必要があります。指定しないと、メッセージは「初期化されていない」とみなされます。デバッグ モードでコンパイルした場合libprotobuf、初期化されていないメッセージをシリアル化するとアサーションが失敗します。最適化されたビルドでは、チェックはスキップされ、メッセージが書き込まれます。ただし、初期化されていないメッセージの解析は常に失敗します (parse メソッドから返されることによりfalse)。それ以外は、必須フィールドはオプション フィールドとまったく同じように動作します。

重要

フィールドをRequired Is Forever としてマークする場合は、細心の注意を払う必要があります required。ある時点で、必須フィールドの書き込みや送信をやめたい場合は、フィールドをオプションに変更します。古い読者はメッセージを受け取り、フィールドが不完全であるとみなして拒否する可能性があります。または誤って破棄してしまうこともあります。バッファを考慮する必要があります。フィールドはGoogle 内では 非常に好まれませんrequired 。proto2 構文で定義されているほとんどのメッセージは optional and repeated のみを使用します (Proto3 は、only をサポートしません (Proto3 はフィールドをまったくサポートしません required 。) のすべてのフィールド)。

ファイルの書き込みに関する完全なガイド.proto(考えられるすべてのフィールド タイプを含む) については、  「プロトコル バッファー言語ガイド」を参照してください。クラス継承のようなものを探しに行かないでください。プロトコル バッファーではそれができません。

プロトコル バッファのコンパイル プロトコル バッファのコンパイル

これで が完成したので.proto、次に行う必要があるのは、AddressBook(したがってPersonと PhoneNumber) メッセージの読み取りと書き込みに必要なクラスを生成することです。これを行うには、プロトコル バッファ コンパイラprotocで実行する必要があります.proto

  1. コンパイラがインストールされていない場合は、 パッケージをダウンロードし、README の指示に従ってください。

  2. 次に、ソース ディレクトリ (値を指定しない場合、アプリケーションのソース コードがまだ存在する場所)、ターゲット ディレクトリ (生成されたコードを配置する場所、通常は同じ)、およびパスを指定してコンパイラを実行します。あなたへ。$SRC_DIRこの .proto場合、あなたは……:

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

C++ クラスが必須であるため、--cpp_outサポートされている他の言語にはオプション -similar が提供されています。

これにより、指定されたターゲット ディレクトリに次のファイルが生成されます。

    • addressbook.pb.h、生成されたクラスを宣言するヘッダー。
    • addressbook.pb.cc、これにはクラスの実装が含まれています。

プロトコル バッファ API プロトコル バッファ API

生成されたコードを見て、コンパイラーがどのようなクラスと関数を作成したかを確認してみましょう。addressbook.pb.h を見ると、addressbook.proto で指定されたメッセージごとにクラスがあることがわかります。Person クラスを詳しく見ると、コンパイラがフィールドごとにアクセサーを生成していることがわかります。たとえば、名前、ID、電子メール、電話番号のフィールドには、次のメソッドを使用できます。

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

  // phones
  inline int phones_size() const;
  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();

ご覧のとおり、ゲッターはフィールドとまったく同じ小文字の名前を持ち、セッター メソッドは で始まりますset_has_フィールドが準備されている場合は、メソッド単数 (必須またはオプション) フィールドもあります。最後に、各フィールドには、clear_フィールドを空の状態に戻すメソッドがあります。

数値idフィールドには上記の基本的なアクセサー セットのみがあり、 nameフィールドemailには文字列と同様に 2 つの追加メソッド (mutable_ 文字列へのポインターを直接取得できるゲッターとセッターmutable_email()設定) があり、自動的に空に初期化されます。弦。この例で繰り返しメッセージ フィールドがある場合、メソッドのemail 代わりにメソッドも含まれます。mutable_

繰り返しフィールドには特別なメソッドもいくつかあります - 繰り返しphonesフィールドを見てみるとわかります。

  • 重複したフィールドがないか_size(つまり、  Person2#2 が持つ電話番号の数) を確認します。
  • インデックスを使用して、指定された電話番号を取得します。
  • 指定されたインデックスにある既存の電話番号を更新します。
  • メッセージに別の電話番号を追加し、編集できます (繰り返しスカラー タイプ用の電話番号が 1 つありadd_、新しい値を渡すだけです)。

プロトコル コンパイラが特定のフィールド定義に対してどのメンバーを生成するかについて詳しくは、「  C++ 生成コード リファレンス」を参照してください。

列挙型と入れ子になったクラス

生成されたコードには、あなたのものにPhoneType対応する.proto列挙型 enum が含まれています。この型を と呼ぶことができPerson::PhoneType、その値は Person::MOBILE、 、Person::HOMEですPerson::WORK(実装の詳細はもう少し複雑ですが、列挙型を使用するためにそれらを知る必要はありません)。

コンパイラは、 というクラスも生成します Person::PhoneNumber。コードを見ると、「実際の」クラスが実際には と呼ばれていることがわかりますが、ネストされたクラスとして扱えるようにするPerson_PhoneNumbertypedef が内部で定義されています 。Personこれが違いを生む唯一のケースは、それを別のファイルに入れたい場合です。C++ ではネストされた型を前方宣言することはできませんが、前方宣言することはできます Person_PhoneNumber

標準的なメッセージ方式

各メッセージ クラスには、メッセージ全体を検査または操作できる次のような他のメソッドも多数含まれています。

  • bool IsInitialized() const;: すべての必須フィールドが入力され、準備ができていることを確認してください。
  • string DebugString() const;: 人間が判読できるメッセージを返します。特にデバッグに役立ちます。
  • void CopyFrom(const Person& from);: 指定されたメッセージ値を使用します。
  • void Clear();: すべての要素を空の状態にクリアします。

これらのメソッドと次のセクションで説明する I/O メソッドは、 Messageすべての C++ プロトコル バッファ クラスで共有されるインターフェイスを実装します。詳細については、  Message の完全な API ドキュメントを参照してください。

解析とシリアル化

最後に、各プロトコル バッファー クラスには、選択したタイプのバイナリ形式でメッセージを読み書きするためのメソッドがあります 。これらには次のものが含まれます。

  • bool SerializeToString(string* output) const;: メッセージをシリアル化し、指定された文字列にバイトを格納します。バイトはテキストではなくバイナリであることに注意してください。stringクラスを便利なコンテナとして使用しているだけです。
  • bool ParseFromString(const string& data);: 指定された文字列から
  • bool SerializeToOstream(ostream* output) const;: C++ #2 にメッセージを書き込みます
  • bool ParseFromIstream(istream* input);: 指定された C++ から istream

これらは、解析とシリアル化のために提供されるオプションのほんの一部です。 完全なリストについては、 メッセージ API リファレンスを参照してください。

メッセージを書く

次に、プロトコル バッファ クラスを使用してみましょう。アドレス帳アプリケーションで最初にできることは、アドレス帳ファイルに個人情報を書き込むことです。これを行うには、プロトコル バッファ クラスを作成して設定し、それを出力ストリームに書き込む必要があります。

以下は、ファイルから読み取り、ユーザー入力に基づいてAddressBook新しいファイルを追加し 、そのファイルに新しいファイルを書き込み、新しい未定義のファイルを再びファイルに書き戻すプログラムです。直接の呼び出しまたは参照は、プロトコル コンパイラによって強調表示されます。PersonAddressBook

#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_phones();
    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_people());

  {
    // 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++ プロトコルを使用する前にこのマクロバッファ ライブラリを実行することをお勧めします。コンパイルしたヘッダーのバージョンと互換性のないライブラリのバージョンに対して誤ってリンクしていないことを確認します。バージョンの不一致が検出された場合、プログラムは中止されます。注意事項.pb.ccこのマクロは起動時に各ファイルによって自動的に呼び出されます。

ShutdownProtobufLibrary()プログラムの最後の への呼び出しにも注意してください。これは、プロトコル バッファ ライブラリによって割り当てられたグローバル オブジェクトを削除するだけです。プロセスはとにかく終了するものであり、OS がすべてのメモリを再利用するため、ほとんどのプログラムではこれは不要です。ただし、使用するメモリ リーク チェッカーで必要な場合、または複数回ロードおよびアンロードできるライブラリを作成している場合は、プロトコル バッファにすべてを強制的にクリーンアップすることをお勧めします。

メッセージタイプを定義する

まず、非常に簡単な例を見てみましょう。各検索リクエストにクエリ文字列、関心のある結果の特定のページ、および各ページの結果の数が含まれるリクエスト メッセージ形式を定義するとします。以下は、メッセージ タイプを定義するために使用するファイルです.proto

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • ファイルの最初の行は、使用しているproto3構文を指定します。指定しない場合、プロトコル バッファ コンパイラは、 プロトタイプ 2を使用していると想定します。これは、ファイルの空でない、コメントでない最初の行である必要があります。
  • SearchRequestメッセージ定義では、このタイプのメッセージを含める 3 つのフィールド (名前と値のペア) を指定します。各フィールドには名前とタイプがあります。

フィールドタイプを指定する

前述の例では、すべてのフィールドはスカラー型です。つまり、2 つの整数 (page_numberresults_per_page) と文字列 ( query) です。他のメッセージ タイプなど、列挙型および複合タイプのフィールドを指定することもできます。

おすすめ

転載: blog.csdn.net/qq_44632658/article/details/130978314