WEB サーバを作る

作成日 : 2008-10-18
最終更新日 :

簡単なWEBサーバ

Ruby では、簡単なWEBサーバを書くことができる。 これには、WEBrick という WEBサーバ用フレームワークを使うとよい(注1)。 よく紹介されているのは、次の例である。 ファイル名を仮に websrv.rb とする。


require 'webrick'

document_root = 'C:/public_html/'

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

trap("INT"){ s.shutdown }

s.start

サーバのプロセスを ruby websrv.rb で立ち上げておき、 ブラウザに http://localhost:10080/index.html としてアクセスすれば、 ブラウザ画面にindex.html の内容が表示される。

コンテントネゴシエーションを実現するには?

さて、実は困ったことがある。私が Webページを作るとき、文字コードには utf-8 形式を用いている。 そして、この utf-8 で読み込むことを明示するために、拡張子に utf8 をつけている。 これは、ASAHI ネットのサーバがコンテントネゴシエーションという仕組みを採用しているからだ。 平たく言えば、拡張子が utf8 であれば、 このファイルでは言語としてutf-8を使っているよ、ということをサーバが教えてくれる。

注1:2012 年あたり、朝日ネット(旧 ASAHI ネット)では拡張子が utf-8 から utf8 に変更された。ただし、 これに伴う本文表記は変更せず、そのままにしている(2014-11-30)。

注2:2016 年 12 月 18 日、本文表記を変更した。

注3: 2020 年 9 月 30 日現在は、utf8 の拡張子はつけていない。 現在では、html ファイルの初めに <meta charset="utf-8"> と指示していれば、 確実に utf8 でのコンテントネゴシエーションがなされるため、拡張子での指示は不要となっている。

問題はその先にある。ファイルの名前としては utf8 を拡張子として使っているが、 HTML のリンク先としてのファイル名には utf8 を用いていない。 リンク先に、index.html.ja とあっても、index.html.ja というファイルは用意しておらず、 代わりの index.html.ja.utf8 というファイルしかない。 そこで、リンク先で省略された utf8 という拡張子を補って読んでくれる WEB サーバが必要だ。

今まではすべて ASAHIネットにアップロードしてから試験していたが、 全世界に公開しているサーバを使うのは試験とは言いがたい。 だから、まずローカルのコンピュータで試験を行い、合格したと判断した時点で初めてアップロードしたい。 そのような Web サーバはあるだろうか。

Apache は確かに高機能だが、それゆえ設定が面倒だ (私は過去やっているはずなのに挫折した)。 Windows であれば Anhttpd がある。けっこう使いやすく、 多くの機能がそろっている。 コンテントネゴシエーションについては、 言語とファイルタイプ、エンコーディングについては用意されている。 しかし、肝心のキャラクタセットについては用意されていない。 そこで、自分で必要な機能を作れる WEBrick を使えば所望のWebサーバができそうな気がした。

もっとも、私がこれから行うことは、単にファイルの拡張子の読み替えをするだけに過ぎない。 したがってこれは、コンテントネゴシエーションではない、ただの自作 WEB サーバの例である。

拡張子読み替えの実装

先のコードでは、ファイル名は明示的には出てこない。 よしなに server オブジェクトが処理している。 そこで、ファイル名を明示して読み込む版を示す。


require 'webrick'

document_root = 'C:/public_html/'

s = WEBRick::HTTPServer.new(
  :DocumentRoot => document_root,
  :BindAddress => '127.0.0.1',
  :Port => 10800
)


s.mount_proc("/") { |req, res|
  res.body = open(File.join(document_root,*req.path.split("/"))).read
}

trap("INT"){ s.shutdown }
s.start

要点は、mount_proc というブロックつきメソッドで、 open の引数になっているところにファイルが入ることである。 そこで、当初の目的を達成するにはここを書き換えればいい。 拡張子 utf8 を読み込めるようにしたコードは次の通り。


require 'webrick'

document_root = 'C:/public_html/'

s = WEBRick::HTTPServer.new(
  :DocumentRoot => document_root,
  :BindAddress => '127.0.0.1',
  :Port => 10800
)

s.mount_proc("/") { |req, res|
  path = File.join(document_root,*req.path.split("/"))
  path += ".utf8" if /\.html\.[a-z][a-z]$/ =~ path
  res.body = open(path).read
}

trap("INT"){ s.shutdown }
s.start

if 文にある正規表現は、これは、拡張子htmlのあとに、 言語コードであるアルファベット2文字で終わるファイルであれば、 これをHTMLファイルとみなす、という意味である。 この条件を満たすファイルのみ、末尾に拡張子 .utf8 を付加する。 この条件がないと、ファイルすべてに対して一律に、 たとえば CSS ファイルまで .utf8 を付加しようとするので、注意が必要だ。 なお、アルファベット2文字は制約が厳しい。 たとえば、言語に地域を加えたコード、たとえば es_ES などには対応できない。 もう少し緩めてもよいのかもしれない。(2008-10-19)

read 文の修正

この WEB サーバは重宝しているのだが、一つ問題点があった。 それは、一度 WEB で表示させると、HTTP のソースコードが編集できない、という問題点がある。 たとえば、私が使っているエディタでソースコードを編集して保存しようとすると、 次のエラーメッセージが出る。

