kdnakt blog

hello there.

Kotlin/NativeでHTTPサーバーを作るアドベントカレンダー(8日目:リクエストをログに残す)

この記事はkdnaktの1人 Advent Calendar 2020の8日目の記事です。

 

2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。

 

 

[ベースとなるecho serverの実装]

7日目はKotlin/NativeのサンプルプロジェクトであるEcho serverをビルドしてみました。

 

Echo serverをベースにして、HTTPサーバーのソケット部分を以下のように実装します。

fun main() {
    val port: Short = 8080

    memScoped {
        val buffer = ByteArray(1024)
        val serverAddr = alloc()
        val listenFd = socket(AF_INET, SOCK_STREAM, 0)
                .ensureUnixCallResult("socket") { !it.isMinusOne() }

        with(serverAddr) {
            memset(this.ptr, 0, sockaddr_in.size.convert())
            sin_family = AF_INET.convert()
            sin_port = posix_htons(port).convert()
        }

        bind(listenFd, serverAddr.ptr.reinterpret(), sockaddr_in.size.convert())
                .ensureUnixCallResult("bind") { it == 0 }

        listen(listenFd, 10)
                .ensureUnixCallResult("listen") { it == 0 }

        val commFd = accept(listenFd, null, null)
                .ensureUnixCallResult("accept") { !it.isMinusOne() }

        buffer.usePinned { pinned ->
            while (true) {
                val length = recv(commFd, pinned.addressOf(0), buffer.size.convert(), 0).toInt()
                        .ensureUnixCallResult("read") { it >= 0 }

                if (length == 0) {
                    break
                }

                send(commFd, pinned.addressOf(0), length.convert(), 0)
                        .ensureUnixCallResult("write") { it >= 0 }
            }
        }
    }
}

 

[リクエストをログに残す]

echo serverをベースに、HTTPサーバーとしての機能を追加します。まずは、クライアントから送られてきたリクエストをprintln()で出力してみます。

buffer.usePinned { }の中で、recv()send()が呼ばれており、echo serverがここでリクエストを受け取って、エコーレスポンスを返していることが分かります。

 

IntelliJのサジェストを見ると、pinnedというラムダ式のパラメーターは、Pinned<ByteArray>という型のようです。

f:id:kidani_a:20201208041225p:plain

 

公式サイトの説明を読むと、Pinned<T>型は、get()関数を呼ぶと実体のT型のオブジェクト、今回の場合はByteArray型のオブジェクトを得られることが分かります。

 

まずは、シンプルにprintln()関数で出力してみます。

buffer.usePinned { pinned ->
    while (true) {
        val length = recv(commFd, pinned.addressOf(0), buffer.size.convert(), 0).toInt()
                .ensureUnixCallResult("read") { it >= 0 }

        if (length == 0) {
            break
        }

        // 以下の追加
        println("[DEBUG]:")
        println(pinned.get())

        send(commFd, pinned.addressOf(0), length.convert(), 0)
                .ensureUnixCallResult("write") { it >= 0 }
    }
}

 

これを./gradlew nativeBinariesコマンドでビルドして実行し、別のターミナルからcurlコマンドでリクエストを送ると、以下のようにハッシュ値が出力されてしまいます。println(pinned.get().toString())のようにtoString()の呼び出しを追加しても結果は変わりませんでした。

f:id:kidani_a:20201208042645p:plain

 

ByteArray型について調べると、toKString()というバイト配列から文字列に変換してくれる関数を見つけました。

toKString - Kotlin Programming Language

 

これを利用して、println(pinned.get().toKString())のように修正・ビルドして実行してみると、無事サーバー側で受け取ったリクエストを文字列として出力することができました。

f:id:kidani_a:20201208041046p:plain

 

ByteArray型のオブジェクトを文字列に変換してターミナルに出力することができました。

このままでは、最終的にはPOSTリクエストでバイナリのデータが送られてくる場合にログが正しく出力できないという問題が起きるでしょう。しかし、いったん素朴な実装のまま次のステップであるリクエストパーサーの実装に進みます。

 

[まとめ]

  • ByteArrayを文字列に変換する場合はtoKString()関数を利用する
  • 実装中のコードは以下のリポジトリにまとめてある

github.com