Skip to content

Instantly share code, notes, and snippets.

@kitasuke
Last active May 8, 2021 14:22
Show Gist options
  • Save kitasuke/3820418e6eb340edc09c17983a7784a3 to your computer and use it in GitHub Desktop.
Save kitasuke/3820418e6eb340edc09c17983a7784a3 to your computer and use it in GitHub Desktop.

Protocol Buffersで高速な通信を型安全に実現する

最近AppleがProtocol Buffersの公式プラグインをGitHubに公開して話題になったので、実際に使ってみました。APIの通信が速くなったり、型安全に通信処理を書ける など、メリットが多いと感じたので簡単に紹介します。

他の事例だと、iOSオールスターズ2で発表された「これから始めるProtocol Buffers導入」が非常に参考になると思うので併せて読んでみてください。

この記事ではProtocol Buffersの概要について書いています。別途こちらの記事で使い方について サンプルを使って説明している ので、併せて読んでみてください。

Protocol Buffersとは

Protocol Buffersとは、言語やプラットフォームに依存せずに構造化されたデータをシリアライズする拡張可能なメカニズムです。XMLと似たような仕組みですが、それより軽量で、早く、シンプルに使えます。 どのような構造のデータが欲しいかを一度定義すれば、自動生成されたソースコードから簡単にデータにアクセスできます。

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

https://developers.google.com/protocol-buffers/

Protocol BuffersはGoogleによって2008年に開発されたもので、Google内でも使用されているそうです。またgRPCでも使われていて、こちらはObjective-Cのソースコードを自動生成して利用しているようです。

対応している言語

ほとんどの言語に対応している ので、その点に関しては特に困らないと思います。C++, Go, Java, Python, Ruby, C#, Objective-C, Javascript, PHP, Swiftなど良く使われている言語に対応しています。ここで挙げた例は一部なので、詳しい内容はこちらを参照してください。

仕組み

.protoファイルにシリアライズするデータ構造を定義します。メッセージタイプという形式に従って、それぞれの構造を下記のように記述します。

message Token {
  string accessToken = 1;
}

// GET - Response
message GetTokenResponse {
  Token token = 1;
}

// POST - Request
message PostTokenRequest {
  string accessToken = 1;
}

// POST - Response
message PostTokenResponse {
  Token token = 1;
}

上記のtoken.protoファイルをprotocコマンドでコンパイルすると、token.pb.swiftというファイルが生成されて、上記のメッセージタイプがstructTokenに変換されます。この生成されたファイルには、上記のprotoファイルで定義されたプロパティはもちろんのこと、serializeProtobuf(), serializeJSON()init(protobuf:), init(json:)といった シリアライズ・デシリアライズ用のメソッドSwiftProtobuf.Messageプロトコルに用意されています。下記が生成されたswiftのソースコードです(一部のみ抜粋)。

struct  Token: SwiftProtobuf.Message {
    var accessToken: String = ""

    init() {}
}

struct  GetTokenResponse: SwiftProtobuf.Message {
    init() {}

    var token:  Token {...}
}

struct  PostTokenRequest: SwiftProtobuf.Message {
    var accessToken: String = ""

    init() {}
}

struct  PostTokenResponse: SwiftProtobuf.Message {
    init() {}

    var token:  Token {...}
}

他には、APIの通信で利用する リクエスト型とレスポンス型structも生成されます。例えばPOSTでHTTPリクエストを送る際は、PostTokenRequest型のオブジェクトをserializeProtobuf()Data型にシリアライズして、そのバイナリをbodyにセットして送信します。レスポンスを受け取る場合は、受信したData型のバイナリをinit(protobuf:)PostTokenResponse型にデシリアライズします。この辺りの処理は型を意識して書ける ので、JSONに比べて非常に書きやすいと思います。

対応している型

こちらに記載されている通りで、ほとんどの型が使えます。

Proto type Swift type
int32 Int32
sint32 Int32
sfixed32 Int32
uint32 UInt32
fixed32 UInt32
int64 Int64
sint64 Int64
sfixed64 Int64
uint64 UInt64
fixed64 UInt64
bool Bool
float Float
double Double
string String
bytes Data

またenumや独自の型も使えるので、一通りのことが実現できると思います。

カスタマイズ

packageでネームスペースを付けたり、structのアクセスコントロールは調整できたり、特定の言語の場合のみ変数名を変えたりなどの調整も可能です。

Protocol Buffersの利点

データ定義の一元管理

.protoファイルから各言語のソースコードを自動生成するので、プラットフォーム間で定義を統一できます。例えば、サーバサイドで意図しない型の変更に対してのクライアント側での対応漏れや、サーバサイドとAndroid側で変更したことをiOS側で対応が漏れるなどの問題を防げます。

また、protoc-gen-docを使えば MarkdownやHTML形式のドキュメントを生成できる ので、.protoファイルから必要な情報を全て出力できます。

