この記事では、Protocol BuffersをSwiftでどう使うかサンプルアプリを使って説明します。Protocol Buffersの詳細はこちらの記事を先に読むと大体のイメージが掴めると思います。個人的には Protocol Buffersのメリットは型安全に書けて高速な通信が実現する ことだと思います。
今回のサンプルアプリは、クライアント側はAPIKit、サーバ側はKituraで 通信処理を実装します。サンプルアプリはGitHubで公開しているので、こちらからダウンロードして試してみてください。
また、URLSession
で実装したサンプルアプリもこちらに用意しました。URLSession
だけでも楽に実装できるので、こちらも参考にしてみてください。
APIKitとは、リクエストもレスポンスも型が明確になっていて、Swiftらしい書き方が出来るライブラリ です。 少し古いですが、詳細は開発者のブログ記事を見てください。そこから特徴だけを抜粋してみました。
- リクエストに渡すパラメーターは型によって明らかになっている。
- レスポンスはモデルオブジェクトとして受け取れる。
- 成功時にレスポンスを非オプショナルな値で受け取れる。
- 失敗時にエラーを非オプショナルな値で受け取れる。
型が決まっているのでAPIKitとProtocol Buffersは 相性が良さそう です。
APIKitはバージョン3.1.0に[Protocol Buffersへの簡易的対応]((https://github.com/ishkawa/APIKit/pull/214)が含まれています。
Kituraとは IBMが開発しているSwift用のWeb APIフレームワーク です。サーバサイドSwiftは他にもフレームワークがありますが、触ったことがあるのがKituraだったのと、IBMからKituraでProtocol Buffersを使ってみるにはという記事があったので、そちらを参考にしてKituraを使ってみました。
今回のサンプルアプリは以下の環境で作成しました。
Swift 3.0.1
Xcode 8.2.1
protoc 3.1
swift-protobuf 0.9.26
Kitura 1.3
APIKit 3.1.0
サンプルアプリの機能は、ボタンを押すとトークン情報を取得するだけです。GET
とPOST
でそれぞれ取得できるように実装しています。通信処理がProtocol Buffersを使ってどう実装するかだけを説明したいので、その他の実装はかなり適当です。その辺りは無視してください。
まずは、クライアントの実装から見ていきます。
こちらが.proto
ファイルから自動生成されたToken
のモデルです。Token
だけでなく、GetTokenResponse
, PostTokenRequest
, PostTokenResponse
も一緒に生成されています。関係ある箇所のみ下記に抜粋してます。
struct Token: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
var accessToken: String = ""
init() {}
}
/// GET - Response
struct GetTokenResponse: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
var token: Token {
get {...}
set {...}
}
init() {}
}
/// POST - Request
struct PostTokenRequest: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
var accessToken: String = ""
init() {}
}
/// POST - Response
struct PostTokenResponse: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
var token: Token {
get {...}
set {...}
}
init() {}
}
次は通信エラー用のNetworkError
モデルです。今回はエラーコードのenum
にbadRequest
だけ用意しました。enum
を生成した場合には初期値がunknown
になり、想定外の値用にUNRECOGNIZED(Int)
も用意されます。
struct NetworkError: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
enum Code: SwiftProtobuf.Enum {
typealias RawValue = Int
/// Unknown
case unknown // = 0
/// 400
case badRequest // = 400
case UNRECOGNIZED(Int)
init() {
self = .unknown
}
}
/// Error code
var code: NetworkError.Code = NetworkError.Code.unknown
/// Optional: Message
var message: String = ""
}
ここからはAPIKitの使い方の説明です。Kituraで走らせるサーバのURLとProtocol Buffers用のパーサーをセットします。
// サンプルアプリ用のリクエストプロトコルの宣言
protocol ProtobufRequest: Request {}
// デフォルト実装
extension ProtobufRequest {
// ローカルホストのURL
var baseURL: URL {
return URL(string: "http://localhost:8090")!
}
// HTTPレスポンスのパーサー
var dataParser: DataParser {
return ProtobufDataParser()
}
}
APIKitはリクエスト型を作る際にレスポンスの型も指定します。
// GETでトークンを取得するリクエスト
struct GetToken: ProtobufRequest {
// HTTPレスポンスの型
typealias Response = GetTokenResponse
// HTTPメソッド
var method: HTTPMethod {
return .get
}
// パス
var path: String {
return "/v1/token"
}
}
POST
の場合はGET
用リクエストでセットした情報に加えて、HTTPボディをセットします。Data型でセットするのでserializeProtobuf()
メソッドを使ってシリアライズしています。
// POSTでトークンを取得するリクエスト
struct PostToken: ProtobufRequest {
// HTTPレスポンスの型
typealias Response = PostTokenResponse
// HTTPメソッド
var method: HTTPMethod {
return .post
}
// パス
var path: String {
return "/v1/token"
}
// HTTPボディへセットするバイナリ
var bodyParameters: BodyParameters? {
var data = PostTokenRequest()
data.accessToken = token.accessToken
return ProtobufBodyParameters(protobufObject: try! data.serializeProtobuf())
}
private let token: Token
init(token: Token) {
self.token = token
}
}
ステータスコードが正常系でないときもエラー情報をprotobufのデータで返しているので、protobufの型にデシリアライズしています。
extension ProtobufRequest where Response: SwiftProtobuf.Message {
// レスポンスデータの処理
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
guard let data = object as? Data else {
throw ResponseError.unexpectedObject(object)
}
// Data型をprotobuf型にデシリアライズ
return try Response(protobuf: data)
}
func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
// ステータスコードのエラー処理
guard 200..<300 ~= urlResponse.statusCode else {
guard let data = object as? Data else {
throw ResponseError.unacceptableStatusCode(urlResponse.statusCode)
}
// エラーの場合もprotobufのバイナリが返ってくるのでNetworkError型にデシリアライズ
let error = try NetworkError(protobuf: data)
throw ResponseError.unexpectedObject(error)
}
return object
}
}
GET
でトークンを取得する処理です
Session.send(GetToken()) { result in
switch result {
case .success(let response):
print(response)
case .failure(let error):
print(error)
}
}
POST
でトークンを取得する処理です
var token = Token()
token.accessToken = "old token"
Session.send(PostToken(token: token)) { result in
switch result {
case .success(let response):
print(response)
case .failure(let error):
print(error)
}
}
こちらもPOST
でトークン情報を取得する処理ですが、ボディにセットするデータが適切ではないためにエラーが返ってきます。
Session.send(PostToken(token: Token())) { result in
switch result {
case .success(let response):
print(response)
case .failure(let error):
print(error)
}
}
APIKitとProtocol Buffersを組み合わせれば、より一層型安全に処理を実装できます。アプリの動作もコンパイル時に保証できるようになります。モックを使ったテストも書きやすいです。
次はサーバ側の実装です。最初にポート番号を設定します。
private let router = Router()
Kitura.addHTTPServer(onPort: 8090, with: router)
Kitura.run()
GETメソッドで/v1/token
パスにリクエストが来た時の処理です。
レスポンス型のGetTokenResponse
をシリアライズして送っています。
router.get("/v1/token") { request, response, next in
var token = Token()
token.accessToken = "my token"
var data = GetTokenResponse()
data.token = token
response.send(data: try data.serializeProtobuf())
next()
}
POSTメソッドで/v1/token
パスにリクエストが来た時の処理です。
まずはリクエスト型のPostTokenRequest
をデシリアライズします。その中のaccessToken
が空の場合はNetworkError
を送っていて、そうでなければレスポンス型のPostTokenResponse
をシリアライズして送っています。
router.post("/v1/token") { request, response, next in
var body = Data()
guard let bytes = try? request.read(into: &body),
let token = try? PostTokenRequest(protobuf: body) else {
return
}
guard token.accessToken.characters.count > 0 else {
var error = NetworkError()
error.code = .badRequest
error.message = "invalid access token"
response.status(.badRequest).send(data: try error.serializeProtobuf())
next()
return
}
var newToken = Token()
newToken.accessToken = "new token"
var data = PostTokenResponse()
data.token = newToken
response.status(.OK).send(data: try data.serializeProtobuf())
next()
}
このサンプルアプリはCarthage
を使っているので下記のコマンドを実行してください。
$ cd Client
$ carthage update
最後に、サーバをローカルで立ち上げます。下記のコマンドを実行してください。
$ cd Server
$ swift build
$ ./.buid/debug/Server
もしくは、下記のコマンドを実行した後にServer.xcodeproj
ファイルを開いて、Server
ターゲットを実行してください。
$ cd Server
$ swift package generate-xcodeproj
サンプルアプリを実際に動かしてみましょう。まずはサーバを先に起動しておく必要があります。手順は上記の通りです。
次にクライアントを起動します。起動するとボタンが3つ見えると思います。
左のボタンはGET
でトークン情報を取得します。
GetTokenResponse(token:Token(accessToken:"my token"))
真ん中のボタンはPOST
でトークン情報を取得します。
PostTokenResponse(token:Token(accessToken:"new token"))
右のボタンはトークン情報を取得しようとしてエラーになります。
responseError(APIKit.ResponseError.unexpectedObject(NetworkError(code:.badRequest,message:"invalid access token")))
こちらの記事で説明した通り、リクエストもレスポンスも型があるのでどんな値を持っているかが明確ですね。クライアントは特に、JSONからprotobufに移行してもそれほどコードに変更を加えなくても大丈夫です。より型安全に実装できて、通信も速くなるので楽しくなりますね。皆さんも是非使ってみてください。
今回のサンプルアプリはダウンロードして上記の手順に従えば動きますが、自分で一から作る場合は、swift-protobufのインストールやドキュメントの生成などが必要になります。その手順はこちらのREADMEを参考にしてください。
https://developer.ibm.com/swift/2016/09/30/protocol-buffers-with-kitura/
https://github.com/kitasuke/ProtobufExample
http://www.kitura.io/
https://github.com/IBM-Swift/Kitura
https://github.com/ishkawa/APIKit