56
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Protocol Buffers: ざっくりとした入門

この記事について

Protocol Buffers(protobuf) の概要、基本的な使い方、*.proto ファイルの書き方についてざっくりまとめます。

  • 検証環境
    • OS: Windows 10
    • 言語: Go 1.13

Protocol Buffers とは

すでにたくさん情報があるので要点だけ記載。

  • データをシリアライズ(マーシャル)するためのフォーマット、メカニズム
    • クライアント/サーバー間の通信や、データの永続化などに用いる
    • 元々は Google が開発
  • IDL でメッセージ(データ構造)を定義
  • 様々な言語とプラットフォームで利用可能
    • iOS や Android でも利用可能
  • バイナリ形式にシリアライズする
    • XML よりもサイズが小さくなり、また、高速
  • シリアライズ/デシリアライズ をするためのコードが自動生成される
  • その他
    • 既存のプログラムに影響を与えずにメッセージを更新(フィールドの追加)が可能

バージョン

  • 最新バージョンは 3 (proto3と呼ばれる)
  • proto3 と、前バージョンの proto2 は、完全には互換性が無い

基本的な使い方

1. Protocol buffers コンパイラ(protoc)をインストール

ダウンロードサイトから自分の環境にあった protoc をダウンロード。今回は v3.11.4 の protoc-3.11.4-win64.zip をダウンロードして展開した。

注意事項:

  • 展開した bin フォルダにパスを通すこと
  • zip に含まれる include フォルダも展開すること(protoc が参照するファイルが入ってるため)

いちおう、バージョンの確認。

> protoc --version
libprotoc 3.11.4

2. 必要に応じてプラグインをインストール

Go言語の場合は protoc に対するプラグインが必要なのでインストールする。

> go get google.golang.org/protobuf/cmd/protoc-gen-go
> protoc-gen-go --version
protoc-gen-go v1.21.0-devel

なお、ドキュメントには go install を使うよう書かれてたが、自分の環境ではエラーになったので go get にした。

※ gRPC のスタブコードの生成についてはProtocol Buffers用 Go言語APIの APIv1 と APIv2 の差異 を参照。

3. *.proto ファイルを作成

*.proto ファイルでメッセージの定義を行う。
以下は参考用にGo言語用のチュートリアル内の addressbook.proto にコメントを入れたもの。

addressbook.proto

// Protocol Buffers のバージョンを指定する。省略すると proto2 と見なされる。
syntax = "proto3";

// package は、プロジェクト間での名前衝突を防ぐためのパッケージ名。
// このパッケージ名は各言語に応じた解釈が行われる。
// (例)
//   C++    : C++の名前空間になる
//   C#     : パスカル形式に変換後にC#の名前空間になる(csharp_namespaceの指定が無い場合)
//   Go     : Goのパッケージ名になる(go_packageの指定が無い場合)
//   Python : 無視される
package addresspb;

// 他の *.proto ファイルの定義された型などを読み込みたい場合は import を使う
// 以下の "google/..." は、protoc に含まれる include ディレクトリ配下を指している。
import "google/protobuf/timestamp.proto";

// option は、特定のコンテキストで解釈される。すべてのリストは以下のファイルに記載されてる。
// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto
//
// go_package は生成される *.pb.go ファイルのパッケージを指定する。
// 無くてもコンパイルできるが警告が出る。
// なお、セミコロンを付けてパッケージのインポートパスとパッケージ名を別々に指定することも可能。
// 
// (例)
//   option go_package = "github.com/hoge/fuga";         // セミコロン無し
//   option go_package = "github.com/hoge/fuga;fuga";    // セミコロンあり
option go_package = ".;addresspb";

// メッセージ(Person)の定義
message Person {
    // メッセージのフィールド。
    // 各フィールドの識別子として 1, 2... というフィールド番号(タグ)が必要。
    // → シリアライズされたデータでは、フィールド番号でフィールドを識別するため。
    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は配列(要素の数は任意。0個でもよい。)
    repeated PhoneNumber phones = 4;

    // Import した *.proto ファイルで定義された型
    google.protobuf.Timestamp last_updated = 5;
}

// メッセージ(AddressBook)の定義
// AddressBook は複数の Person を含む。
message AddressBook {
    repeated Person people = 1;
}

4. *.proto ファイルをコンパイル

コマンドプロンプトで *.proto ファイルのあるディレクトリに移動して以下を実行。今回の場合 addressbook.pb.go が生成される。

protoc addressbook.proto --go_out=.\

5. 生成されたコードを用いてシリアライズ/デシリアライズを行う

生成された *.pb.go には、*.proto で定義したメッセージ対応する struct やその Getter が定義される。

addressbook.pb.go
type Person struct {
    // 中略
    Name  string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Id    int32  `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
    Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
    Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
    LastUpdated *timestamp.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
}

func (x *Person) GetName() string { /* 中略 */ }
func (x *Person) GetId() int32 { /* 中略 */ } 
func (x *Person) GetEmail() string { /* 中略 */ }
func (x *Person) GetPhones() []*Person_PhoneNumber { /* 中略 */ }
func (x *Person) GetLastUpdated() *timestamp.Timestamp { /* 中略 */ }

これらのメッセージに対して、github.com/golang/protobuf/proto パッケージの Marshal(), Unmarshal() 関数を用いてシリアライズ/デシリアライズを行う。

コードはこの辺りを参照。

ハマったところ

  • *.pb.go が生成される場所
    *.proto ファイルで option go_package = "github.com/hoge/fuga"; というようにパッケージのフルパスを指定して protoc を実行すると、 --go_out オプションで指定したディレクトリを起点としてgithub.com/hoge/fuga というディレクトリが作成されて、そこに *.pb.go が生成される。自分が生成したい場所を意識して --go_out または go_package の指定の仕方を調整する必要あり。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
56
Help us understand the problem. What are the problem?