MyEnigma

とある自律移動システムエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

ロボットエンジニアのためのProtocol buffers入門


Practical gRPC (English Edition)

目次

はじめに

ロボットをやっていると、

複数の言語で作られた、

複数のサービス間でデータをやりとりしたくなります。

ROSが使える場合は、ROSのTopicを使えば良いですが、

myenigma.hatenablog.com

どうしてもROSが使えない場合は、JSON+HTTPなどの他の方法を使う必要があります。

myenigma.hatenablog.com

 

今回の記事では、JSONと似たデータフォーマットの一種である、

Protocol buffersの概要と簡単な使い方を紹介したいと思います。

 

Protocol buffersとは?

Protocol buffersは、

Googleが提唱しているデータフォーマットです。

ja.wikipedia.org

developers.google.com

 

ある構造をもったデータを、バイナリ化し、それを復元できるため、

シリアライズフォーマットとも呼ばれます。

 

このデータフォーマットは、

2008年にGoogleが発表し、

それ以前から現在にいたるまで

Googleのサービス間の通信フォーマットとして、

実際の製品にも使用されているようです。

現在Googleでは30万個以上のprotoファイルが、

RPC用やデータ保存用として、使用されているとのことです。

 

このProtocol buffersのためのツール(コンパイラ)が

GoogleからOSSとして公開されています。

github.com

 

Protocol buffersの特徴

構造化されたデータのデータフォーマットとしては、

JSONや,

myenigma.hatenablog.com

Message packがありますが、

msgpack.org

Protocol buffersの特徴について簡単にまとめたいと思います。

 

様々なプラットフォームや言語で利用することができる

Protocol buffersは*.protoというファイル形式で、

構造化されたデータを定義します。

そして、

Protocol buffers用のコンパイラを使って、

protoファイルを入力として、

様々な言語用にコードを自動生成することができます。

 

下記の公式のGoogleのコンパイラでは、

github.com

C++, Java, Python, Objective-C, C#, JavaScript, Ruby, Go, PHP, Dartなどの言語用に、

コンパイラが準備されていますが、その他にも下記のリストにあるように、

様々な言語用のコンパイラが準備されています。

github.com

 

これらのコンパイラで生成されるコードは

OS非依存であるため、

様々なOSで利用することができます。

 

データサイズが小さい

構造化されたデータフォーマットの代表として、

XMLやJSONがありますが、

Protocol buffersはJSONに比べると

データサイズが小さくなることも大きな特徴です。

Googleのドキュメントによると、

Protocol buffersのデータサイズはXMLと比べると3-10倍データが小さいらしいです。

 

しかし、公式ドキュメントによると

Protocol Buffersは

あまり大きなデータを取り扱うには向いていないらしく、

具体的には1Mbyte以上のデータを扱いたい場合は、

別のフォーマットを使った方が良いとのことです。

 

エンコードやデコードが早い

Protocol buffersは、データのサイズが小さいだけでなく、

XMLに比べると、データのエンコードやデコードが

20-100倍高速になるとのことです。

 

データの構造を精密に規定できる

JSONは様々なデータ構造を表現できますが、

データの受信側がそのデータフォーマットが、

指定したフォーマットになっているか、判断が難しいです。

このような問題を解決するために、

JSONには、JSONのスキーマであるJSONスキーマがあります。

myenigma.hatenablog.com

しかし、Protocol buffersはすでにprotoファイルが、

スキーマとして、データ構造を精密に規定することができるため、

より安全性の高い、データのやりとりをすることができます。

 

データフォーマットに後方互換性がある。

Protocol buffersの最も重要な特徴に、

データフォーマットの後方互換性があるということです。

 

システムを開発していくと、

データフォーマットも変更が必要な場合が多いですが、

protoファイルのフィールドのID(タグナンバーとも呼ばれる)をユニークにして、

古いデータで使っていたIDをそのままにしておけば、

新しいprotoファイルから生成されたコードは、

古いフォーマットのデータをそのまま読むことができます。

新しいprotoファイルから削除されたデータは、

たとえ古いデータファイルに含まれていたとしても無視され、

新しいprotoファイルに定義されたデータのみを読み込むことができます。

 

またリファクタリングなどをして、

フィールド名を変更したい場合も、

Protocol buffersでは、このタグナンバーでデータを管理するため、

IDをそのままにして、フィールド名を変更すれば問題ありません。

 

例えば、ROSのトピックのmsgファイルは、

変更すると古いフォーマットのbagファイルは読み込めなくなります。

Protocol buffersでは、この後方互換性を保つ機能があるため、

データ構造の変更がしやすくなっています。

  

