Protocol buffer kita altcon header

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

北裕介と申します。メルカリというサンフランシスコに拠点を置く日本企業で働いています。eBayのようなマーケットプレイスアプリを作っていて、アメリカのユーザーに向けて国際化対応をしています。

今日は大きなメリットを享受できるProtocol Bufferによって得られた知見を共有したいと思います。

Web APIs

TwitterやGitHubクライアントのように、多くのアプリではネットワーク通信が行われています。たいていの場合、APIを呼ぶと、サーバーからJSONが返ってきます。これはデータをシリアライズするのに当たり前な方法だと思われがちですが、これは Swiftらしくないな と思いながらSwiftのコードを書いています。

次のように JSONオブジェクトをData型 に変換して、HTTPリクエストを送信する例を見てみましょう。


let json = ["id": 1]
let data = try? JSONSerialization.data(
    withJSONObject: json,
    options: []
)

var request = URLRequest(url: url)
request.httpBody = data

このリクエストは、userIdを送ると、サーバーからユーザ情報を取得するものとなっています。keyはString型で、ID自身はInt型です。これをData型に変換し、httpBodyにセットします。

レスポンスはURLSessionを介してData型からJSONオブジェクトへ変換されます。

記事の更新情報を受け取る


{"user": {"name": "Yusuke"}}
URLSession.shared.dataTask(with: request) { (data, _, _) in
    let json = (try? JSONSerialization.jsonObject(
        with: data!,
        options: []
    )) as? [String: Any]
    let user = json?["user"] as? [String: Any]
    let name = user?["name"] as? String
}

ここで、IDをミスタイプしたり、間違うと失敗します。Swiftはすべてが型の世界です。JSONには Any 型を使われてしまうので、Swiftらしくないと思っています。

Protocol Buffers

Protocol Buffer(Protobuf)は、2008年に開発されたシリアライゼーション用のフォーマットです。

Example

先程の例をProtobufで見てみましょう。Protobufでは自分で型をカスタムして定義することができます。


custom type
let userRequest = UserRequest.with {
    $0.id = 1
}
let body = try? userRequest.serializedData()

var request = URLRequest(url: url)
request.httpBody = body

keyの名前をStringリテラルで書く必要はありません。Protobufには serializedData() というシリアライズメソッドがあります。これを呼ぶことで、httpBodyにセットできます。

レスポンスを見てみると、Protobufを使うことで、 UserResponse 型を用いることができます。UserResponse型は私が定義したものです。UserResponse型には name プロパティを持った User 型を持たせています。Data型をUserResponse型にデシリアライズするだけで、プロパティにアクセスできるようになります。


UserResponse(user: User(name: "Yusuke"))
URLSession.shared.dataTask(with: request) { (data, _, _) in
    // custom type
    guard let response = try? UserResponse(serializedData: data!) else {
        // error
        return
    }
    let user = response.user
    let name = user.name
}

Protobufには型があるので、よりSwiftらしく書けます。

使い方

Protobufを使うには、Protobufのコンパイラ(最新版は3.3)をインストールします。Appleから公式にリリースされているSwift用のプラグインも必要です。

ProtobufはObjective-CとSwift両方をサポートしています。両方同時に使うこともできますよ!

Protobufの実装にはやることが3つあります。

  1. Message型の定義
  2. コード生成
  3. シリアライズ/デシリアライズ

Message型は .proto ファイル内のデータ構造を定義したkey-valueベースのコード

iOSConのプロフィールページを例に、 .protoファイルを作ってみましょう。このファイルには、ユーザーのID、名前、紹介文へのURL、ユーザータイプがあります。どのフィールドにも型(intやstring, enum)があります。


talk.proto syntax = "proto3";

import "user.proto";

message Talk {
    int32 id = 1;
    string title = 2;
    string desc = 3;
    User speaker = 4;
    repeated string tags = 5; // Array
}

talk.protoファイルもあります。 これにはID、タイトル、説明、ユーザータイプ、およびタグを持っています。これは似ていますが、User型を使っているので、user.protoを一番上でインポートし、Protobuf自体のバージョンを指定しなければなりません(最新バージョンは無料です)。なので、proto3と書きます。

Protobufコンパイラは .proto ファイルからすべてのコードを生成する

Protobufは .proto ファイルからすべてのコードを生成します。

サポートされている基本的な型には、int, Bool, string, Dictionaryがあります。C++, JavaScript, Ruby, Rustのような他の言語もサポートしています。

以下はAppleによって公式にサポートされているSwift版の特徴です。

  • structとなっており、データ構造を持つ
  • structであり、 class ではない。
  • enumを定義できるが、RawValue は常に int 型となる。

どのプロパティにもデフォルト値があります。これはプロパティ自体はOptional型を持てないことになります。usersのnameプロパティをチェックする際、それが空のStringかそうでないかを確認する必要があります。Optionalではなく、空のStringがデフォルト値となるからです。

シリアライズ/デシリアライズ

シリアライズはProtobuf自体に実装されていますし、JSONやtextを受け取れます。seralizedData(); を呼ぶだけでいいのです。

バイナリエンコーディング

message Talk {
    int32 id = 1; ← // Field number
    string title = 2;
    string desc = 3;
    User speaker = 4;
    repeated string tags = 5;
}

ワイヤータイプという型のグループがあり、intなら0、stringなら2となります。

次のstructを考えてみましょう。


message Test1 {
    int32 a = 1;
}
test1.a = 300

// encoded message
08 96 01

08 // field number and wire type
96 01 // value which is 300

300について見てみましょう。 a はint型で、エンコードした結果は 08 96 01 となっています。最初の2桁はフィールド番号とワイヤータイプの組み合わせを表しています.

バイナリエンコーディングによって、JSONに比べサイズが小さくなり、すべて数値となります。その結果、ネットワークのパフォーマンスがよくなります。

使用上の注意

バージョン管理

Protobufには、後方互換性があります。レスポンスにnon-fieldがある場合、レスポンスは無視されます。または、レスポンスに何か足りないものがある場合には、デフォルト値になります。サーバー側から新しいプロパティーを簡単に追加したり、クライアント側にある使っていないプロパティーを削除したりすることができます。

protobufとJSONの共存

HTTPのヘッダにあるcontent typeをみることで、JSONとProtobufをどちらも持たせることができます。返り値をJSONかProtobufにするかを設定できます。

デメリットは何か

Protobufはバイナリデータなので、人間が読める形式ではありません。コンソールで生データを見るには、カスタム型にデシリアライズする必要があります。

Protobufを使い始めると、最初は時間がかかります。Protobufの全てのことを知っている必要があることに加えて、iOSプロジェクトだけでなく、他のプラットフォームでも使われるからです。

Codable

つい先日、SwiftにCodableが追加されることが発表されました。これによりシリアライズに型安全性が担保されます。Protocolになっていて、Protobufと同じようにCodableを扱うことができます。SwiftのFoundationライブラリに公式に含まれます。

Q & A

Q: ProtobufはRubyでも使えますか? Yusuke: はい、Rubyでも使えますよ。

About the content

This talk was delivered live in June 2017 at AltConf. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

北 裕介

サンフランシスコにある株式会社メルカリの支社でiOSアプリの国際化対応を行っているiOSエンジニア。iOSにおける新しい技術に貪欲です。休みの日はサイクリングをしています。