この記事はkdnaktの1人 Advent Calendar 2020の15日目の記事です。
2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。
[ファイル読込]
14日目はハードコーディングしたHTMLをモックレスポンスとして返す実装をしました。
本物のHTMLファイルをレスポンスとして返す前に、HTMLファイルを読み込む部分の実装を行います。
Kotlin/Nativeリポジトリのsamplesディレクトリにて公開されている、CSV Parserプロジェクトを参考にしながら進めます。
CsvParser.ktでの実装をみていくと、fopen()
関数でファイルを開き、memScoped
ブロック内のfgets()
関数でファイルを読込み、fclose()
関数でファイルを閉じる、という流れで処理を行っているのが分かります。
これと同様の処理を、FileReaderクラスとして実装します。ファイルのパスが与えられた場合に、ファイルの中身を文字列として返します。
package com.kdnakt.kttpd import kotlinx.cinterop.* import platform.posix.* class FileReader(private val path: String) { private var loaded = false private var content: String = "" fun content(): String { if (loaded) return content val file = fopen(path, "r") try { if (file == null) { perror("cannot open file: $path") throw NotFoundException() } memScoped { val bufferLength = 64 * 1024 val buffer = allocArray<ByteVar>(bufferLength) while (true) { val nextLine = fgets(buffer, bufferLength, file)?.toKString() if (nextLine == null || nextLine.isEmpty()) break content += nextLine } loaded = true } return content } finally { fclose(file) } } } class NotFoundException(): HttpException(404, "Not Found")
ファイルが見つからなかった場合は、自作のHttpException
クラスを継承したNotFoundException
を投げることにします。
これをテストするために、プロジェクト直下にpublicTest
ディレクトリを作成し、以下のテストを実装します。
package com.kdnakt.kttpd import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith class FileReaderTest { @Test fun testReadSampleTxt() { val reader = FileReader("publicTest/Sample.txt") assertEquals("""First line |Second line |Third line """.trimMargin(), reader.content()) } @Test fun testFileNotExist() { val reader = FileReader("publicTest/FileNotExists.txt") val actual = assertFailsWith<NotFoundException> { reader.content() } assertEquals(404, actual.status) assertEquals("Not Found", actual.reason) } }
テストがそれぞれ成功することを確認できたら、FileReader
クラスをmain.kt
に組み込みます。
[ファイルをレスポンスに変換する]
HTTPリクエストで指定されたターゲットのファイルを読み込んだ後、HTTPレスポンスに変換する際、HTTPバージョンに注意が必要です。
HTTP/0.9の場合には、HTTP/1.1 200 OK
のようなstatus-line
は存在しません。レスポンスとしてはHTMLのみが返ります。
したがって、HTTPバージョンに応じてレスポンスを組み立てるよう実装します。ファイル読込部分を合わせると以下のようなコードとなります。
var content = "" var res = OkResponse() as Response try { content = FileReader("public" + request.requestTarget).content() } catch (e: NotFoundException) { res = ErrorResponse(e) } val ret = when (request.httpVersion) { HttpVersion.HTTP_0_9 -> content HttpVersion.HTTP_1_0, HttpVersion.HTTP_1_1 -> "${request.httpVersion.version} ${res.status} ${res.reason}\r\n" .plus("Content-Length: ${content.length}\r\n") .plus("\r\n").plus("${content}\r\n") } send(commFd, ret.cstr, ret.cstr.size.toULong(), 0) .ensureUnixCallResult("write") { it >= 0}
本番環境では、とりあえずpublic
ディレクトリ以下にあるファイルを取得するように実装しています。動作確認のため、プロジェクトのルート直下にpublic
ディレクトリを作成し、以下の内容でindex.html
ファイルを作成します。
<html> <h1>Hello kttpd!</h1> </html>
ネイティブバイナリを起動し、http://localhost:8080/index.html
にアクセスすると、以下のように表示されました。成功です!