この記事は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にアクセスすると、以下のように表示されました。成功です!
