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