Hatena::ブログ(Diary)

再帰の反復

2009-07-22

WEBrickを使うためのメモ

WEBrickは、HTTPサーバを簡単に作ることができるライブラリらしい。

しかし「じゃあ簡単に作ってみよう」と思ってRubyマニュアルWEBrickの部分を見ても、まったく要領を得ないので、使うためのメモを書いておく。

HTTPServer

まず標準リファンレンスは次

簡単なHTTPサーバ

とりあえずぜんぜん役に立たないHTTPサーバを作ってみる。

HTTPサーバの仕事は、クライアントからの要求(リクエスト)に対して適当な返答(レスポンス)を返すこと。

次のプログラムサーバとしての最小限の仕事を一応おこなう。

require 'webrick'

server = WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1',
                                  :Port => 10080})

trap(:INT){server.shutdown}
server.start

ポート番号は適当に10080に設定しているけど、デフォルト値は80なのでそれで良いなら省略できる。

プログラムを実行すると、たぶん無事にサーバが起動するはず。

ためしにwebブラウザのロケーションバーに

http://localhost:10080/zzz

とか

http://localhost:10080/index.html

とか

http://localhost:10080/a/b/c

などと打ち込んでみると、

Not Found

`/zzz' not found.

みたいに表示されるはず。

上のプログラムではサーバの仕事を何も設定もしていない。その場合どんなリクエストに対しても「見つからない」というレスポンスを返す。 それでも、クライアントからのリクエストに応じてレスポンスを返しているので、一応HTTPサーバにはなっている。

サーバがおこなう仕事を設定する

サーバの仕事の中身は、HTTPServerクラスのmount_procメソッドによって指定できる。

次のように書く。

server.mount_proc(dir) {|req, res|
  # 具体的な仕事をここに書く
}

mount_procの第一引数dirはProcオブジェクトをマウントする位置。とりあえずは、mount_proc('/')と書いておけば良い。

ブロックの引数reqにはHTTPRequestオブジェクトが入り、クライアントからのリクエストの内容が入っている。まずはreq.pathとreq.query_stringを見れば良い。例えばクライアント側が

http://localhost:10080/a/b/c/d.html?foo=3&bar=4

を指定した場合、pathは"/a/b/c/d.html"で、query_stringは"foo=3&bar=4"となる。

あと、どのリクエストメソッド(GET、POST、……)が使われたのかを知りたい場合、req.request_methodで参照できる(メソッド名が文字列で得られる)。

もう一つの引数resはHTTPResponseオブジェクトで、クライアントへのレスポンスの内容を表すオブジェクトで、デフォルトの値がすでに設定されている。

res.bodyにクライアントに返す本体データを設定する。データは文字列か、あるいはIOオブジェクト。IOオブジェクトの場合は、res.content_lengthに内容のサイズを設定するかres.chunkedをtrueにしておかないとうまく動かないような。よくわからない。

追記1: bodyに文字列を代入した場合は、content_lengthが文字列の大きさから自動で設定されているみたい。一方、IOオブジェクトの場合は明示的に与える必要がある。

追記2: HTTPのメッセージの本体(body)の長さを特定する仕方はいくつかある(詳しいことはRFC 2616 section 4.4)。

  1. Transfer-Encodingヘッダがあり、値がchunked: チャンク形式で長さを伝えながら送る。
  2. Content-Lengthヘッダがある: Content-Lengthの値で本体の長さを指定。
  3. 長さの指定がない: 送信側が接続を閉じることで本体の終わりを示す。

WEBrickでは、Responseオブジェクトresの状態で送信方法が決まる。

  • res.chunked がtrueに設定されていれば、チャンク形式で送信(デフォルトではfalse)。
  • res.chunked がfalseでres.content-lengthが設定されていれば、Content-Lengthヘッダを付けて送信する。res.bodyの値が文字列の場合はcontent-lengthを明示的に設定しなくても、長さを見てContent-Lengthヘッダを付けてくれる。
  • chunkedがfalseでcontent-lengthの設定もない場合、res.bodyをそのまま送る。

res.bodyがIOオブジェクトの場合は、content-lengthの値は自動には設定はされない。そのため次のいずれかの設定が必要。

  • res.content-lengthの値を設定する。
  • res.chunkedをtrueにする。
  • res.keep_aliveをfalseにして、送信終了時に接続を閉じる(keep_aliveはデフォルトではtrue)。

追記終わり


content_typeも設定した方が良いかもしれない。ファイル名のサフィックスからファイルタイプを推定するWEBrick::HTMLUtils.mime_typeというメソッドがある(マニュアルには載ってない)ので、それを使ってcontent_typeを設定することもできる。(追記: マニュアルに載るようになっていたRubyリファレンスマニュアル module WEBrick::HTTPUtils)

res.content_type = WEBrick::HTTPUtils.mime_type(req.path, HTTPUtils::DefaultMimeTypes)

reqの内容を確認して、resに適切な内容を設定してやることにより、HTTPサーバとしての仕事が果たされることになる。

次のプログラムは、指定されたパスに対して、その名前のファイルを返すサーバ

require 'webrick'

$docroot = Dir.pwd

server = WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1',
                                  :Port => 10080})

server.mount_proc('/') do |req, res|
  filename = File.join($docroot, req.path)
  open( filename) do |file|
    res.body = file.read
  end
  res.content_length = File.stat(filename).size
  res.content_type =
    WEBrick::HTTPUtils.mime_type(req.path, WEBrick::HTTPUtils::DefaultMimeTypes)
end

trap(:INT){server.shutdown}
server.start

WEBrick::HTTPServlet::FileHandlerを使えばもっと簡単に書ける(このプログラムでやっている処理の説明は省略)。

require 'webrick'

server = WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1',
                                  :Port => 10080})

server.mount_proc('/') do |req, res|
  WEBrick::HTTPServlet::FileHandler.
    new(server, Dir.pwd, {:FancyIndexing => true}).service(req, res)
end 

trap(:INT){server.shutdown}
server.start

しかしこの場合、mount_procではなくmountを使うのが普通なのだろう。

require 'webrick'

server = WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1',
                                  :Port => 10080})

server.mount('/', WEBrick::HTTPServlet::FileHandler, Dir.pwd,
                    {:FancyIndexing => true})

trap(:INT){server.shutdown}
server.start

もちろん「Dir.pwd」の部分は適切なディレクトリに変えたほうが良い。

なおFileHandlerのFancyIndexingオプションをtrueにすると、指定パスがディレクトリだったときにディレクトリに含まれるファイルの一覧を返す設定になる。

path_infoについての注意

マニュアルにはpath_infoはpath同値だと書いてあるけど、実際にはマウントの位置に応じて、pathとは違う値になる。

例えば'/a/b'にマウントされたProcやサーブレットが処理を行う場合、pathが"/a/b/c/d.html"の時、path_infoは"/c/d.html"になる。

マウントで仕事を分ける

複数のProcとサーブレットをマウントして、仕事を振り分けることができる。

たとえば

server.mount_proc('/', proc1)
server.mount_proc('/a', proc2)
server.mount('/a/b', Servlet1)

とすると、指定パスが「/a/b/...」ならServlet1(のインスタンス)が処理を担当し、パスが「/a/...で」...の部分が/b/で始まらなければproc2が処理を担当し、それ以外ならproc1が処理をおこなう。

次のプログラムはパスとして「/cgi_test/...」が指定された場合、カレントディレクトリの「cgi/...」ファイルをcgiプログラムとして実行する。このサーバcgiプログラムの実行テストなんかに使える。

パスが「/cgi_test/」から始まらない場合は(FileHanderが処理を担当し)指定されたファイルをそのまま返す。

ハンドラー(のクラス)がサーバーにマウントされている場合は、リクエストが来たときに自動でハンドラーオブジェクトを生成してserviceメソッドが呼ばれるのだけど、ここでは明示的にnewでオブジェクトを生成してserviceメソッドを呼び出している。

require 'webrick'

$cgi_dir = Dir.pwd + "/cgi"

server = WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1',
                                  :Port => 10080})

server.mount('/', WEBrick::HTTPServlet::FileHandler, Dir.pwd,
                    {:FancyIndexing => true})

server.mount_proc('/cgi_test') do |req, res|
  WEBrick::HTTPServlet::CGIHandler.new(server, $cgi_dir + req.path_info).service(req, res)
end

trap(:INT){server.shutdown}
server.start

実行するCGIプログラムが固定の場合は、次のようにCGIHandlerをマウントする。

server.mount(マウントポイント ,
             WEBrick::HTTPServlet::CGIHandler ,
             CGIプログラムのファイル名)

テスト用Webサーバとして使う

テスト用サーバを立ち上げる場合、サーバ側の準備ができる前にクライアント側が通信を始めてしまわないように注意する必要がある。サーバが動き出す(ソケットの準備ができる)前にクライアント側が通信を始めるとエラーになってしまう。

WEBrickの起動を待つには、例えば次のようなやり方が考えられる。

ログ出力を見る

WEBrick標準エラー出力(ログファイルが指定されていればそこ)にログを出力する。

WEBrickを起動するとまず次のようなログが出力される。

[2009-10-11 12:34:45] INFO  WEBrick 1.3.1
[2009-10-11 12:34:45] INFO  ruby 1.8.7 (2008-08-11) [i686-linux]
[2009-10-11 12:34:45] INFO  WEBrick::HTTPServer#start: pid=9082 port=10080

ここで3行目はソケットの準備ができてから出力される。

したがってクライアント側は3行目が出力されるのを確認してから通信を始めれば通信エラーにはならない。

StartCallback

HTTPServer.newの受け取るキーワード引数にStartCallbackというものがある。WEBrickはソケットの準備ができた後、StartCallbackで渡されたプロセスオブジェクトを呼び出す。この機構を使えばサーバの起動をクライアント側に通知することができる。

例えばクライアント側がメインスレッドで動いているとする。

クライアント側は通信を始める手前で「Thread.stop」で停止しWEBrickの起動を待ち、サーバ側はStartCallbackの動作でメインスレッドを起こす。

server = WEBrick::HTTPServer.new({
  :BindAddress => '127.0.0.1',
  :Port => 10080,
  :StartCallback => Proc.new {Thread.main.wakeup} })

# serverの動作を定義する

server.start

自前のサーブレット(ハンドラー)を定義する

Procオブジェクトではなくサーブレットオブジェクトに仕事をおこなわせる場合、AbstractServletのサブクラスを定義する。

class FooServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, res)
    ...
  end
  # 必要ならdo_POST(req, res)なども定義する。
  # GETと同じ動作でいいなら「alias :do_POST :do_GET」とかでもよい
end

server.mount(dir, FooServlet)     # (dir, FooServlet.new) ではない

演習問題

  1. ここまでのプログラムに対するテストを作成せよ。必要ならテストがおこないやすいようにプログラムを書き直せ。
  2. ここまでのプログラム(と同等のもの)をテスト駆動開発(あるいはそれに類似したもの)で作成せよ。

GenericServer

タイムアウト機能あり複数接続可能でログ機能も付いたTCPサーバデフォルトでは何もしないので、

のどちらかで、リクエストに対する処理を実装する(startメソッドの呼び出し時にブロックを渡していればリクエスト処理にそのブロックが使われ、渡していない場合はrunメソッドが使われる)。

WEBrick::HTTPServerは、runメソッドオーバーライドして機能を実装している(あとはinitializeで追加の初期設定をしている以外はGenericServerの動作を変更していない)。

例をあげると、echoサーバは次のように書ける。

require 'webrick'
serv = WEBrick::GenericServer.new({:Port => 7777,
                                   :BindAddress => '127.0.0.1'})
trap(:INT) { serv.shutdown }

serv.start do |sock|
  while ! sock.eof?
    sock.puts(sock.readline)  # 送られてきた行を読んでそのまま返す
  end
end

HTTPProxyServer

WEBrickにはプロキシサーバ用のクラスHTTPProxyServerもついている。サーバの処理は、HTTPProxyServer.newのProxyContentHandlerオプションで与える。

handler = Proc.new() do |req, res|
  # 処理内容を書く
end
server = WEBrick::HTTPProxyServer.new({:BindAddress => ..., :Port => ...,
                                       :ProxyContentHandler => handler})


あとはブラウザプロキシの設定を適当におこなってからプログラムを実行すればよし。

HTTPServerの場合と違うのは、handlerに渡される引数res.bodyがすでに設定されていること。

res.bodyの中身を見て書き換えをすれば、持ってきたデータに対してフィルタリングをしたり単語の訳や注釈やスクリプトを追加してみたりと色々なことができる。たぶん。

次のプログラムは、送られてきたデータのうちjpgファイルをimageディレクトリ以下に保存する。これを応用すれば自分の行動の履歴をとって分析する、みたいなこともできる。

require 'webrick'
require 'webrick/httpproxy'
require 'fileutils'
include WEBrick

$image_root = "image"

def datedir(root, date)
  s = File::SEPARATOR
  File.join(root,
            sprintf("%d%s%02d%s%02d", date.year, s, date.month, s, date.day))
end

handler = Proc.new() do|req, res|
  if %r{image/jpeg} =~ res.content_type
    time = Time.now
    image_dir = datedir($image_root, Time.now)
    FileUtils.mkdir_p(image_dir)

    filename = File.join(image_dir, req.path_info.gsub(%{/}, "_"))
    File.open(filename, "wb") do |file|
      file.write(res.body)
    end
  end
end

server = WEBrick::HTTPProxyServer.new({:BindAddress => '127.0.0.1',
                               :Port => 8080,
                               :ProxyContentHandler => handler})


trap(:INT){server.shutdown}
server.start

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証