【Swift】 Protocol Buffers

こんにちは。スマートテック・ベンチャーズ1日目担当します工藤です。
初めてQiitaに投稿したのが去年のAdvent Calendarなので、ちょうど1年前ということになります。
早いですね。。
さて、今日はProtocol Buffersについてです。

# Protocol Buffersとは
JSONやXMLのようにデータ構造をシリアライズする形式です。
Googleにより2008年の夏にオープンソースとして公開されました。
以下のような流れで違うプラットフォーム・言語間のデータのやりとりができます。

```
データ構造
↓(Protocol Buffers でシリアライズ)
バイナリデータ
⇅(通信)
バイナリデータ
↓(Protocol Buffers でデシリアライズ)
(元と違う言語とかの)データ構造
```

Protocol Buffersに関する詳細は[Googleのドキュメント](https://developers.google.com/protocol-buffers/)に記されています。

## 特徴
JSONやXMLを使用する場合と比べ、Protocol Buffersは以下のような特徴を持ちます。

・テキストではなくバイナリに変換するため、サイズが小さくなる。速い。
・型情報がつくから安全。

Protocol BuffersのメリットやJSONとの比較は[こちら](http://blog.codeclimate.com/blog/2014/06/05/choose-protocol-buffers/)に詳しく載っています。

# Protocol Buffers と Swift
Appleが今年(2016年)の9月に[apple/swift-protobuf](https://github.com/apple/swift-protobuf)を公開しました。
また、[alexeyxo/protobuf-swift](https://github.com/alexeyxo/protobuf-swift)というライブラリが2014年から存在します。
他にもいくつか実装はあるようですが、この二つが主だったところです。

## apple/swift-protobufとSwiftの型
apple/swift-protobufにおける型とSwiftの型は以下のように対応しています。

| 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               |

[API Overview](https://github.com/apple/swift-protobuf/blob/master/Documentation/API.md)より

# Protocol Buffers サンプル
サーバーサイドはKitura、クライアントサイドはiOSアプリで実際にProtocol Buffersを使った通信をしてみたいと思います。

## .protoファイルの定義
初めに、.protoファイルを以下のように定義します。

```RockBand.proto
syntax = "proto3";

message Member {
    int64 id = 1;
    string name = 2;

    enum Instrument {
        VOCAL  = 0;
        GUITAR = 1;
        BASS   = 2;
        DRUMS  = 3;
    }

    Instrument instrument = 3;
}

message RockBand {
    int64 id = 1;
    string name = 2;
    repeated Member members = 3;
}
```

## コンパイル
次にこの.protoファイルを.swiftファイルにコンパイルします。
Googleが公開しているコンパイラではswiftへのコンパイルができないので、apple/swift-protobufのプラグインを使い.swiftファイルに変換します。

導入の手順は、

- [apple/swift-protobuf](https://github.com/apple/swift-protobuf)
- [これから始める Protocol Buffers 導入](https://speakerdeck.com/kyoheig3/protocol-buffers)

にも詳しく紹介されています。

1.コンパイラをインストール

```
brew install protobuf
```

2.apple/swift-protobufを導入。

```
$ git clone https://github.com/apple/swift-protobuf.git
$ cd swift-protobuf
```

3.最新のダグをcheckoutし、プラグインをビルド

```
$ git checkout tags/0.9.24
$ swift build
```

4.作成されたプラグインを指定して、.protoファイルをコンパイル

```
$ protoc --plugin=protoc-gen-swift=.build/debug/protoc-gen-swift --swift_out=. path/to/file/RockBand.proto
```

(補足)
プラグインのPATHを通しておくと便利です。

```
$ mkdir ~/.protoc
$ cp .build/debug/protoc-gen-swift ~/.protoc/protoc-gen-swift
$ echo 'export PATH=$PATH:$HOME/.protoc' >> ~/.bash_profile
$ source ~/.bash_profile
```

```
$ protoc --swift_out=. path/to/file/RockBand.proto
```

([これから始める Protocol Buffers 導入](https://speakerdeck.com/kyoheig3/protocol-buffers)より)

## サーバーサイド
次にサーバーサイドでProtocol Buffersを送信するコードを作成します。
プロジェクトの作成は[KituraでHello Worldを表示させる](http://qiita.com/nnsnodnb/items/3b028a7e72b155fe1b56)が参考になります。

1.Package.swift

Package.swiftにプラグインを作成したものと同じバージョンのapple/swift-protobufを指定します。

```Package.swift
import PackageDescription

let package = Package(
    name: "Server",
    dependencies: [
        .Package(url: "https://github.com/apple/swift-protobuf-runtime.git", Version(0,9,24)),
        .Package(url: "https://github.com/IBM-Swift/Kitura", Version(1,2,0))
    ]
)
```

2.コンパイルによって生成されたRockBand.pb.swiftファイルをmain.swiftと同じフォルダに配置

3.main.swift

シリアライズには `serializeProtobuf` メソッドを使います。
またJSONに変換することもでき、その場合は `serializeJSON` メソッドを使います。

Content-Typeに指定する値は特に決まっておらず、

```
application/octet-stream
application/protobuf
application/x-protobuf
```

などが使われているようです。
([Google Protocol Buffers and HTTP](http://stackoverflow.com/questions/1425912/google-protocol-buffers-and-http))


main.swiftには以下のように記述しておきます。

```main.swift
import Kitura
import SwiftProtobuf

let router = Router()

var oasis = RockBand(id: 1, name: "Oasis", members: [
    Member(id: 1, name: "Liam Gallagher", instrument: .vocal),
    Member(id: 2, name: "Noel Gallagher", instrument: .guitar),
    Member(id: 3, name: "Gem Archer",     instrument: .guitar),
    Member(id: 4, name: "Andy Bell",      instrument: .bass),
    Member(id: 5, name: "Chris Sharrock", instrument: .drums)
])

router.get("/") { request, response, next in

    let acceptType = request.headers["Accept"]

    if acceptType == "application/octet-stream" {
        response.headers["Content-Type"] = "application/octet-stream"
        response.send(data: try oasis.serializeProtobuf())
    } else {
        response.headers["Content-Type"] = "application/json; charset=UTF-8"
        response.send(try oasis.serializeJSON())
    }

    next()
}


Kitura.addHTTPServer(onPort: 8090, with: router)
Kitura.run()
```


この状態でサーバーを起動し、アクセスすると次のJSONが得られます。

```
{
    "id": "1",
    "members": [
        {
            "id": "1",
            "name": "Liam Gallagher"
        },
        {
            "id": "2",
            "instrument": "GUITAR",
            "name": "Noel Gallagher"
        },
        {
            "id": "3",
            "instrument": "GUITAR",
            "name": "Gem Archer"
        },
        {
            "id": "4",
            "instrument": "BASS",
            "name": "Andy Bell"
        },
        {
            "id": "5",
            "instrument": "DRUMS",
            "name": "Chris Sharrock"
        }
    ],
    "name": "Oasis"
}
```

## クライアント
クライアントでもapple/protobuf-swiftをインポートし、RockBand.pb.swiftファイルをプロジェクトに追加します。

デシリアライズは

```swift
let rockBand = try! RockBand(protobuf: data!)
```

により行います。


クライアントでは以下のようにデータをテーブル表示しました。

```ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    fileprivate var members = [Member]()

    override func viewDidLoad() {
        super.viewDidLoad()

        var request = URLRequest(url: URL(string: "http://localhost:8090/")!)
        request.httpMethod = "GET"
        request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            let rockBand = try! RockBand(protobuf: data!)
            self.members = rockBand.members
            DispatchQueue.main.async {
                self.title = rockBand.name
                self.tableView.reloadData()
            }
        }.resume()
    }
}

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return members.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = members[indexPath.row].name
            + " - "
            + members[indexPath.row].instrument.debugDescription

        return cell
    }
}
```

**表示結果**

![スクリーンショット 2016-11-30 13.40.58.png](https://qiita-image-store.s3.amazonaws.com/0/84014/a52243f5-65e5-010c-2541-8f9a736e33e3.png)


# 所感
実際使ってみた感想は、

**○API仕様書がいらない**
.protoファイルさえ共有すればあとは各々(サーバー・クライアント)好きな言語にコンパイルするだけなので、API仕様書がいらなくなります。
またJSONでよくある、「マッピング失敗するなーと思ったらキーをタイプミスしてた」というバグがなくせるのは大変嬉しいです。

**○型安全**
これもあるあるな、「ここ仕様書では数値で指定されてるのに数値の文字列で返ってきてます」といった不都合がなくなるのは嬉しいなと思いました。

**×導入が手間**
プラグインを用意したり、コンパイルしたりといった手間がかかるのは仕方ないですが面倒です。

**×swiftらしいクラス定義ができない**
Himotokiのような美しさに比べると変数がvarであったり、どうしてもswiftらしさに欠けてしまうのが気になります。
またproto2であった `optional` `required` がproto3では廃止されてしまったのも少々残念に感じます。
([Why required and optional is removed in Protocol Buffers 3](http://stackoverflow.com/questions/31801257/why-required-and-optional-is-removed-in-protocol-buffers-3))

# まとめ
手軽さに欠けるためJSONやXMLを置き換える技術になるのは難しいと思いますが、
サーバとクライアントでチームが別れている場合ではコミュニケーションコストを減らせるメリットは大きいのではないかと思います。
機会があれば積極的に使っていきたいと思います。

コード全体は[KentaKudo/ProtobufSample](https://github.com/KentaKudo/ProtobufSample)に置いてあります。

# 最後に
スマートテックベンチャーズは未経験者や、経験者だけどもSwift&iOSやってみたいという人を大募集しています。
このエントリーを見ている方は経験者かつswiftを使っている人だと思うので、ターゲット層ではないかもしれませんが、
もし身近にそういう方がいたら「そういえばこんな会社があったよ」と伝えていただければ幸いです。

あとOasisが好きな人は個人的に熱く語りましょう。

明日は田口さんによる心暖まる話です！

# 参考
[Googleのドキュメント](https://developers.google.com/protocol-buffers/)
[複雑なデータをXMLより効率よく送る！～Protocol Buffers編](http://www.acroquest.co.jp/webworkshop/JavaNetwork/NP_lecture07.html)
[apple/swift-protobuf](https://github.com/apple/swift-protobuf)
[これから始める Protocol Buffers 導入](https://speakerdeck.com/kyoheig3/protocol-buffers)
[Protocol Buffers in your Kitura Apps](https://developer.ibm.com/swift/2016/09/30/protocol-buffers-with-kitura/)