JSONに変換できる

Protocol buffers ver3から、

データは、MessageToJsonという関数で、

JSONに変換することができるようになりました。

 

デメリット

以上のように、Protocol buffersには、

色々良い所が多いですが、他のデータフォーマットに比べると、

下記のようにいくつか問題点もあります。

  • エンコードされたデータがバイナリなので、人間には読みにくい or 読めない

  • 事前にスキーマを定義するファイル(.proto)を用意する必要があり一手間かかる

  • 複数のメッセージを送信したいときは、Protocol buffersそのものにはデリミタが無いので、サイズなどをつけて送信する必要がある。

  • Protocol Buffersのデータフォーマットはあまりにも大きいデータを扱うのには適していない。

 

protoファイルを作成する時の注意点

下記はprotoファイルを作成する時の注意点です。

  • IDは基本的にすべてのメッセージに、固有の数値を入れる。

 この数値は1 ~ 536870911まで設定できるが、19000-19999はProtobufそのもの専用にreserveされている。

  • repeatedは配列のデータの前に指定する

  • reserved: すでに過去に使ったIDや、フィールド名を削除して、あとの人が使うと古いprotoコードを使った人のコードが壊れるので、reserved fieldで使わないようにすることができる。Enumにも使える

  • protoファイルのコメントはC++ styleで書くことができる。

  • protoファイルではEnumも使える。しかし、Enumの番号の開始は0始まりになる。

  • import文を使うことで、他のprotoファイルをimportすることができる

  • ネストされた型は、ドットでアクセスすることができる。

その他の詳しい説明は、下記のドキュメントを参照ください。

developers.google.com

 

ちなみに、

そうです。

protobufのコンパイラのインストール

それぞれのプラットフォームにおける

コンパイラのインストールは下記の通りです。

MacでHomebrewをって、公式のprotobufをインストール場合

下記でOKです。

$ brew install protobuf

 

ubuntuへの公式protobufのインストール

下記のようにapt-getでインストール可能です

$ sudo apt-get install libprotobuf-dev libprotoc-dev protobuf-compiler

しかし、現時点でUbuntu16.4ではapt-getでインストールした

protobufはv2系になっており、最新のv3系ではありません。

 

もし、v3系をインストールしたい場合は、

こちらの通り、v3系のバイナリzipをダウンロードし、

自分で/user/local/の下にバイナリを置く必要があります。

gist.github.com

 

Juliaでprotoファイルをコンパイルする

JuliaはGoogleの公式のコンパイラは対応していませんが、

こちらの3rd partyのOSSを使うことで、

protoファイルからjulia用のコードを生成することができます。

github.com

 

具体的には、juliaのREPLで

using Pkg;Pkg.add("ProtoBuf")

とし、同じくREPLで

using ProtoBuf;run(ProtoBuf.protoc(--julia_out=julia addressbook.proto))

とすれば、事前に作っておいたjuliaというディレクトリに、

protoファイルから生成されたJuliaコードが作られるはずです。

 

使い方

下記は、各言語における

Protocol buffersのデータ作成(ファイル出力)と

データ読み込み(ファイル入力)のサンプルコードです。

下記のすべてのコードは、下記のリポジトリで公開されています。

github.com

 

ちなみに、下記のサンプルコードでは、

様々な言語で生成されたデータファイルを、

それぞれ別の言語で読むこともできます。

 

C++

下記はC++によるデータ作成(ファイル出力)のサンプルコードです。

/**
 * Protocol buffer data writer in cpp
 *
 * @author Atsushi Sakai
 **/
#include <fstream>
#include <iostream>
#include <string>

#include "addressbook.pb.h"

using namespace std;

int main(void) {
  cout << "Start Protocol buffers writer sample" << endl;
  tutorial::AddressBook address_book;

  tutorial::Person *person1 = address_book.add_people();
  person1->set_id(1234);
  person1->set_name("John Doe");
  person1->set_email("jdoe@example.com");

  tutorial::Person::PhoneNumber *phone = person1->add_phones();
  phone->set_number("555-4321");
  phone->set_type(tutorial::Person::HOME);

  tutorial::Person *person2 = address_book.add_people();
  person2->set_id(4321);
  person2->set_name("Tom Ranger");
  person2->set_email("tranger@example.com");

  tutorial::Person::PhoneNumber *phone2 = person2->add_phones();
  phone2->set_number("555-4322");
  phone2->set_type(tutorial::Person::WORK);

  // Human readable print
  cout << address_book.DebugString() << endl;

  ofstream stream("pbdata_cpp.dat");
  if (!stream.bad()) {
    address_book.SerializeToOstream(&stream);
    stream.close();
  }

  cout << "Done" << endl;
}

