この書籍を参考に、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 # クライアントからのHTTPリクエストを受信する # 空行が来るまで受信する request = @client.gets @path = request.split[1] if request.start_with?('GET') request = @client.gets until request.chomp.empty? end def send # クライアントにHTTPレスポンスを送信する # ファイルが存在する場合は、ファイルの内容を送信する # ヘッダーを送信する @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
を用意しました
<!DOCTYPE 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ファイルを返し、ブラウザで表示することが出来ました。 次は
- TCP接続を繰り返し受け付けるようにする
- 画像やCSSなどを送信できるようにする
- ディレクトリトラバーサル脆弱性がある
に対応してみたいと思います。
実装
画像や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接続を繰り返し受け付けるようにする
# frozen_string_literal: true 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を書き換え
<!DOCTYPE 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を用意する
<!DOCTYPE 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エラーに対応していきたいと思います