gihyo.jp
この書籍を参考に、RubyでWebサーバを作ってみようと思います。
目次
動作環境
実装
実装のゴールの確認
WebサーバはブラウザからのHTTPリクエストに対し、HTTPレスポンスを返すのが仕事です。
具体的には
- ステータスライン
- レスポンスヘッダー
- 空行
- 要求されたパスの内容
をクライアントに返します。
今回はまず、前回作ったTCPサーバ/クライアントをベースに、クライアントに上記の内容を返すことを目指します。
初期化
class Modoki
def initialize
@server = TCPServer.open(8001)
@client = nil
@path = nil
end
end
TCPServerはRubyの標準ライブラリで、TCPプロトコルを使用したサーバーを作成することができます。
open
メソッドは新しいサーバーを作成し、指定したポート(ここでは8001)で接続を待ち受けます。
@clientは、後でTCP接続のクライアントを保存し、@pathはHTTPリクエストから取得したリクエストパスを保存するために用意します。
待機
def accept
@client = @server.accept
end
accept
メソッドは、クライアントからの接続があるまで処理をブロックします。 接続があるとクライアントとの通信用のソケットを返すので、それを@client変数に代入します。
リクエスト受信
def recv
request = @client.gets
@path = request.split[1] if request.start_with?('GET')
request = @client.gets until request.chomp.empty?
end
gets
メソッドは、接続済みのクライアント(@client)からHTTPリクエストの1行目を受け取ります。
リクエストが'GET'で始まる場合は、リクエストを空白文字で分割しリクエストのパスを@pathに代入し、
リクエストが空行になるまで1行のデータを受け取り、それを変数requestに代入し続けます。
レスポンス送信
def send
@client.puts("HTTP/1.0 200 OK\r\n")
@client.puts("Date: #{Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT')}\r\n")
@client.puts("Server: Modoki/0.1\r\n")
@client.puts("Content-Type: text/html\r\n")
@client.puts("\r\n")
File.open("#{DOCUMENT_ROOT}#{@path}", 'r') do |f|
f.each_line { |line| @client.puts(line) }
end
end
ヘッダーとボディを送信します。
ボディは、ドキュメントルート(DOCUMENT_ROOT)と、@pathを連結したパスを作成し、そのファイルから1バイトずつ読み込んでクライアントに送信しています。
切断
def close
@client.close
end
最後にクライアントとの接続を切断します
全体のコード
require 'socket'
class Modoki
DOCUMENT_ROOT = '/mnt/c/mywebsite'
def initialize
@server = TCPServer.open(8001)
@client = nil
@path = nil
end
def run
accept
recv
send
close
end
private
def accept
@client = @server.accept
end
def recv
request = @client.gets
@path = request.split[1] if request.start_with?('GET')
request = @client.gets until request.chomp.empty?
end
def send
@client.puts("HTTP/1.0 200 OK\r\n")
@client.puts("Date: #{Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT')}\r\n")
@client.puts("Server: Modoki/0.1\r\n")
@client.puts("Content-Type: text/html\r\n")
@client.puts("\r\n")
File.open("#{DOCUMENT_ROOT}#{@path}", 'r') do |f|
f.each_line { |line| @client.puts(line) }
end
end
def close
@client.close
end
end
server = Modoki.new
server.run
index.htmlを用意する
C:\mywebsite\
フォルダにindex.html
を用意しました
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>It works🎉
</body>
</html>
起動
$ ruby Modoki/modoki.rb
確認
ブラウザでhttp://localhost:8001/index.htmlを叩くと、
成功しました😄
https://github.com/kei-kmj/HenacatRuby/pull/3/commits/d93d49d1c3b8451d690a05d11d5d488c0ff49800
モドキじゃないサーバへ
1つのHTMLファイルを返し、ブラウザで表示することが出来ました。
次は
に対応してみたいと思います。
実装
画像やCSSなどを送信できるようにする
リクエストの拡張子に応じてMIMEタイプを変えないといけません。
@client.puts("Content-Type: text/html\r\n")
これを
@client.puts("Content-Type: #{content_type}\r\n")
にしてcontent_type
メソッドを書いていきます。
EXTENSION_TO_MIME =
{
'html' => 'text/html',
'txt' => 'text/plain',
'png' => 'image/png',
'jpg' => 'image/jpg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'css' => 'text/css',
'js' => 'text/javascript'
}.freeze
def content_type
ext = @path.split('.')[-1]
EXTENSION_TO_MIME[ext] || 'text/plain'
end
リクエストされたパスからファイルの拡張子を取得し、対応するMIMEタイプを EXTENSION_TO_MIMEハッシュから引き出し、
対応するMIMEタイプが見つからない場合は、デフォルトとして 'text/plain' を返します。
TCP接続を繰り返し受け付けるようにする
require_relative 'server_thread'
require 'socket'
class Server
DOCUMENT_ROOT = '/mnt/c/mywebsite'
def initialize
@server = TCPServer.open(8001)
end
def run
loop do
Thread.start(@server.accept) do |client|
ServerThread.new(client).process_request
end
end
end
end
server = Server.new
server.run
クライアントからの接続を受け続けるServer
クラスを用意しました。
Thread.start
メソッドを使うことで、各クライアントからのリクエストがそれぞれ独立したスレッドで処理されるので、多数のクライアントからのリクエストを効率良く処理できます。
実際のリクエストの処理はServerThread
が担当します。
HTMLファイルの用意
index.htmlを書き換え
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body {
background-color: #fffce6;
text-align: center;
padding: 20px;
}
img {
width: 80px;
height: auto;
}
a {
font-size: 50%;
}
</style>
</head>
<body>
It works<br>
<img src="\blob_cheer.jpg" alt="blob_cheer"><br>
<a href="next.html">次のページ</a>
</body>
</html>
css、画像、リンクを書き足してみました。
next.htmlを用意する
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Next</title>
<style>
body {
background-color: #fffce6;
text-align: center;
padding: 20px;
}
img {
width: 30px;
height: auto;
}
</style>
</head>
<body>工事中です 🙇<200d>♀️</body>
</html>
起動
$ ruby FlimsyServer/server.rb
確認
CSSも画像も表示でき、リンクも辿れました🎉
Implement web server by kei-kmj · Pull Request #3 · kei-kmj/HenacatRuby · GitHub
- 画像の表示が遅いです。まるで20年前のWebサイトに訪問したかのようです。画像データも1行ずつ送っているためだと思われます。バッファリングなどを考えれば良いのかもしれませんが、今回の目的から外れるので、ここでは対応しません。
- 次回、ディレクトリトラバーサル脆弱性、404エラーに対応していきたいと思います