型安全な通信処理

リクエスト型もレスポンス型も定義されているので、型安全にデータにアクセスできます 。特に状態を表す場合などに、Intではなくswiftenumでアクセスできるのが便利です。シリアライズ・デシリアライズも実装されているので、オブジェクトをマッピングするライブラリが不要になります(Protocol Buffersを使わないモデルに対しては必要)。また、APIの通信もContent-TypeAcceptヘッダーをapplication/protbufにして、bodyにバイナリデータをセットして送信するだけなので、通信処理の実装もシンプルになります

通信処理の高速化

送信するデータサイズが小さくできたり、エンコード・デコード処理の負荷が小さくなるので、通信自体が高速になります。具体的にJSONよりどれほど速くなるかのベンチマーク調査はしていないので、気になる方はこちらの記事を参考にしてください。

公式のサイトでもパファーマンスについて記載されていますが、XMLとの比較しか無いです。

バージョニング

バージョニングも考慮して設計されています。新しいフィールドを追加しても、そのフィールドはオプショナルとして扱われます。 ただし、既存のフィールドの型を変更したり、フィールドのナンバリングを変えるとエラーになるので注意が必要です。

懸念点

バイナリの可読性

サーバとの通信で受け取るバイナリデータは中身を見ても良く分かりません。JSONと比べると格段に可読性が低くなります。その場合はログにString.init(data:encoding:)を使って文字列に変換して表示するか、開発時のみJSONで受け取るようにする などの対策が必要になります。自分はバイナリデータについては全く理解してないですが、こちらの記事を読めば少しは分かるようになるのかもしれません。

安定バージョンが無い(swift-protobufプラグイン)

プレリリースという状態なので、破壊的な変更もまだ多く、追加機能の実装も盛んに行われてますここで議論されているように、まだツールとして公開するには早いという状態なようです。それを考慮すると、プロダクションで使うには少々不安な面もあります。

代替案として、.protoファイルから生成されたソースコードでもJSONを扱えるので、生成されたファイルを使いつつも通信だけJSONにするのも可能です。

コンパイル時の言語バージョン

.protoファイルをコンパイルするprotocコマンドが使用している言語バージョンと、プロジェクトで使用している言語バージョンが一致しているとは限りません。例えばSwiftなどまだ更新頻度が多い言語は、注意しないと 突然コンパイル出来なくなるという可能性 もあります。この問題はProtocol Buffersに限らず、コードを自動生成するツールを使う上で細心の注意を払うポイントです。

SwiftではなくObjective-Cでソースコード生成するのも解決策の一つかもしれません。

WebブラウザでのJavascript

Javascriptはバイナリの扱うのに苦労する言語らしいです。その場合は無理せずに、素直にJSONで通信するのが良いかもしれません。

まとめ

少し使ってみた感想として、導入コストはそれなりにあると思いますが、それ以上の効果があると思いました。やはりリクエストもレスポンスも型があり、型安全にアクセスできるのがSwiftとの相性が良くて 嬉しいです。また型がはっきりしているおかげで、ランタイムエラーではなくコンパイルエラーでミスを防げる のも良い点です。今後も積極的にProtocol Buffersを使っていきたいと思いました。

具体的な使い方については、こちらの記事サンプルを使いながら説明している ので、併せて読んでみてください。

おまけ

swift-protobufの開発はGoogleのエンジニアも 参加しています。主なコミッターはAppleとGoogleのエンジニアで半々のようです(現時点で)。Appleのオーガナイゼーションなのに面白い構成です。実は、Googleの方はProtocol Buffers自体のコミッター でもありProtocol Buffersについて精通しているので、このような構成になっているのだと思います。

今回は触れませんでしたが、protobuf-swiftという非公式のプラグインがあり、こちらは昔からあるので機能が充実しています。しかし、公式のプラグインの方が 仕組みを理解している人達で開発している ので、将来的には良くなると個人的には思っています。

プラグインはコントリビュートが歓迎なようで、自分のPRもマージしてもらえました🎉 このPRでは、structinternalにしても中のプロパティやメソッドがpublicになっていたので、そのメンバーのアクセスコントロールをstructに合わせるという変更をしました。コードジェネレーター側のソースコードを修正したので手間がかかりますが、一旦PRを出せばやり方を親切に教えてくれるので、不足している機能を見つけたらコントリビュートすれば良いかと思います。

参考資料

https://developers.google.com/protocol-buffers/
https://github.com/apple/swift-protobuf
https://github.com/google/protobuf
https://speakerdeck.com/kyoheig3/protocol-buffers
https://speakerdeck.com/kyoheig3/ca-dot-swift
http://www.grpc.io/docs/
https://github.com/estan/protoc-gen-doc
http://qiita.com/tayama0324/items/499a5fed2a1c8479a5cf
http://qiita.com/aiueo4u/items/38195248a29e9ff719c7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment