HTTP/0.9とClojureとNIO2と

いつもお世話になっているHTTPの歴史を学んでいこう、とふと思い立ったのでHTTPサーバを実装している。
あまり進展はしていないけど、コードは下記にある。

https://github.com/marchrock/etude

今のところHTTP/0.9リクエストの受信と、マルチスレッドでの動作は実装済み。サーバ停止は思いつかなかったのでTomcatの方式をリスペクトしている。
今後はもっと本格的にcore.asyncを適用していきながら、HTTP/1.0へと進んでいく予定。
その前に設定変更できるようにしたり、テスト書いたり、異常ケースのハンドリングをできるようにしないとか。

HTTP/0.9

0.9というバージョン番号自体はレトロニムだと思うし、正式な呼称ではなさそうなのだが、W3C自身が「HTTP/0.9として知られている」[1]と言っているのでこれで通すことにする。

HTTP/0.9自体はSir Tim Berners-Leeが初めて実装したプロトコルで、本当にリソースを取得してくることだけを目的のプロトコルになっている。

GET request-path e.g. GET /index.html

シンプルなのでパーサの実装も凄く楽で、先頭がGETであれば、2要素目をリクエストパスとするだけ。
あとは指定パスのリソースを取得して返すだけだったりする。
一応、リクエストパスにはリモートリソースを指定できるので、その辺の対処は必要だけど(etudeではまだしてない)。

現代にも残っている……と言いたかったのだけど、RFC 7230のp.81を読みなおすとHTTP/0.9のサポートは取り除かれたようだ。

並列処理

普通サーバというやつはリクエストを並列に処理するので、当然並列処理を実装していきたくなる。

thread

最初に実装したのは凄くシンプルな方法で、accept用にthreadブロックを作ってacceptし、受信したコネクションの処理をさらに別のthreadブロックで実行させるというやつ。
まぁ、普通のマルチスレッドですね……

イメージはこんな感じ

(thread
  (loop []
    ; ↓でacceptしている
    (when-let [s (. server-sock accept)]
      ; ↓threadを使い、別スレッドでリクエスト処理関数を動かしている
      (thread (request-processor s)))
    (recur)))

acceptもthreadで分けているのは終了処理のため。
threadで分けなかったらacceptでブロックし続けるから、終了するためにはSHUTDOWNコマンドの他にリクエストも投げてacceptさせないといけないとかいう謎な状況になる。
(SHUTDOWNコマンド周りはTomcatの方式をパクリスペクトしている)

chanとgo-loopとやっぱりthread

core.asyncを調べていると、goブロックとchanを使ったデータのパッシングを行うことも重要な機能の一つだとわかったので使ってみることにしたのがこの段階。

イメージはこんな感じ

(let [out-chan (chan)]
  ; ↓acceptするブロック
  (go-loop []
    (when-let [s (. server-sock accept)]
      (>! out-chan s)
      (recur)))

  ; ↓acceptしたsocketを処理するthreadを呼ぶブロック
  (loop []
    (when-let [s (<!! out-chan)]
      (thread (request-processor s))
      (recur))))

goブロックの中でacceptを回し、受け付けたsocketをchanに放り込むという形。
書いてて気づいたんだけど、threadのままchan使う形にするべきだね……

nio2

そして、ここまで実装してそもそもJava側がブロッキングI/Oだと気づいたのでNIO2で実装し直している……
NIO2はJavaの方のFutureを使ったものと、CompletionHandlerを使ったものの2通りの結果の取得方法があるのたけど、基本的にはCompletionHandlerを使う方向で進めている。
と言うのも、Futureはgetするときに結局ブロッキングする可能性があって、それならブロッキングI/Oの方がシンプルなんだからわざわざ使うまでもないなぁ、と思ったため。
ただ、単純に実装してしまうとCompletionHandler内にHTTP特有の処理が入り込んでしまってあまりよろしくないのでほんわり考えている(必要な条件分岐だけ別書きできるようにするマクロになりそう)。

ま、こんな感じ。
最終的には

[HTTP Endpoint]  --+
                   +-[request handler]-[out]
[HTTPS Endpoint] --+

というようなchanで繋いだパイプラインを構築できればな、と思っている。
(もうちょっと繋げたい要素があったような気がするけど忘れた)

しかしこうちまちま書いてるとClojureでお仕事してみたいなぁ、という気にはなってくるなぁ。