More than 1 year has 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