C:/abc/def/ghi.html.ja.utf8
プロセスはファイルにアクセスできません。別のプロセスが使用中です。

そこでしばらく悩んでいた。 ソースコードを見ると、原因らしきものが見えた。
res.body = open(path).read
というコードでは、path のファイルを open したままで、 クローズしていないのが原因ではないかと推測した。 そこで、上記の行を次のように書き換えた。
res.body = File.read(path)
こうしておけば、読み終わった後できちんとクローズしてくれるはずだ。 実際やってみたところ、成功した。一度読んだファイルを編集、セーブしても問題ない。

画像が見えない問題の解決

上記の Webserver を作って重宝していたが、実はまだ問題があった。 それは、img タグで参照している画像がまったく表示されていないという問題である。 また、カスケーディングスタイルシート(CSS)で指定している情報も、 まったく読み込まれない。 この現象は、mount_proc を使わない版では全く生じない。原因は何だろうか?

実は2つ原因があった。まず一つ目を先に述べる。 一つは、拡張子に応じた Content-Type の情報を送っていないことであった。 これを送っていないと、ブラウザでは取り込んだ情報をすべて text/html と解釈してしまうため、 表示できない。 表示の仕方は、下の masuidrive さんの記事にあるソースの httpserver6.rb を参考にした。 なお、httpserver6.rb では、拡張子とContent-Typeの対応表をハッシュで "html" => "text/html" のように表現しているが、これは誤りで、正しくは、 ".html" => "text/html" のようにドットをつけないといけない。 これは、拡張子を切り出すクラスメソッド File.extname の仕様で、 拡張子はドットを含めて返すからである。

もう一つは Windows 固有の仕様で、 Windows でファイルを開くときはデフォルトでテキストモードになる。 これを見落として、本来バイナリモードで開くべきテキストもテキストモードになり、 内部でデータが壊れていたのだ。 これを避けるには、File.open で開いた後でバイナリモードに変更した上で、 File#read を使って読み込む必要がある(参考:Ruby レシピブック 268 の技より、 152 ファイルの内容を読み込む)

以上2つを反映させた Webserver は次の通り。(2009-01-18)、(2016-12-18 バグ修正)。


require 'webrick'

document_root = 'C:/public_html/'

s = WEBrick::HTTPServer.new(
  :DocumentRoot => document_root,
  :BindAddress => '127.0.0.1',
  :Port => 10800
)


s.mount_proc("/") { |req, res|
  path = File.join(document_root,*req.path.split("/"))
  path += ".utf8" if /\.html\.[a-z][a-z]$/ =~ path
	
	res.body = File.open(path){|file|
		file.binmode # バイナリモードでのオープン
		file.read
	}

  # 拡張子とContent-Typeの対応表
  content_types = {
    ".html" => "text/html", ".txt" => "text/plain",
    ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg",
    ".gif" => "image/gif", ".png" => "image/png", 
    ".mp3" => "audio/mpeg", ".mid"   => "audio/midi", 
    ".css"   => "text/css", ".xhtml" => "application/xhtml+html", 
    ".svg"   => "image/svg+xml"  

  }
  # filenameの拡張子を見てContent-Typeを設定
  content_type = content_types[File.extname(path)]
  # Content-Typeが見つからなかったらtext/htmlを設定
  if content_type==nil
    content_type = "text/html"
  end
  res["Content-Type"] = content_type
}

trap("INT"){ s.shutdown }
s.start

Last-Modified を返すには

改良の種はまだある。ここでその一つを紹介しよう。 一般的に、Webサーバーが返す値(エンティティヘッダフィールド)に、 ファイルの最終更新日(Last-Modified)がある。 しかしこの簡易 Web サーバーは、ファイルの最終更新日を返さない。 そのため、JavaScript などで最終更新日を表示するページは、 ブラウザによってまちまちな日を返す。たとえば、IE では1970年1月1日を、Firefox では当日を、 Opera では NaN を返す。この Webサーバで、ファイルの最終更新日を返すようにしたい。

これを実現するには、Webサーバがファイルのタイムスタンプを見る必要がある。 そのタイムスタンプをサーバが最終更新日とみなして、Last-Modified エンティティヘッダフィールドに付け加える。 実際には、上記コードの先頭に require 'time' を付け加えると共に、 res["Content-Type"] = content_type の行の次に、次の2行を付け加えればよい。

  stat = File.stat(path)
  res["Last-Modified"] = Time.parse(stat.mtime.to_s).httpdate

上記2行目の Time.parse の引数 stat.mtime.to_s のメソッド to_s を忘れて、 デバッグに無駄な時間を費やしてしまった(2010-01-04)。

注釈

注釈1:Ruby 3.0 では、それまでの版では標準添付だった WEBRick が標準添付ではなくなった。 Ruby 3.0 で WEBRick を使うには、 gem install webrick として、インストールする必要がある。以下はインストールの一例だ。(2021-06-10)

C:> gem install webrick
Fetching webrick-1.8.1.gem
Successfully installed webrick-1.8.1
Parsing documentation for webrick-1.8.1
Installing ri documentation for webrick-1.8.1
Done installing documentation for webrick after 0 seconds
1 gem installed

参考にしたWEBページ

まりんきょ学問所Rubyの浮き輪 > Web サーバを作る


MARUYAMA Satosi