LoginSignup
31
26

More than 5 years have passed since last update.

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

Protocol Buffersとは

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

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

Protocol Buffersに関する詳細はGoogleのドキュメントに記されています。

特徴

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

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

Protocol BuffersのメリットやJSONとの比較はこちらに詳しく載っています。

Protocol Buffers と Swift

Appleが今年(2016年)の9月にapple/swift-protobufを公開しました。
また、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より

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ファイルに変換します。

導入の手順は、

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

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 導入より)

サーバーサイド

次にサーバーサイドでProtocol Buffersを送信するコードを作成します。
プロジェクトの作成はKituraでHello Worldを表示させるが参考になります。

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)

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ファイルをプロジェクトに追加します。

デシリアライズは

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

所感

実際使ってみた感想は、

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

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

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

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

まとめ

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

コード全体はKentaKudo/ProtobufSampleに置いてあります。

最後に

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

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

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

参考

Googleのドキュメント
複雑なデータをXMLより効率よく送る!~Protocol Buffers編
apple/swift-protobuf
これから始める Protocol Buffers 導入
Protocol Buffers in your Kitura Apps

31
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
26