この記事はkdnaktの1人 Advent Calendar 2020の16日目の記事です。
2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。
[echoServerの問題点]
これまで、kotlin-nativeリポジトリのサンプルプロジェクトであるechoServerをベースにHTTPサーバーの開発を進めてきました。
しかし、echoServerはtelnetでのHTTP接続を終了すると、echoServer自体が終了してしまう実装となっていました。そのため、実装したHTTPサーバーは1リクエストを処理すると停止してしまいます。
この問題を解決する方法を探していたところ、同じkotlin-nativeリポジトリに、nonBlockingEchoServerというプロジェクトを見つけました。
README.mdの説明によると、Kotlinの非同期処理の仕組みであるCoroutineとノンブロッキングI/Oを利用して、複数の接続を同時に処理できるとあります。このプロジェクトのソースコードを取り込めば、echoServerベースのHTTPサーバーを改善することができそうです。
[nonBlockingEchoServerを試す]
サンプルのnonBlockingEchoServerプロジェクトをビルドして起動し、実際にtelnet
コマンドで接続して動作を確認します。
まずは、初回の接続です。hello
と入力した場合、#1: hello
という接続IDつきのレスポンスが返ります。
$ telnet localhost 3000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. hello #1: hello ^] telnet> q Connection closed.
初回接続を閉じたあと、再度nonBlockingEchoServerに接続できるかを試します。
$ telnet localhost 3000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. hola #2: hola ^] telnet> q Connection closed.
echoServerの場合と異なり、一度接続を閉じたあとでもアプリケーションが停止せず、継続してレスポンスを返していることが分かります。
ターミナルを2つ同時に起動し、それぞれtelnet
でnonBlockingEchoServerに同時接続した場合も、問題なくレスポンスが返ってきました。
echoServerの抱えていた問題点が解消されています。
[echoServerとnonBlockingEchoServerの差分]
ソースコードを元に、差分を確認し、ノンブロッキングI/Oを実装するためのポイントを見ていきます。
まず目につくのは、fcntl()
という新たなシステムコールがnonBlockingEchoServerで利用されています。O_NONBLOCK
というフラグが渡されており、ソケットをノンブロッキングに設定しています。
fcntl(listenFd, F_SETFL, O_NONBLOCK) .ensureUnixCallResult { it == 0 }
また、buffer.usePinned()
のかわりに、acceptClientsAndRun()
という新たな関数が追加されています。memScoped
ブロックの処理をみると、接続IDを管理している部分以外はechoServerのコードをほぼ同じであることがわかります。
var connectionId = 0 acceptClientsAndRun(listenFd) { memScoped { val bufferLength = 100uL val buffer = allocArray<ByteVar>(bufferLength.toLong()) val connectionIdString = "#${++connectionId}: ".cstr val connectionIdBytes = connectionIdString.ptr try { while (true) { val length = read(buffer, bufferLength) if (length == 0uL) break write(connectionIdBytes, connectionIdString.size.toULong()) write(buffer, length) } } catch (e: IOException) { println("I/O error occured: ${e.message}") } } }
acceptClientsAndRun()
関数の引数は次のようになっています。
fun acceptClientsAndRun(serverFd: Int, block: suspend Client.() -> Unit)
Coroutineについてはまだ詳しく学べていないのですが、suspend
キーワードを追加することで、該当の関数を中断し、他の処理にスレッドを利用できるようになるようです。今回の場合、複数のHTTPコネクションを同時に扱えます。
この辺りの関連する実装を移植し、いくつかの型を修正すると、一度リクエストを処理したあともHTTPサーバーが動き続けるようになりました。
Chromeで連続してアクセスした場合も問題なく動作しています。