Protocol Buffers

概要

Protocol Buffersのデザインの目的はシンプルさとパフォーマンスである。とりわけ、XMLより高速になるようデザインされている。GoogleはXMLとの比較で、3〜10倍小さく、20〜100倍高速であると主張している。Google自身が挙げている例では、XMLでは69バイト以上の物が Protocol Buffersでは28バイトであり、XMLのパースは5〜10マイクロ秒も必要だが、Protocol Buffersのパースは0.1〜0.2 マイクロ秒で完了するとしている。
Protocol BuffersはGoogleではデータの保存や構造化されたあらゆる種類の情報の交換で用いられている。Protocol Buffersサーバはカスタマイズされた遠隔手続き呼出し (RPC) に基づいており、Googleにおける全てのマシン間通信で実際に用いられている。
Protocol Buffersは既にあるサービス向けへの明確なRPCスタックを含まない部分を除いてFacebookによるThriftプロトコルに非常に似ている。Protocol Buffersはオープンソースであり、RPCスタックはこれらのギャップを埋めるために生まれてきた。
データ構造およびサービスはプロトコル定義ファイル (Proto Definition file (.proto)) でデザインされ、protocプログラムによりコンパイルされる。このコンパイル作業により目的のサービスに適合するコードが生成される。例として、「example.proto」は「example.pb.cc」および「example.pb.h」を生成し、この中には「example.proto」で定義されたメッセージおよびサービスのためのC++クラスが定義されている。
Protocol Buffersによりあらゆる種類のフォーマットをシリアライズすることができるようになる。公式にサポートされた実装では完全なリフレクションインタフェースが利用可能であり、XMLやJSONと同じくらい容易にシリアライズ機構を整備することができる。
Protocol Buffersの主な目的は「ネットワーク上の通信を高速化すること」ではあるが、その自己記述性の低さに起因するシンプルさおよび高速さによりC++クラスおよび構造体を用いたデータ格納からの有力な置き換え手段としても用いられている。

例

以下のように、polyline.protoファイルを作成する。
option java_package = "com.example";option java_outer_classname = "PolyLineProtos";// 点message Point {
  required int32 x = 1;
  required int32 y = 2;
  optional string label = 3;}// 線分message Line {
  required Point start = 1;
  required Point end = 2;
  optional string label = 3;}// 多角形message PolyLine {
  repeated Point point = 1;
  optional string label = 2;}
Pointメッセージは2つの必須フィールドxとyを定義している。labelフィールドはオプションである。それぞれのフィールドはタグ番号を持っている。このタグ番号はイコール記号「=」の後で定義される。(例: xはタグ番号 1 を持つ)。タグ番号は1以上の自然数であり、タグ番号でフィールドを識別するため、重複なく必ず指定しないといけない。
LineおよびPolylineメッセージは Protocol Buffersによる入れ子メッセージの例であり、どちらもPointを用いている。PolyLineは繰り返しフィールドpointを持ち、ベクトルとして扱う。
メッセージのフィールドにはrequired、optional、repeatedのどれかが指定可能。また、値の型としては、各種プリミティブ型の整数や浮動小数点数、bool、string、bytesを指定可能。optionalはデフォルト値を指定可能。repeatedはJavaではjava.util.Listになる。
この後、これ protocによりコンパイルし、PolyLineProtos.javaやpolyline.pb.hなどが生成される。
Javaの場合は、以下のように利用することができる。
import com.example.PolyLineProtos.Point;import com.example.PolyLineProtos.Line;import com.example.PolyLineProtos.PolyLine;Point start = Point.newBuilder().setX(10).setY(20).build();Point end = Point.newBuilder().setX(30).setY(40).build();Line line1 = Line.newBuilder().setStart(start).setEnd(end).setLabel("LINE1").build();byte[] bytes = line1.toByteArray();Line line2 = Line.parseFrom(bytes);PolyLine.Builder polyLineBuilder = PolyLine.newBuilder();for (int i = 0; i < 5; i++) {
    polyLineBuilder.addPoint(Point.newBuilder().setX(i).setY(i));}PolyLine polyLine = polyLineBuilder.build();
C++の場合は、以下のように利用することができる。
#include "polyline.pb.h"  // 'protoc polyline.proto'のコマンドラインからの呼び出しにより自動生成されるLine* createNewLine(const std::string& name) {
  Line* line = new Line;
  line->mutable_start()->set_x(10);
  line->mutable_start()->set_y(20);
  line->mutable_end()->set_x(30);
  line->mutable_end()->set_y(40);
  line->set_label(name);
  return line;}PolyLine* createNewPolyline() {
  PolyLine* polyline = new PolyLine;
  Point* point1 = polyline->add_point();
  point1->set_x(10);
  point1->set_y(10);
  Point* point2 = polyline->add_point();
  point2->set_x(10);
  point2->set_y(10);
  return polyline;}

エンコーディング

各フィールドごとに順番にエンコードされるが、各フィールドの先頭にはタグ番号と3ビットで表現された型の種類がつく。
int32などの整数値をエンコードした場合、必ず4バイト使うというわけではなく、0に近い数字はより少ないバイト数で表現される可変長数値表現を利用している。7ビット以下で表現できる整数は1バイトでエンコードされる。int32もint64もエンコード結果は同じ値になる。タグ番号＋3ビット型種別も同じく可変長数値表現でエンコードされる。ただしfixed32、fixed64などを使うと固定長でエンコードされる。またfloat、double などの浮動小数点数も固定長でエンコードされる。
文字列やバイト配列は、先頭に長さがつく形でエンコードされる。長さは上記の可変長数値表現を使っているため、長さの32ビット制限などはない。文字列はUTF-8で符号化する。
可変長数値表現は数値をリトルエンディアンで表現し7ビット単位で区切り、それぞれの最上位ビットにフラグを立てて8ビット単位のバイト列に変換する。最上位ビットは最終バイトだけ0として、それ以外は1を立てる。符号付き整数の場合はジグザグエンコーディングを使い、32ビットの場合は (n << 1) ^ (n >> 31) で正の数に変換してから可変長数値表現を使う。両方を併用した場合、-64〜63が1バイト、-8192〜8191が2バイトになり、32ビット整数は最長5バイトで表現される。