Kitura を使って Ubuntu でサーバーサイド Swift してみる。
Server Side Swift
Kitura の実行環境も整えられたことですし、サンプルコードを真似て簡単な Web アプリケーションを作ってみることにしました。
Swift コードの在り方についても少し考察してみます。
先日に こちら で記したように整えた Ubuntu の Kitura 実行環境を使って、Swift で簡単な Web アプリケーションを作成してみることにしました。手順としては Kitura/README.md に記されている通りで、今回は実際にそれを試してみながら思ったことを綴ってみることにします。
Kitura が関与するところ
とりあえず、自分は思い違いをしていたみたいで、先日の環境構築で Kitura に関するサーバープログラムみたいな何かがインストールされてそれを使ってサーバーサイド Swift を実現するのかと思い込んでいたのですけど、どうやらそうではなさそうでした。
道筋としては、普通に Swift コンパイラを使って Web サーバーを作るみたいな感じになるようです。その時に Web サーバー的なところを簡単に実装するために IBM-Swift/Kitura-router を使う、そんなイメージになるようでした。
前回の Kitura 環境構築は、そんな Kitura ライブラリを Swift で動作させるために必要な環境整備をしていたことになるようです。
Kitura プロジェクトを作成
さて、まずは Kitura で作る Web アプリケーションのソースコードをまとめる Swift パッケージプロジェクトを作成します。
手順としては、好きな場所にディレクトリを作成して、その中で swift build --init
を実行すれば良いようなので、今回は ~/kitura/ESApi
というディレクトリを用意してみることにしました。
mkdir -p ~/kitura/ESApi
cd ~/kitura/ESApi
swift build --init
このようにすることで、Swift パッケージ用の空のプロジェクトが出来上がりました。ディレクトリ内は次のような構成になる様子です。
- ESApi
- .gitignore
- Package.swift
- Source
- main.swift
- Tests
使用するパッケージに Kitura を指定する
Swift パッケージのプロジェクトができたら、その中の Package.swift
ファイルにある dependencies
に IBM-Swift/Kitura-router
を記載します。
import PackageDescription
let package = Package(
name: "ESApi",
dependencies: [
.Package(url: "https://github.com/IBM-Swift/Kitura-router.git", majorVersion: 0),
]
)
これで、プロジェクト内で Kitura パッケージを使えるようになります。
Kitura で使うパッケージを読み込む
Kitura を使ったプロジェクトを make
でビルドするために使う Makefile を用意する必要があるようです。
これは先ほど指定した IBM-Swift/Kitura-router
が依存している IBM-Swift/Kitura-net
に含まれている Makefile-client
を流用することになるようなので、まずはターミナルを使ってプロジェクトディレクトリーで次のコマンドを実行し Kitura-net パッケージをダウンロードしておきます。
swift build --fetch
このコマンドを実行すると
現在では Kitura-router
に関連する次のパッケージがダウンロードされる様子でした。
- Kitura-router 0.3.2
- Kitura-net 0.3.2
- Kitura-sys 0.3.0
- LoggerAPI 0.2.0
- BlueSocket 0.0.4
- Kitura-CurlHelpers 0.2.0
- Kitura-HttpParserHelper 0.2.0
- Kitura-Pcre2 0.2.0
- SwiftyJSON 3.1.0
Makefile を準備する
パッケージのダウンロードができたら、その中の Kitura-net
パッケージから Makefile-client
をコピーして、自分のプロジェクトの Makefile
として使います。
cp Packages/Kitura-net-0.3.2/Makefile-client Makefile
Kitura を使って実装する
パッケージ周りの準備が整ったら、いよいよ Kitura を使った Web アプリケーションのコーディングです。ソースコードは Sources/main.swift
に記載します。
今回はすごく簡単にですけれど、次のようなコードを記載してみました。
import KituraRouter
import KituraNet
import KituraSys
let router = Router()
let server = HttpServer.listen(8090, delegate: router)
defer {
Server.run()
}
router.get("/") { request, response, next in
response.status(.OK).send("EZ-NET")
next()
}
とりあえずこのようなコードを記載して、コマンドラインから次のようにしてビルドします。
make
このようにすると、先ほどダウンロードした Kitura-router
などのモジュールも含めて Swift コードがビルドされて、パッケージ名と同じ名前の実行ファイルがビルドされました。
初回のビルドもそれほど時間はかかりませんけど、2回目の make
からは、変更したファイルだけがビルドされるため、さらに早くビルドが終わります。
ビルドの Configuration 指定について
ところで、このようにして Web アプリケーションをビルドした時は Debug ビルドされるのか Release ビルドされるのか、要は最適化が図られるのかとかが、Swift Package Manager 周りの知識がなくて、まだ分かりませんでした。
ビルドの Configuration は swift build --configuration release
みたいにして指定できるようにはなっているのですけど、それには swift build --configuration release
というようにしないといけないはずなのですけど、ビルドで使う Makefile
が Kitura-net
モジュール内のもので、それを直接書き換えないといけないようです。
そもそも Configuration に応じて設定を切り替えるには何らかの設定ファイルが必要になると思うんですけど、そのあたりがどのように制御されているかがわからないため、とりあえずは今のまま swift build
に任せてしまっておくことにします。
Configuration を指定して比べてみる
それでも一応、ビルド時に --configuration debug
をつけた場合と --configuration release
をつけた場合とで、出来上がったファイルサイズを比べてみました。
そうしたところ、前者 (debug) が 1,378,373 で、後者 (release) が 1,680,649 と、デバッグビルドを期待した方がファイルサイズが小さくなったりしましたけれど、とりあえず詳しいことはわからないながらも Configuration の指定によってビルドの仕方に変化は生まれているようです。
ちなみにビルドされた Web アプリケーションが出力される .build/debug
ディレクトリは debug
という名前が付いてますけど、これは単にビルドに使う Makefile
でそのディレクトリを作って出力先に指定しているだけなので、今回の Configuration 指定とは特には関係ないようでした。
パッケージマネージャーでビルドした場合を検出
余談になりますけど Configuration について調べていたときに、パッケージマネージャーでビルドしているかを判定する仕組みが用意されているのを知ったので、それについて記しておきます。
今回みたいにパッケージマネージャーでビルドをすると、Swift コード内の #if
で SWIFT_PACKAGE
が定義されているかを判定すると 真
として判定される様子でした。
作った Web アプリケーションを動かす
さて、上のようにしてビルドすると、パッケージ名と同じ名前の Web アプリケーションファイルが .build/debug
ディレクトリに出来上がるので、それを実行します。
.build/debug/ESApi
こうすることで、ソースコードの listen
で指定した待ち受けポートで HTTP を受け付けるプログラムが起動した状態になります。
そうしたら、たとえば http://localhost:8090/ みたいな、この Web アプリケーションに適切に辿り着ける URL に Web ブラウザーなどでアクセスすると、今回の場合であれば EZ-NET
という文字が画面に表示されます。
Swift の観点でソースコードを眺めてみる
これでひと通り、Kitura を使って Web アプリケーションを作るための入り口のところはできましたけど、今回書いたサンプルコードについて、今の知識でわかる範囲で何をしているか記しておこうと思います。
せっかくなので、これまで見てきた言語としての Swift という観点から、ここはこうしたらもっときれいな感じかもしれない?みたいなことも添えてみようと思います。Web アプリケーションフレームワークについては無知なので、その辺りの流儀は無視して語ることになると思いますけれど。
Kitura ライブラリのインポート
特筆するほどではなさそうですけど、Kitura の流儀で Web アプリケーションを作成するには、次の 3 つのライブラリをインポートすることになるようでした。
import KituraRouter
import KituraNet
import KituraSys
そうしてインポートされた Router
と Server
を使って Web サービスを実装していくことになるようです。
Router と Server
Router
では URL のエンドポイントとそこにリクエストがあった時の処理を記載します。ひとつの Router
インスタンスを作って、そこに必要な数だけ get
や post
といったメソッドを使って、エントリーポイントを追加して行くことになるようです。
Server
は listen
メソッドでインスタンスを作り、待ち受けポート番号を指定して HTTP サーバーを待機状態にします。この時、先ほどの Router
を指定して、リクエストの処理をそれに委ねるようにします。
考察
ところで、まだよくわかっていないのですけど、Router
や Server
は複数のインスタンスを生成することってあるのでしょうか。もし、どちらも1つだけだとしたら、わざわざ Router
のインスタンスを作って Server
に渡すみたいな必要もないのかなと思ったんですけど、これはもしかするとこれらの依存をなくしたいという設計思想なのかもしれませんね。
それはとりあえず維持しておくとして、それとは別のサーバーの待ち受け処理が気になりました。
まず HttpServer
の listen
メソッドを実行して、最後に run
を実行して待ち受け状態に移行してそのまま処理がブロックされる仕組みみたいですけど、Web サーバーを作ることが目的なら、サーバーは絶対に listen
で始まって run
で終わるわけなので、特に run
をわざわざ書きたくないようにも思います。
そこで、次のように HttpServer
に新しい listen
メソッドを追加してみることにしました。main.swift
はエンドポイントの定義に集中したいので、新しい定義は Listen.swift
という名前で定義してみることにしました。
import KituraRouter
import KituraNet
import KituraSys
private let router = Router()
extension HttpServer {
static func listen(port: Int, notOnMainQueue: Bool = false, @noescape routing: (HttpServer, Router) throws -> Void) rethrows {
let server = HttpServer.listen(port, delegate: router, notOnMainQueue: notOnMainQueue)
try routing(server, router)
Server.run()
}
}
こうすると main.swift
は次のように、Router
の生成とサーバーの起動を意識しないコードに書き換えられます。
HttpServer.listen(8090) { server, router in
router.get("/") { request, response, next in
response.status(.OK).send("EZ-NET")
next()
}
}
今回は Web アプリケーションを作ることが明らかなので、待ち受けのきっかけを HttpServer
型にこだわる必要もなさそうです。そこで、先ほどの Listen.swift
に次の定義を追加します。
func listen(port: Int, notOnMainQueue: Bool = false, @noescape routing: (HttpServer, Router) throws -> Void) rethrows {
try HttpServer.listen(port, notOnMainQueue: notOnMainQueue, routing: routing)
}
こうすることで main.swift
から HttpServer
の記述を取り去ることができました。
listen(8090) { server, router in
router.get("/") { request, response, next in
response.status(.OK).send("EZ-NET")
next()
}
}
エンドポイント
そうすると、次にエンドポイントの登録ですけど、ここで気になるのが最後に next
メソッドを呼び出しているところです。これを呼び出さないとレスポンスが終わったことにならないみたいですけど、つまり毎回書かなくても、エンドポイントの処理が終わったら必ず呼び出されるようにすれば良さそうです。
そこで、次のように next
を考慮しない get
を作って、その中で必ず next
が呼ばれるようにします。ついでに Error Handling にも対応させて、エラー発生時に速やかに Internal Server Error を通知できるようにしてみます。
import KituraRouter
import KituraNet
import KituraSys
extension Router {
typealias Handler = (request: RouterRequest, response: RouterResponse) throws -> Void
func get(path: String, handler: Handler) -> Router {
return get(path) { request, response, next in
defer {
next()
}
do {
try handler(request: request, response: response)
}
catch {
response.status(.INTERNAL_SERVER_ERROR).send("\(error)")
}
}
}
}
こうすることで、次のように main.swift
から next
の呼び出しを省くことができるようになりました。
listen(8090) { server, router in
router.get("/") { request, response in
response.status(.OK).send("EZ-NET")
}
}
他にも、単に慣れてないだけだと思うのですけど、Router
に GET メソッドのハンドラーを登録するのに get
メソッドを呼ぶところになんとなく違和感を覚えます。また、エンドポイントの実装が長くなったり複雑になるとクロージャーでは扱いづらそうなので、型を使って管理したいような気もしてきます。
とりあえずそんなところが気になりますけど、それはおいおい考えていくとして、ひとまずはこんなところで Kitura を使い始めてみようと思います。