下記はC++によるデータ読み込み(ファイル入力)のサンプルコードです。

/**
 * Protocol buffer data reader in cpp
 *
 * @author Atsushi Sakai
 **/
#include <fstream>
#include <iostream>
#include <string>

#include "addressbook.pb.h"

using namespace std;

int main(void) {
  cout << "Start Protocol buffers reader sample" << endl;
  tutorial::AddressBook address_book;

  // fstream input("./pbdata_cpp.dat", ios::in | ios::binary);
  // fstream input("../python/pbdata_py.dat", ios::in | ios::binary);
  fstream input("../julia/pbdata_julia.dat", ios::in | ios::binary);
  address_book.ParseFromIstream(&input);

  // Human readable print
  cout << address_book.DebugString() << endl;

  cout << "Done" << endl;
}

Python

下記はPythonによるデータ作成(ファイル出力)のサンプルコードです。

"""
Protocol buffer writer in Python
author: Atsushi Sakai
"""

import addressbook_pb2


def main():
    print("start!!")

    address_book = addressbook_pb2.AddressBook()

    person1 = address_book.people.add()
    person1.id = 1234
    person1.name = "John Doe"
    person1.email = "jdoe@example.com"
    # person1.no_such_field = 1  # raises AttributeError
    # person1.id = "1234"        # raises TypeError

    phone = person1.phones.add()
    phone.number = "555-4321"
    phone.type = addressbook_pb2.Person.HOME

    person2 = address_book.people.add()
    person2.id = 4321
    person2.name = "Tom Ranger"
    person2.email = "tranger@example.com"
    phone = person2.phones.add()
    phone.number = "555-4322"
    phone.type = addressbook_pb2.Person.WORK

    print(address_book)  # Human readable print

    # writing the data
    f = open("pbdata_py.dat", "wb")
    f.write(address_book.SerializeToString())
    f.close()

    print("done!!")


if __name__ == '__main__':
    main()

下記はPythonによるデータ読み込み(ファイル入力)のサンプルコードです。

"""
Protocol buffer reader in Python
author: Atsushi Sakai
"""

import addressbook_pb2


def main():
    """
    main func
    """
    print("start!!")

    address_book = addressbook_pb2.AddressBook()

    # Read the existing address book.
    # file_path = open("pbdata_py.dat", "rb")  # read data from python code
    # file_path = open("../cpp/pbdata_cpp.dat", "rb")  # read data from cpp code
    file_path = open("../julia/pbdata_julia.dat",
                     "rb")  # read data from cpp code

    address_book.ParseFromString(file_path.read())
    file_path.close()

    print(address_book)

    print("done!!")


if __name__ == '__main__':
    main()

Java

下記はJavaによるデータ作成(ファイル出力)のサンプルコードです。

package writer;

import java.io.FileOutputStream;

import tutorial.Addressbook.AddressBook;
import tutorial.Addressbook.Person;
import tutorial.Addressbook.Person.PhoneNumber;
import static tutorial.Addressbook.Person.PhoneType.*;

public class Writer {

    public static void main(String[] args) throws Exception {
        System.out.println("Protocol buffer writer start!!");

        AddressBook.Builder addressBook = AddressBook.newBuilder();

        Person.Builder person1 = Person.newBuilder();
        person1.setId(1234);
        person1.setName("John Doe");
        person1.setEmail("jdoe@example.com");

        PhoneNumber.Builder phone = PhoneNumber.newBuilder()
                                    .setNumber("555-4321")
                                    .setType(HOME);
        person1.addPhones(phone.build());

        Person.Builder person2 = Person.newBuilder();
        person2.setId(4321);
        person2.setName("Tom Ranger");
        person2.setEmail("tranger@example.com");

        PhoneNumber.Builder phone2 = PhoneNumber.newBuilder()
                                    .setNumber("555-4322")
                                    .setType(WORK);
        person2.addPhones(phone2.build());

        addressBook.addPeople(person1.build());
        addressBook.addPeople(person2.build());


        System.out.println(addressBook); // Human readable print

        FileOutputStream output = new FileOutputStream("pbdata_java.dat");
        addressBook.build().writeTo(output);
        output.close();

        System.out.println("Protocol buffer writer done!!");
        

    }

}

下記はJavaによるデータ読み込み(ファイル入力)のサンプルコードです。

package reader;

import java.io.FileInputStream;

import tutorial.Addressbook.AddressBook;

public class Reader {

