Practical gRPC (English Edition)
目次
- 目次
- はじめに
- Protocol buffersとは?
- Protocol buffersの特徴
- protoファイルを作成する時の注意点
- protobufのコンパイラのインストール
- 使い方
- protoファイルのスタイルガイド
- Protocol buffersと一緒に使うと便利なツール
- ProtoBuf3で変わったこと
- Protocol buffersを通信のデータフォーマットとして利用しているプロジェクト
- 参考資料
- MyEnigma Supporters
はじめに
ロボットをやっていると、
複数の言語で作られた、
複数のサービス間でデータをやりとりしたくなります。
ROSが使える場合は、ROSのTopicを使えば良いですが、
どうしてもROSが使えない場合は、JSON+HTTPなどの他の方法を使う必要があります。
今回の記事では、JSONと似たデータフォーマットの一種である、
Protocol buffersの概要と簡単な使い方を紹介したいと思います。
Protocol buffersとは?
Protocol buffersは、
Googleが提唱しているデータフォーマットです。
ある構造をもったデータを、バイナリ化し、それを復元できるため、
シリアライズフォーマットとも呼ばれます。
このデータフォーマットは、
2008年にGoogleが発表し、
それ以前から現在にいたるまで
Googleのサービス間の通信フォーマットとして、
実際の製品にも使用されているようです。
現在Googleでは30万個以上のprotoファイルが、
RPC用やデータ保存用として、使用されているとのことです。
このProtocol buffersのためのツール(コンパイラ)が
GoogleからOSSとして公開されています。
Protocol buffersの特徴
構造化されたデータのデータフォーマットとしては、
JSONや,
Message packがありますが、
Protocol buffersの特徴について簡単にまとめたいと思います。
様々なプラットフォームや言語で利用することができる
Protocol buffersは*.protoというファイル形式で、
構造化されたデータを定義します。
そして、
Protocol buffers用のコンパイラを使って、
protoファイルを入力として、
様々な言語用にコードを自動生成することができます。
下記の公式のGoogleのコンパイラでは、
C++, Java, Python, Objective-C, C#, JavaScript, Ruby, Go, PHP, Dartなどの言語用に、
コンパイラが準備されていますが、その他にも下記のリストにあるように、
様々な言語用のコンパイラが準備されています。
これらのコンパイラで生成されるコードは
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スキーマがあります。
しかし、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することができる
ネストされた型は、ドットでアクセスすることができる。
その他の詳しい説明は、下記のドキュメントを参照ください。
ちなみに、
ROSのmsgファイルは、msgファイル用のgitリポジトリで管理する場合が多いけど、やっぱりprotoファイルも専用のgitリポジトリで管理するのが多いのか。:gRPCとProtocol Buffers 3とKubernetesとEnvoy - Kekeの日記 https://t.co/DF95PJFvRX
— Atsushi Sakai (@Atsushi_twi) 2019年6月15日
そうです。
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/の下にバイナリを置く必要があります。
Juliaでprotoファイルをコンパイルする
JuliaはGoogleの公式のコンパイラは対応していませんが、
こちらの3rd partyのOSSを使うことで、
protoファイルからjulia用のコードを生成することができます。
具体的には、juliaのREPLで
using Pkg;Pkg.add("ProtoBuf")
とし、同じくREPLで
using ProtoBuf;run(ProtoBuf.protoc(
--julia_out=julia addressbook.proto
))
とすれば、事前に作っておいたjuliaというディレクトリに、
protoファイルから生成されたJuliaコードが作られるはずです。
使い方
下記は、各言語における
Protocol buffersのデータ作成(ファイル出力)と
データ読み込み(ファイル入力)のサンプルコードです。
下記のすべてのコードは、下記のリポジトリで公開されています。
ちなみに、下記のサンプルコードでは、
様々な言語で生成されたデータファイルを、
それぞれ別の言語で読むこともできます。
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)です。
protoc-gen-doc
protoファイルのドキュメントを自動生成してくれるツールです。
使い方としては、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ののドキュメントを自動生成できます。
ProtoBuf3で変わったこと
Protocol buffersが公開されたときは、ver2でしたが、
現時点での最新のProtocol buffersはver3になっています。
ver2とver3は互換性のあるプロトコルではないので、
proto3とproto2は混在させることはできません。
Proto2からProto3になって、できなくなったこととしては、
requiredやoptionalがなくなった (すべてoptionalになった)
各フィールドのデフォルト値設定できなくなった。
などがあります。
一方、Proto3でできるようになったこととしては、
- MessageToJsonでJSONに変更できる。
があります。
Protocol buffersを通信のデータフォーマットとして利用しているプロジェクト
代表的なものとして、下記があります。
参考資料
Practical gRPC (English Edition)
MyEnigma Supporters
もしこの記事が参考になり、
ブログをサポートしたいと思われた方は、
こちらからよろしくお願いします。