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 パッケージ用の空のプロジェクトが出来上がりました。ディレクトリ内は次のような構成になる様子です。

使用するパッケージに Kitura を指定する

Swift パッケージのプロジェクトができたら、その中の Package.swift ファイルにある dependenciesIBM-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 に関連する次のパッケージがダウンロードされる様子でした。

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 というようにしないといけないはずなのですけど、ビルドで使う MakefileKitura-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 コード内の #ifSWIFT_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

そうしてインポートされた RouterServer を使って Web サービスを実装していくことになるようです。

Router と Server

Router では URL のエンドポイントとそこにリクエストがあった時の処理を記載します。ひとつの Router インスタンスを作って、そこに必要な数だけ getpost といったメソッドを使って、エントリーポイントを追加して行くことになるようです。

Serverlisten メソッドでインスタンスを作り、待ち受けポート番号を指定して HTTP サーバーを待機状態にします。この時、先ほどの Router を指定して、リクエストの処理をそれに委ねるようにします。

考察

ところで、まだよくわかっていないのですけど、RouterServer は複数のインスタンスを生成することってあるのでしょうか。もし、どちらも1つだけだとしたら、わざわざ Router のインスタンスを作って Server に渡すみたいな必要もないのかなと思ったんですけど、これはもしかするとこれらの依存をなくしたいという設計思想なのかもしれませんね。

それはとりあえず維持しておくとして、それとは別のサーバーの待ち受け処理が気になりました。

まず HttpServerlisten メソッドを実行して、最後に 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 を使い始めてみようと思います。