Protocol Buffersはデータ構造をシリアライズする技術になります。いわゆるXMLやJSONみたいなものではありますが、バイナリ形式なので「サイズが小さい」「シリアライズ、デシリアライズが高速」でprotoファイルによってメッセージ定義を「シンプルに記述可能」なのが売りです。またメッセージ定義はプログラムにコンパイルされるため、プログラムから利用しやすいのも利点です。
ということで今回はProtocol Buffersを触ってみました。
ダウンロード&インストール
以下のリンクで各言語のパッケージをダウンロードします。今回はPythonを使うので、protobuf-python-3.0.0.zipをダウンロードします。
Release Protocol Buffers v3.0.0 · google/protobuf
$ unzip protobuf-python-3.0.0.zip
$ cd protobuf-3.0.0
まずはコンパイラのprotocコマンドをビルド&インストール
$ ./configure
$ make
$ make check
$ sudo make install
$ sudo ldconfig # refresh shared library cache.
Protocol Buffersを使ってプログラムを記述
Python用のライブラリをインストール
$ cd python # protobuf-3.0.0/python
$ python setup.py build
$ python setup.py test
$ sudo python setup.py install
.protoファイルからソースコードをコンパイル
$ cd {PROJECT_PATH}
$ mkdir src
$ protoc --python_out=src ./addressbook.proto
addressbook.protoはこんな感じで(proto3にしているのはRubyで利用するため)
syntax = "proto3";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phone = 4;
}
Pythonスクリプトからは以下のようにして利用します。
#!/usr/bin/env python
import addressbook_pb2
import sys
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phone.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.HOME
sys.stdout.write(person.SerializeToString())
出力結果はバイナリなのでxxdコマンドなどで確認可能です。
$ ./hoge.py > fuga
$ xxd fuga
0000000: 0a08 4a6f 686e 2044 6f65 10d2 091a 106a ..John Doe.....j
0000010: 646f 6540 6578 616d 706c 652e 636f 6d22 doe@example.com"
0000020: 0c0a 0835 3535 2d34 3332 3110 01 ...555-4321..
https://github.com/google/protobuf/tree/master/ruby
$ sudo gem install google-protobuf
$ protoc --ruby_out=src addressbook.proto
Rubyスクリプトはこんな感じで
#!/usr/bin/env ruby
require 'google/protobuf'
require './addressbook_pb'
# person = Tutorial::Person.new()
# person.id = 1234
# person.name = "John Doe"
# person.email = "jdoe@example.com"
# phone = Tutorial::Person::PhoneNumber.new
# phone.number = "555-4321"
# phone.type = Tutorial::Person::PhoneType::HOME
# person.phone.push(phone)
STDIN.binmode
person = Tutorial::Person.decode(STDIN.read)
puts Tutorial::Person.encode_json(person)
あとはPythonの出力をRubyの入力に食わせるだけ
$ ./hoge.py | ./hoge.rb
{"name":"John Doe","id":1234,"email":"jdoe@example.com","phone":[{"number":"555-4321","type":"HOME"}]}
こんな感じで、同一のメッセージ定義からそれぞれの言語でコンパイルして、異なる言語間でメッセージをやり取りすることが出来ました。今回は同一ホストの入出力を使いましたが、異なるホスト間でネットワークを介してAPIでメッセージ交換をすることも可能ですし、有用そうです。
実際どれくらいサイズメリット、速度メリットがあるのか
以下のJSONで記述可能な構造体に対して確認してみました。
{
"name": "John Doe",
"id": 1234,
"email": "jdoe@example.com",
"phone": [
{
"number": "555-4321",
"type": "HOME"
}
]
}
まずはサイズメリット
$ ls -lh bin_out json
-rw-r--r-- 1 mtajitsu staff 45B 9 21 00:24 bin_out
-rw-r--r-- 1 mtajitsu staff 103B 9 21 00:24 json
2倍以上差がつきました。
性能差に関しては以下のスクリプトで検証しました。
#!/usr/bin/env ruby
require 'google/protobuf'
require './addressbook_pb'
require 'benchmark'
require 'json'
STDIN.binmode
bin_body = STDIN.read
# protocolbuffersで予め出力したファイルを標準入力に食わせる
result = Benchmark.realtime do
10_000_000.times do
Tutorial::Person.decode(bin_body)
end
end
puts "ProtocolBuffers #{result}s"
json_str = '{"name":"John Doe","id":1234,"email":"jdoe@example.com","phone":[{"number":"555-4321","type":"HOME"}]}'
result = Benchmark.realtime do
10_000_000.times do
JSON.parse(json_str)
end
end
puts "JSON #{result}s"
出力結果はこんな感じで、2倍くらいの性能差が出ています。
$ ./speed.rb < bin_out
ProtocolBuffers 41.555945s
JSON 82.674674s