JJUG CCC 2017 Fallで「新しいプログラミング言語の学び方」のセッションでhttp serverを作っていました。自分でhttp serverを書いたのは遥か昔のことです。セッションを聞いてみて、久しぶりに自分でも書いてみるか、という気になり、勢いで書いてみました。あまり、ちゃんとしてはいないですが…。
生のソケットを扱うことは普通のWebアプリケーションの開発者にとってはあまりないことなので、少し新鮮でした。
http serverのコードはこちら。なるべく、発表で使われたソースコードの構造に合わせています。
で、自分用のメモです。
前提
まず、普通にブロッキングIOでソケットを扱うと、JavaやScalaと違いがないので、nioを使ってなるべくノンブロッキングで処理します。nioでサーバーソケットを扱うのはこれが初めてです。
nioに関する処理はSimpleHttpServer.ktにあります。
大まかな構造
メインスレッドの中でselectします。acceptすると読み込みを開始します。
読み込みは、readableになると別のスレッドで読み込みをして、メインスレッドはブロックしません。
HTTPヘッダーの読み込みが完了すると、メインスレッドのselectでwritableになったら、別スレッドに通知します。ここはread用のスレッドと同じようなパターン。ただ、writeの時に、handlerを呼び出して、書き込みのデータを準備します。ここがブロックされるとボトルネックになるので、ここでさらに別スレッドを立ち上げてhandlerを呼び出して書き込みまで行います。
ここまでが大きな流れです。
初期化
nioでサーバソケットを扱うときは、グローバルにはSelectorとchannelを利用します(L15, L16)。
そして、チャネルをノンブロッキングに指定して(L19)、ソケットを初期化します(L22)。最後にチャネルでacceptのイベントを登録します(L23)。
メインスレッド
runメソッド(L31)の中の無限ループでselectします(L37)。selectしてイベントがある場合は、イベントの種類に応じて処理して(L39-L48)、最後にキーを削除しています(L49)。ここではあとでまとめて削除していますが、イテレータでループしている最中にremoveしてもいいかも。
扱うイベントの種類は、accept/read/writeです。acceptのときは(L72)、ServerSocketChannelでacceptして、ソケットを取得します(L75)。ここでも、ノンブロッキングに設定して(L77)、読み込み可能な状態を待つように登録します(L81)。
各ソケットに対応したデータを紐づけて登録できるので、分断された処理も継続できます。
readableになったら、read用のキューにselectedKeyを登録しています。SynchronizedQueueなので、データが登録されたら別スレッドで処理が開始されます(L84-L86)。
writableになった場合も、メインスレッドの処理は同じです(L87-L90)。
ReadWorker
ReadWorkerでソケットからデータを読み込みます(L127)。一度の処理ですべてのヘッダーを読み込めるとは限らないので、ここでは、\r\n\r\nが現れるまで読み込みが継続します。
読み込みが完了したら、読み込み終了の状態にしてwriteのイベントを登録します。
WriteWorker
WriteWorkerでは、writableになったらExecutorでスレッドを生成して、実行します(L162)。handlerが処理した後(L173)、channelに書き込んで(L176)、チャネルを閉じています(L177)。ここのtry - catchは余分でしたね。
パフォーマンスはどう?
発表内容でのブロッキングIOでの処理とnioのノンブロッキングの処理と比べて、パフォーマンスは有意な差は見られません。(てへぺろ)
ただ、それは接続数が少ない場合で、接続数がおおくなると、ノンブロッキングの方が処理を継続しやすいです。(ブロッキングする方だとエラーが発生)。でも、おそらく、OSの設定を変更すれば大丈夫なはず。
websocketとかhttp2とか永続的なコネクションを扱うようになると、ブロッキングIOでスレッドでとやると、ある程度のオーバーヘッドが発生したりしてしまいそう。また、メモリも貧乏人には勿体無いかも。
おわり
とりあえず、ノンブロッキングはめんどくさいので、そういうことは若者にやってもらいましょう。
まじめに作ると結構大変そう。