    public static void main(String[] args) throws Exception {
        System.out.println("Protocol buffer reader start!!");

        AddressBook addressBook =
            AddressBook.parseFrom(new FileInputStream("pbdata_java.dat"));

        System.out.println(addressBook); // Human readable print

        System.out.println("Protocol buffer reader done!!");
    }

}

Julia

下記はJuliaによるデータ作成(ファイル出力)のサンプルコードです。

"
    Protocol buffer data writer in Julia 
    author: Atsushi Sakai
"

using ProtoBuf

include("tutorial.jl")
include("addressbook_pb.jl")

function main()
    println(PROGRAM_FILE," start!!")

    address_book = AddressBook(
                               people=Person[]
                              )

    person1 = Person(
                     id = 1234,
                     name = "John Doe",
                     email = "jdoe@example.com",
                     phones = Person_PhoneNumber[]
                    )

    phone = Person_PhoneNumber(
                               number="555-4321",
                               _type = __enum_Person_PhoneType().HOME
                              )
    push!(person1.phones, phone)

    push!(address_book.people, person1)
 
    
    person2 = Person(
                     id = 4321,
                     name = "Tom Ranger",
                     email = "tranger@example.com",
                     phones = Person_PhoneNumber[]
                    )

    phone = Person_PhoneNumber(
                               number="555-4322",
                               _type = __enum_Person_PhoneType().WORK
                              )
    push!(person2.phones, phone)

    push!(address_book.people, person2)
 

    println(address_book)  # Human readable print

    iob = PipeBuffer();
    writeproto(iob, address_book)

    open( "pbdata_julia.dat", "w" ) do fp
        write( fp, iob )
    end

    println(PROGRAM_FILE," Done!!")
end


main()

下記はJuliaによるデータ読み込み(ファイル入力)のサンプルコードです。

"
    Protocol buffer data reader in Julia 
    author: Atsushi Sakai
"

using ProtoBuf

include("tutorial.jl")
include("addressbook_pb.jl")

function main()
    println(PROGRAM_FILE," start!!")

    stream = UInt8[]
    # open( "pbdata_julia.dat", "r" ) do fp
    # open( "../cpp/pbdata_cpp.dat", "r" ) do fp
    open( "../python/pbdata_py.dat", "r" ) do fp
        stream = read( fp )
    end
    iob = PipeBuffer(stream)
    address_book = readproto(iob, AddressBook())

    println(address_book)  # Human readable print

    println(PROGRAM_FILE," Done!!")
end


main()

 

protoファイルのスタイルガイド

下記は、公式ドキュメントによる

protoファイルを作成する際のスタイルガイドです。

・一行は80文字以下

・インデントは2スペース

・ファイル名は、小文字のスネークケース

・メッセージの名前はキャメルケース

・フィールドはスネークケース

・repeatedがついた配列の名前は、複数形にする。

・Enumに名前はキャメルケース、Enum typeの名前には大文字のアンダースコアを使う

・Enumの最初のtypeはid=0でunspecified(指定無し)としたほうが良い。

・RPCのサービスを定義するときは、RPCのサービス名もRPCの関数もキャメルケースにする。

 

Protocol buffersと一緒に使うと便利なツール

protoc-gen-lint

protoファイルの自動フォーマットツール(Linter)です。

github.com

   

protoc-gen-doc

protoファイルのドキュメントを自動生成してくれるツールです。

github.com

 

使い方としては、dockerを使って、

$ docker pull pseudomuto/protoc-gen-doc

として、docker imageをpullして、

$docker run --rm -v $(pwd):/out -v $(pwd):/protos pseudomuto/protoc-gen-doc

とすれば、current directoryのprotoファイルから、

下記のようなhtmlののドキュメントを自動生成できます。

f:id:meison_amsl:20190615171649p:plain

 

ProtoBuf3で変わったこと

Protocol buffersが公開されたときは、ver2でしたが、

現時点での最新のProtocol buffersはver3になっています。

ver2とver3は互換性のあるプロトコルではないので、

proto3とproto2は混在させることはできません。

 

Proto2からProto3になって、できなくなったこととしては、

  • requiredやoptionalがなくなった (すべてoptionalになった)

  • 各フィールドのデフォルト値設定できなくなった。

などがあります。

一方、Proto3でできるようになったこととしては、

  • MessageToJsonでJSONに変更できる。

があります。

 

Protocol buffersを通信のデータフォーマットとして利用しているプロジェクト

代表的なものとして、下記があります。

grpc.io

github.com

github.com

 

参考資料

speakerdeck.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com


Practical gRPC (English Edition)

 

MyEnigma Supporters

もしこの記事が参考になり、

ブログをサポートしたいと思われた方は、

こちらからよろしくお願いします。

myenigma.hatenablog.com