FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Pokemon Goを支える技術: Protocol Bufferとは何なのか(2)

f:id:msyksphinz:20160723175322p:plain

msyksphinz.hatenablog.com

前回、Protocol Bufferを使って通信したいメッセージのテンプレートを作成するところまで行った。

次に、このテンプレートを使ってプログラム間の通信を行ってみる。 通信と言っても、テキストファイルを読み出した上でプロトコルバッファに格納していき、それをバイナリファイルとして出力、その後別のプログラムでそのバイナリファイルを呼び出して複合するという処理だ。

プロトコルバッファを用いてテキストファイルの送受信を行う

まずは例を参照しよう。GoogleのProtocol Bufferの公式サイトに載っている例が最も簡単だ。

Protocol Buffer Basics: C++  |  Protocol Buffers  |  Google Developers

これを参考に、自分でもプログラムを構築してみた。以下に登録した。

github.com

プロトコルバッファのメッセージ形式を定義する

メッセージ形式は前回のものと基本的には変わらないが、もう一つラッパーを加えることで、Personメッセージ構造体を複数定義できるように変更する。それがPersonList Message形式だ。

syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
}

message PersonList {
  repeated Person person = 1;
}

上記をPerson.protoとして保存してprotobufを使ってコンパイルC++のコードを生成する。

protoc --cpp_out ./ Person.proto

プロトコルバッファを使ってテキストをシリアライズ化する

次に、Googleのサンプルプログラムを用いて以下のようなプログラムを作成した。 名前と年齢が対になっているテキストファイルを読み込んで、プロトコルバッファに登録していく。

$ head Person.file
ipjdp5kD66EJn5zu        50
8zhebItQzHcYpXBV        49
VTPFIgHzLKgPiFb8        92
6bHeL4NUHPcdRsxB        61
dtBgzL2DvLJ4F4pb        91
FQkX3WNckgAcf5zR        82
Zka0XGfrzmrSQFLJ        14
Hgxrnc2kzPWgFCoY        4
DEnnFraVvZdQepCM        87
oYO1vdtmZPubJZVh        58
...

これをプロトコルバッファに登録するのが以下のコードだ。

  • write.cpp
  PersonList person_list;

  while (!ifs.eof()) {
    std::string str_name;
    int32_t     age;
    ifs >> str_name;
    ifs >> age;

    Person *person = person_list.add_person();

    // std::cout << str_name << '\n';
    // std::cout << age << '\n';

    person->set_name (str_name);
    person->set_age (age);
  }
  std::ofstream ofs;
  ofs.open ("person.dat", std::ios::out | std::ios::trunc | std::ios::binary);
  if (!person_list.SerializeToOstream(&ofs)) {
    std::cerr << "Failed to write address book." << std::endl;
    return -1;
  }

PersonListクラスのインスタンスを1つ生成し、そこでadd_person()を呼び出すことでPersonメッセージ1つ分を格納できるインスタンスのポインタが取得できる。 このポインタに対して情報の登録を行っていく、という訳だ。

プロトコルバッファへの登録プログラム(write.cpp)のコンパイル

コンパイルおよび実行は、以下のようにして行う。今回は、Person.fileに100000個のデータを格納して実行してみた。

$ g++ -std=c++11 write.cpp Person.pb.cc -o write `pkg-config --cflags --libs protobuf`
$ ./write Person.file
$ls 
...
-rw-rw-r-- 1 vagrant vagrant 2197936 Jul 24 13:59 person.dat
-rw-rw-r-- 1 vagrant vagrant 1990074 Jul 24 13:59 Person.file
...

person.datがプロトコルバッファから出力されたバイナリデータだが、データ量は削減されるどころか増えてしまった。データ量が少ないのか、データの質が良くないのか、まだ良く分からない。

プロトコルバッファのデータを読み込んでデシリアライズ化する

次にプロトコルバッファのデータを読み込み、バイナリからデシリアイライズして元のデータに戻してみる。

これもGoogleのサンプルプログラムを用い、以下のようなコードを作成した。

  • read.cpp
  std::fstream ifs;
  ifs.open(argv[1], std::ios::in | std::ios::binary);
  if (!person_list.ParseFromIstream(&ifs)) {
    std::cerr << "Failed to parse address book." << std::endl;
    return -1;
  }

  std::ofstream ofs;
  ofs.open("output.txt");

  for (int i = 0; i < person_list.person_size(); i++) {
    Person person = person_list.person(i);
    ofs << person.name () << ' ' << person.age ()  << '\n';
  }

ParseFromIstreamでファイルからプロトコルバッファのバイナリデータを読み込み、そして複合する。それぞれの複合されたデータはperson_size()個分のデータとして保持され、それらを一つずつテキストデータとして出力している。

プロトコルバッファからのデシリアライズプログラム(read.cpp)のコンパイル

$ g++ -std=c++11 read.cpp Person.pb.cc -o read `pkg-config --cflags --libs protobuf`
$ ./read person.dat
$ less output.txt
ipjdp5kD66EJn5zu 50
8zhebItQzHcYpXBV 49
VTPFIgHzLKgPiFb8 92
6bHeL4NUHPcdRsxB 61
dtBgzL2DvLJ4F4pb 91
FQkX3WNckgAcf5zR 82
Zka0XGfrzmrSQFLJ 14
Hgxrnc2kzPWgFCoY 4
DEnnFraVvZdQepCM 87
oYO1vdtmZPubJZVh 58
...

正しくデシリアライズ化された。

まとめ

Googleプロトコルバッファ protobuf の基本的な使い方については一通り学ぶことができた。ただしファイルサイズの問題や、速度の問題については未検証で、実際にこのライブラリを使うとなると、有用なデータを用いて検証しなければならないと思う。