Scala + finagle + MongoDB + Herokuで2chクローンを作る(連載第3回)
11/3に、@xuwei_k さんの誕生祝 & Scalaハッカソン!に参加した。とりあえず、Hello Finagleを見ながら、Finagle製のWebサーバを立てることを目指す。まずは、静的なHTMLファイル返答するところまで作ろう。
むむむ…sbtの標準ディレクトリ構成がわからん。sbtのディレクトリ構成はMaven由来らしいので、Mavenのディレクトリ構成を見る。
どうやら、src/main/webappというディレクトリを掘ればいいらしい。ホントにこれでいいのかな。「sbt resources 」でGoogle検索。そのものずばりな、What are “resources” folders in SBT projects for?という質問を発見。そこのリンクから、さらにhow to get a resource within scalatest w/ sbtを見る。
どうやら、リソースファイルのパスprefixはgetClass.getResource()で取得するのがJavaでは普通で、Scalaでもそうするらしい。Javaに詳しくないことがバレてしまう。staticに配信するHTMLファイルはsrc/main/resourcesかsrc/main/webappかどちらに置いたほうがいいのか、よくわからなくなってきたぜ。とりあえずwebappに置くことにする。
finagleを用いたWebサーバのひな形(あきこ)がある。どうやら、Nettyが用意するHTTPハンドラを使っているらしい。このひな形をベースに、
- ^/$, ^/css, ^/js -> staticなファイルを返す、mime-typeは拡張子からマッピングする
- それ以外は、それぞれのcaseクラスにマッピングしてハンドリングする
まずは、HTTP Methodのハンドリングだな。NettyのHTTPRequestのメソッドであるgetMethod()でパターンマッチするのが常道か。「"getMethod match"」でGoogle検索。それでひっかかった、PunkというWebフレームワークがサイズ小さそうなので読んでみる。
ソースを読む上で、vimのsyntax highlightが欲しい。
sbaz install scala-tool-support cp -R /usr/local/Cellar/scala/2.9.1/libexec/misc/scala-tool-support/vim/ ~/.vimという風にしたら、syntax highlightがついた。いえー。
Punkでは、静的ファイルをどのように読み込み、クライアントに返しているのか。
ack Fileでgrep。src/main/scala/punk/PunkFilter.scalaにreadFile()というメソッドがあるらしい。
private def readFile(url: URL) = { scala.io.Source.fromURL(url).mkString }うーむ。scala.io.Source.fromURL(url).mkStringってその場で文字列作るから、ファイルI/O待ってブロックするんじゃねーの? FinagleのイベントループにFile I/Oも載せられるような何かはないのかなー。初心者のクセに欲張りかもしらんけど。
と思ったらあったー、Finagleのドキュメント内、Using Future Pools。
FinagleはFutureという型をよく使うが、一種の遅延評価みたいなもんだろう。上記のサンプルに、importを補うとこんな感じか。
import com.twitter.finagle.Service import com.twitter.util.FuturePool import java.util.concurrent.ExecutorsこのThriftFileReaderを実行してみると、型エラーが出た。fromFileはscala.io.BufferedSourceを返すが、FuturePool.apply()は、Array[Byte]を受け取る。scala.io.BufferedSourceからArray[Byte]に変換する方法を調べよう…と思ったけど、面倒なのでやめる。toArrayあたりでいいのかな?class ThriftFileReader extends Service[String, Array[Byte]] { val diskIoFuturePool = FuturePool(Executors.newFixedThreadPool(4))
def apply(path: String) = { val blockingOperation = { scala.Source.fromFile(path) // potential to block } // give this blockingOperation to the future pool to execute diskIoFuturePool(blockingOperation) // returns immediately while the future pool executes the operation on a different thread } }
とりあえず、同期でもいいからファイルをHTTP経由で返すところまでもっていこう。
response.setContent(copiedBuffer(scala.io.Source.fromFile("src/main/webapp/index.html").mkString, UTF_8))うむ。ブラウザからアクセスしたところ、index.htmlが表示された。しかし、このcopiedBufferをはさむところがなんかダサいな。
responseはNettyのHttpResponseのインスタンス。setContentはバッファを受け取る。fromFile()はscala.io.BufferedSourceを返す。setContentが受け取るのはorg.jboss.netty.buffer.ChannelBufferインターフェース互換のもの。うまくやれば直接変換できそうな予感。そうすれば、FuturePoolでラップしなくても、実際にI/Oが必要となったときにFile -> Networkに直に転送してくれたりできそうな気配がする。けど、これもあまり深追いしないでおく。
pathによってきちんと返すファイルを変えよう。
response.setContent(copiedBuffer(scala.io.Source.fromFile("src/main/webapp" + request.getUri).mkString, UTF_8))動作した。
上記のサンプルは、「/」へのアクセスでindex.htmlを返してくれない。また、ありがちな「../../../etc/passwd」的なものに弱い。前者について、HttpRequestのパスが「/」の場合には「/index.html」と解釈するようにしよう。後者について、パスを作ったあとパスの正規化をして、そのprefixをチェックする、っていうのが一般的な処理だろう。今回は、Scalaの正規表現を勉強したいので、とりあえず「..」という部分文字列があったら例外を投げるようにしてみる。
こんな感じでよかんべ。「..」にマッチする正規表現をソースコードに埋め込むのは面倒だ。最初は、"..“と書いたが、”“”..“”“とも書けるようだ。
val tentenRegex = """..""".r val path = request.getUri初match!どきどき…あれれ、動作しない。ルートにアクセスしても、/index.htmlの内容を返してくれない。なんでだろ?調べてみると、pathが「/」の際に、2個目のcaseにマッチしていないようだ。なんでだ。俺たちはまだ、青春知らずさ。val filePath = "src/main/webapp/" + path match { case tentenRegex() => throw new Exception case "/" => "/index.html" case _ => path }
ためしにifでマッチするかどうか試してみるか。
val path = request.getUrimatch de-suって表示された。ifだとうまくいくようだ。なんでだろう。matchへの理解不足だな。とりあえず、スタンドアロンのmatch検証プログラムを書いて動作させるか。道のりは遠い。if (path == "/") { println("match de-su") }
今回の分はコミットしていないですが、githubでソースコードを公開し始めました。ニコニコ生放送でライブコーディング(という名のつぶやき放送)もやっております。