この記事はkdnaktの1人 Advent Calendar 2020の22日目の記事です(3日遅れ)。
2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。
[POSTリクエストを処理する]
POSTリクエストを処理するのはHTTPサーバー(Webサーバー)の役割か、というとちょっと違う気もしますが、あまり気にせず実装してみます。完全なPOSTリクエストを処理しようとすると大変なので、シンプルな内容にとどめます。
▼HTMLファイルの用意
POSTリクエストにはリクエストボディに含まれるコンテンツのMIMEタイプを表すために、Content-Type
ヘッダーが含まれます。Content-Type
ヘッダーには以下の3種類があります。
- application/x-www-form-urlencoded: 初期値。属性を指定していない場合、この値が使用されます。
- multipart/form-data: type 属性で "file" を指定した <input>要素のために使用する値です。
- text/plain: (HTML5)
とりあえず、application/x-www-form-urlencoded
でPOSTリクエストを送ることができるように、以下のHTMLファイルをpost.html
という名前で作成し、public
ディレクトリに追加します。
<html>
<form action="/api/simple-post" method="post">
<div>
<label for="name">Who are you?</label>
<input name="name" id="name" />
</div>
<hr />
<div>
<input type="submit" value="Send" />
</div>
</form>
</html>
HTTPサーバーを起動し、/post.htmlにアクセスすると以下のような画面が表示されます。
▼テストを追加しておく
E2Eテストとして、以下の内容でtest/post_simple.html
ファイルを作成します。
フォームに「E2ETest」と入力して送信すると、「Hello, E2ETest san!」というメッセージが返ってくることを期待しています。
#!/bin/bash msg "post simple form" RES=$(http --check-status --ignore-stdin --timeout=4.5 --form post $SERVER_PATH/api/simple-post name=E2Etest) diff -w <(echo "$RES") <(echo "Hello, E2Etest san!") if [ "$?" != "0" ] ; then fail "wrong response" else ok "right response" fi
上記のテストを実行するために、run_test.sh
を以下のように修正します。これで今後テストを追加した際に、自動的にテストを実行してくれるようになります。
# 修正前 . ./test/get_index_html.sh # 修正後 for f in ./test/*.sh; do . "$f" done
▼実装を修正する
現在の実装では、main.kt
内でGETリクエストオブジェクトを作成し、ターゲットのファイルを読み込んでレスポンスとして返しています。
POSTメソッドと処理を分けるために、RequestHandler.kt
ファイルを以下の内容で作成します。
package com.kdnakt.kttpd.handler import com.kdnakt.kttpd.HttpVersion import com.kdnakt.kttpd.RequestContext import com.kdnakt.kttpd.Response abstract class RequestHandler { fun handle(req: RequestContext): String { val (content, res) = handleImpl(req) return when (req.httpVersion) { HttpVersion.HTTP_0_9 -> content HttpVersion.HTTP_1_0, HttpVersion.HTTP_1_1 -> "${req.httpVersion.version} ${res.status} ${res.reason}\r\n" .plus("Content-Length: ${content.length}\r\n") .plus("\r\n").plus("${content}\r\n") } } protected abstract fun handleImpl(req: RequestContext): Pair<String, Response> }
GETリクエストの処理をしていたコードを、以下のようにGetHandler.ktに分割します。
package com.kdnakt.kttpd.handler import com.kdnakt.kttpd.* class GetHandler: RequestHandler() { override fun handleImpl(req: RequestContext): Pair<String, Response> { var content: String var res = OkResponse() as Response try { content = FileReader("public" + req.requestTarget).content() } catch (e: NotFoundException) { res = ErrorResponse(e) content = "${e.status} ${e.reason}" } return Pair(content, res) } }
POSTリクエストの処理は、とりあえず受け取ったリクエストボディに含まれる入力内容を取得できればよいので、PostHandler.kt
は以下のように雑な実装としておきます。
package com.kdnakt.kttpd.handler import com.kdnakt.kttpd.OkResponse import com.kdnakt.kttpd.RequestContext import com.kdnakt.kttpd.Response class PostHandler( val contentType: String, val body: String): RequestHandler() { override fun handleImpl(req: RequestContext): Pair<String, Response> { val res = "Hello, ${body.split("=")[1]} san!" return Pair(res, OkResponse()) } }
もともとmain.kt
内でGETリクエストを処理していた部分のコードを以下の内容で置き換えます。リクエストボディをソケットから読み込む部分が非常に雑なので実用には耐えませんが、最低限の動作確認は可能です。
val handler = when(request.method) { HttpMethod.GET -> GetHandler() HttpMethod.POST -> { val contentType = request.headers["Content-Type"] ?: throw BadRequestException() val contentLength = request.headers["Content-Length"] ?.toInt()?: throw BadRequestException() val body = requestString.let { val bodyStart = it.indexOf("\r\n\r\n") + 4 it.substring(bodyStart, bodyStart + contentLength) } println("[DEBUG $connectionIdString] body $body") PostHandler(contentType, body) } } write(handler.handle(request))
▼動作確認
事前に実装しておいたテストを実行すると、無事成功しました。
ブラウザでも実際に動かしてみます。
名前を入力してSendボタンを押します。
レスポンスが帰ってきました!🎉