gRPC関連でprotocol buffersを使うことが増えたのだが、protocol buffers自体にまだ慣れていないので調べてまとめる。
https://developers.google.com/protocol-buffers/docs/proto3
protocol buffers自体のおさらい
Googleが2008年にOSS化したプログラミング言語に中立なデータのシリアライズ形式である。(略称はprotobuf)
型情報は.proto
というIDLに書き、これを送信と受信の双方で事前共有しておく。こうすることによって余計な型情報を省いてバイナリに固めることができる。
この性質から、APIドキュメントとしても使える。「RESTっぽいJSON API + OpenAPIによる記述」とだいたい同じニーズをカバーする。(IDLを抜きにしてMessagePackやJSONと比較することはできない)
protobufを使った開発の順序はこんな感じ。
-
.proto
を書く (お好きなエディタをお使いください) -
protoc
コマンドで各言語用のシリアライズ/アンシリアライズのコードを吐き出す - 何らかの通信手段でシリアライズしたものをやり取りする
シリアライズしたものはバイナリフォーマット。HTTP上にprotobufのバイナリを流すことも可能だが、残念ながらIANAのMedia Typesには登録されていない。一応、expireしたproposalはあり、application/protobufを提案していたようだ。
ちなみに、ググるとyuguiさんの記事がいっぱいヒットする。とても勉強になるのでみんな読もう。私はあと100回読むと思う。
今さらProtocol Buffersと、手に馴染む道具の話 - Qiita
バージョンについて
proto3とproto2があり、混在させることが可能。
ただ、互換性問題などを考えた上でproto3が開発されたようなので、proto3を使っていけばいいと思う。機能自体もproto3の方が少なく、覚えることも少ない。この記事でも極力proto3のことだけを書く。
syntax = "proto3";
これをファイルの頭に書いておくとproto3として認識される。
ざっとsyntax
拡張子は.proto
を使う。勉強のために作ったprotoがここにあるので、ビルドまで試したい場合は参考にしてほしい。 https://github.com/hirak/protobuf-test/
syntax = "proto3";
package myapp;
import "google/api/annotations.proto";
import "google/type/date.proto";
import "google/protobuf/empty.proto";
message User {
uint64 id = 1;
string first_name = 2;
string family_name = 3;
Sex sex = 4;
uint32 age = 5 [ deprecated = true ];
google.type.Date birthday = 6;
enum Sex {
SEX_UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
OTHER = 3;
}
}
message UserList { repeated User users = 1; }
service UserService {
rpc Get(GetRequest) returns (User) {
option deprecated = false;
option (google.api.http) = {
get : "user"
};
}
rpc List(google.protobuf.Empty) returns (UserList) {}
}
message GetRequest { uint64 id = 1; }
主に、message
という構造体のようなものを定義していく。
更に、service
という定義を作ると、serverおよびclientの実装を吐き出してくれるprotocプラグインもある。(対応するプラグインを使わなければ無視されるだけ)
インデントを綺麗にする
clang-formatがおすすめ。https://clang.llvm.org/docs/ClangFormat.html
macOSならHomebrewでインストールできる。
$ brew install clang-format
-i
でインデントし直して上書きする。
$ clang-format -i path/to/xxx.proto
import定義
別のファイルに書いてある定義を読み込むことができる。
import "google/api/annotations.proto";
import "google/type/date.proto";
import "google/protobuf/empty.proto";
冒頭のサンプルだと、 google.protobuf.Empty
, google.type.Date
, google.api.http
はどこにも定義されていないが、これらの外部ファイルから読み込むことでコンパイルを通している。
importのファイルは、何もオプションを指定しなければカレントディレクトリから探され、 -IPATH
か--proto_path=PATH
で探索ディレクトリを複数指定することができる。
この辺の仕様は若干古めかしい。。まあ2008年の仕組みだからね。protoc
コマンド単体でビルドを組むのは無理ゲーなので、Makefileなどを組み合わせることになると思う。
なお、よく登場するこの辺のprotoは色々なリポジトリに点在している。git submoduleで寄せ集めてくる場合はimport pathを合わせるのが割と面倒くさい…。(bazelならうまくやってくれるのだろうか?未確認)
- google/api/xxx.proto → https://github.com/googleapis/googleapis/tree/master/google/api
- google/type/xxx.proto → https://github.com/googleapis/googleapis/tree/master/google/type
- google/protobuf/xxx.proto → https://github.com/protocolbuffers/protobuf/tree/master/src/google/protobuf
protobufの型
組み込みのスカラ型
スカラ型は一通り組み込まれている。
- bool
- string
- bytes
- double, float
- int32, int64
- uint32, uint64
- sint32, sint64
- 負数を効率よくシリアライズする
- fixed32, fixed64
- 巨大な数を効率よくシリアライズする
- sfixed32, sfixed64
ユーザー定義型
message
というキーワードで構造体のようなものを定義することができる。protocでコンパイルすると、大抵は言語のクラスや構造体としてコードが生成される。スカラ型や、他のユーザー定義の型を集約して構造を作っていく。
enum
列挙型も作れる。
なお、enumの最初の値がデフォルト値のゼロでなければならない。これはproto3の決まりなので必ず守ろう。特に使いみちがない場合はUNKNOWN = 0
などを入れておくこと。
値に使えるのは32bit 整数の範囲のみ。
こちらにもまとめた。
protocol buffers ENUM型 完全ガイド - Qiita
googleが用意している型の例
メジャーな概念であれば、大抵googleのリポジトリに存在していたりする。
-
google.protobuf.Empty ... 空オブジェクト。JSONにすると
{}
になる。 -
google.protobuf.Timestamp ... ナノ秒まで保持できる時刻型。JSONにすると
"1996-10-30T12:08:25Z"
という文字列になる。timezoneは保持しない。 -
google.protobuf.Duration ... 時刻じゃなくて時間。ナノ秒単位まで保存できる。JSONにすると
"3s"
というような秒数を表す文字列になる。 - google.type.Date ... 単純にY-M-Dの3点セットを保持する日付型。timezoneは持たない。生年月日に便利。
- google.type.TimeOfDay ... 単純にhours,minutes,seconds,nanosの4点セットを保持する時刻型。Timestampと違って日付を持たない。
- google.type.DayOfWeek ... 曜日のenum。
- google.type.Money ... currency_codeを含む通貨型。
- google.type.LatLng ... 緯度経度
- google.type.PostalAddress ... 住所
- google.type.Color ... RGBAで表す色の型
他にもgoogleapis/googleapisには色々な型が定義されているので、読むと勉強になる。
repeated
型の前に「repeated」というキーワードを書くと、配列のように複数個含められるようになる。
message UserList { repeated User users = 1; }
初期値は空のリスト。
map
連想配列のようなものも作ることができる。ただし、keyになりうるのはstringとint系、boolだけである。enumや独自のmessage型、floatやbytesはkeyにできない。
map<string, Project> projects = 3;
mapとrepeatedを混ぜることはできない。
oneof
messageフィールドのある範囲が、どれか一つしか含まれないという状態を宣言することができる。
C言語でいう共用体のようなものか。構造上は平べったいままなので、タグ番号は一意なものを振っていく必要がある。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
service
RPCを定義する構文。protocプラグインに、これを認識するものがなければ無視される。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
オプションについて
(力尽きたので別の記事にします)