Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでわずか30行のコードでHTTPサーバーを構築する方法

WebサーバーやHTTPという仕組みは、一見すると難解に感じられるかもしれません。ブラウザはどのようにリクエストを組み立て、レスポンスはどうやってユーザーの手元に届くのでしょうか?この記事では、わずか30行のコードでRubyによるHTTPサーバーを構築する方法を解説します。完成したサーバーはHTTP GETリクエストを処理できるようになり、最終的にはRackアプリケーションを配信できるところまで進めます。

HTTPとTCPが連携する仕組み

TCPは、サーバーとクライアントがデータをやり取りする方法を定めたトランスポートプロトコルです。

一方、HTTPはWebサーバーとHTTPクライアント(Webブラウザ)との間でデータを交換する方法を具体的に定めた、リクエスト・レスポンス型のプロトコルです。HTTPは通常、TCPをトランスポート層として利用します。要するに、HTTPサーバーとは「HTTPを話す」TCPサーバーのことなのです。

# tcp_server.rb
require 'socket'
server = TCPServer.new 5678
 
while session = server.accept
  session.puts "Hello world! The time is #{Time.now}"
  session.close
end

このTCPサーバーの例では、サーバーはポート5678にバインドし、クライアントの接続を待ち受けます。接続があるとクライアントへメッセージを送信し、その後接続を閉じます。最初のクライアントとの通信が完了すると、サーバーは次のクライアントの接続を待ち、再び同じメッセージを送ります。

# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
 
while line = server.gets
  puts line
end
 
server.close

サーバーに接続するには、TCPクライアントが必要です。この例のクライアントは同じポート(5678)に接続し、server.getsを使ってサーバーからデータを受信して出力します。データの受信が終わると、サーバーへの接続を閉じてプログラムは終了します。

サーバーを起動($ ruby tcp_server.rb)したら、別のターミナルタブでクライアントを起動すれば、サーバーからのメッセージを受け取れます。

$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$

少し想像力を働かせると、このTCPサーバーとクライアントの関係は、Webサーバーとブラウザのそれによく似ています。クライアントがリクエストを送り、サーバーが応答し、接続が閉じられる——これがリクエスト・レスポンスパターンの基本であり、まさにHTTPサーバーを構築するために必要な考え方です。

本題に入る前に、まずHTTPのリクエストとレスポンスがどのような形式なのかを見ておきましょう。

基本的なHTTP GETリクエスト

最もシンプルなHTTP GETリクエストは、追加のヘッダーやリクエストボディを持たない、リクエストラインだけのものです。

GET / HTTP/1.1\r\n

リクエストラインは以下の4つの要素で構成されます。

  • メソッドトークン(この例ではGET)
  • リクエストURI(/)
  • プロトコルバージョン(HTTP/1.1)
  • 行末を示すCRLF(キャリッジリターン\rとラインフィード\n)

サーバーは、次のようなHTTPレスポンスで応答します。

HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\nHello world!

このレスポンスは以下の要素で構成されています。

  • ステータスライン:プロトコルバージョン("HTTP/1.1")、スペース、ステータスコード("200")、そしてCRLF(\r\n)
  • 任意のヘッダーライン。この例では1行だけ("Content-Type: text/html")ですが、複数あっても構いません(CRLF:\r\nで区切ります)
  • ステータスラインとヘッダーをボディから分離する改行(二重CRLF):\r\n\r\n
  • ボディ:"Hello world!"

最小構成のRuby HTTPサーバー

前置きはここまでにしましょう。RubyでTCPサーバーを作る方法と、HTTPのリクエスト・レスポンスの形式がわかったところで、最小構成のHTTPサーバーを構築できます。このWebサーバーは、先ほどのTCPサーバーとほぼ同じ構造であることに気づくでしょう。基本的な考え方は同じで、メッセージのフォーマットにHTTPプロトコルを使う点だけが異なります。また、今回はリクエストの送信とレスポンスの解析にブラウザを利用するため、クライアント側を実装する必要はありません。

# http_server.rb
require 'socket'
server = TCPServer.new 5678
 
while session = server.accept
  request = session.gets
  puts request
 
  session.print "HTTP/1.1 200\r\n" # 1
  session.print "Content-Type: text/html\r\n" # 2
  session.print "\r\n" # 3
  session.print "Hello world! The time is #{Time.now}" #4
 
  session.close
end

サーバーはリクエストを受け取った後、以前と同様にsession.printを使ってクライアントへメッセージを返します。ただし、単にメッセージを送るだけでなく、ステータスライン、ヘッダー、改行を先頭に付与しています。

  1. ステータスライン(HTTP/1.1 200\r\n):HTTPバージョンが1.1で、レスポンスコードが"200"であることをブラウザに伝える
  2. レスポンスがtext/htmlコンテンツタイプであることを示すヘッダー(Content-Type: text/html\r\n)
  3. 改行(\r\n)
  4. ボディ:"Hello world! …"

前の例と同様に、メッセージ送信後は接続を閉じます。まだリクエストの中身は読んでいないので、とりあえずコンソールへ出力しているだけです。

サーバーを起動してブラウザで https://localhost:5678 を開くと、先ほどTCPクライアントで受け取ったのと同じように、現在時刻付きの"Hello world! …"という行が表示されるはずです。🎉

Rackアプリケーションの配信

これまでのサーバーは、リクエストごとに決まった単一のレスポンスを返していました。もう少し実用的にするために、レスポンスの種類を増やしてみましょう。ただし、サーバーに直接ロジックを書き足すのではなく、Rackアプリを利用します。サーバーはHTTPリクエストを解析してRackアプリへ渡し、Rackアプリが返したレスポンスをクライアントへ送り返します。

Rackは、RubyをサポートするWebサーバーと、RailsやSinatraといった主要なRuby Webフレームワークをつなぐインターフェースです。最もシンプルな形では、Rackアプリとはcallに応答し、"トリプレット"——HTTPレスポンスコード、HTTPヘッダーのハッシュ、ボディの3要素からなる配列——を返すオブジェクトのことです。

app = Proc.new do |env|
  ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end

この例では、レスポンスコードが"200"、ヘッダー経由で"text/html"をコンテンツタイプとして指定し、ボディは文字列を1つ含む配列になっています。

このアプリのレスポンスを配信できるようにするには、返されたトリプレットをHTTPレスポンス文字列へ変換する必要があります。以前のように常に静的なレスポンスを返すのではなく、Rackアプリが返したトリプレットをもとにレスポンスを組み立てていきます。

# http_server.rb
require 'socket'
 
app = Proc.new do
  ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
 
server = TCPServer.new 5678
 
while session = server.accept
  request = session.gets
  puts request
 
  # 1
  status, headers, body = app.call({})
 
  # 2
  session.print "HTTP/1.1 #{status}\r\n"
 
  # 3
  headers.each do |key, value|
    session.print "#{key}: #{value}\r\n"
  end
 
  # 4
  session.print "\r\n"
 
  # 5
  body.each do |part|
    session.print part
  end
  session.close
end

Rackアプリから受け取ったレスポンスを配信するために、サーバーに以下の変更を加えました。

  1. app.callが返したトリプレットから、ステータスコード・ヘッダー・ボディを取得する
  2. ステータスコードを使ってステータスラインを組み立てる
  3. ヘッダーをループ処理し、ハッシュ内の各キー・バリューペアごとにヘッダーラインを追加する
  4. 改行を出力して、ステータスラインとヘッダーをボディから分離する
  5. ボディをループ処理して各部分を出力する。ボディ配列には要素が1つしかないため、接続を閉じる前に"Hello world"メッセージをセッションへ出力するだけです

リクエストの読み取り

ここまで、サーバーはrequest変数を無視してきました。Rackアプリが常に同じレスポンスを返していたため、読む必要がなかったのです。

Rack::Lobsterは、Rackに同梱されているサンプルアプリで、動作のためにリクエストURLのパラメータを使用します。以前Procをアプリとして使っていましたが、今後はこれをテスト用アプリとして採用します。

# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
 
app = Rack::Lobster.new
server = TCPServer.new 5678
 
while session = server.accept
# ...

ブラウザを開くと、以前表示されていた味気ない文字列の代わりに、ロブスターが表示されます。なんとも美味しそうですね!

"flip!"と"crash!"のリンクは、それぞれ/?flip=left/?flip=crashに紐づいています。しかし現時点では、リンクをクリックしてもロブスターは反転せず、クラッシュもしません。これは、サーバーがまだクエリ文字列を処理していないためです。以前無視していたrequest変数を覚えていますか?サーバーのログを見ると、各ページへのリクエスト文字列が記録されているのがわかります。

GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1

HTTPリクエスト文字列には、リクエストメソッド("GET")、リクエストパス(//?flip=left/?flip=crash)、HTTPバージョンが含まれています。この情報を使えば、何を配信すべきかを判断できます。

# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
 
app = Rack::Lobster.new
server = TCPServer.new 5678
 
while session = server.accept
  request = session.gets
  puts request
 
  # 1
  method, full_path = request.split(' ')
  # 2
  path, query = full_path.split('?')
 
  # 3
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query
  })
 
  session.print "HTTP/1.1 #{status}\r\n"
  headers.each do |key, value|
    session.print "#{key}: #{value}\r\n"
  end
  session.print "\r\n"
  body.each do |part|
    session.print part
  end
  session.close
end

リクエストを解析してパラメータをRackアプリへ渡すために、リクエスト文字列を分割してアプリに送っています。

  1. リクエスト文字列をメソッドとフルパスに分割する
  2. フルパスをパスとクエリに分割する
  3. それらをRack環境ハッシュに格納してアプリへ渡す

たとえば、GET /?flip=left HTTP/1.1\r\nというリクエストは、次のようなハッシュとしてアプリに渡されます。

{
  'REQUEST_METHOD' => 'GET',
  'PATH_INFO' => '/',
  'QUERY_STRING' => '?flip=left'
}

サーバーを再起動して https://localhost:5678 にアクセスし、"flip!"リンクをクリックするとロブスターが反転し、"crash!"リンクをクリックするとWebサーバーがクラッシュするようになります。

私たちが触れたのはHTTPサーバー実装のごく表面部分ですが、サーバー全体でもわずか30行のコードでありながら、基本的な考え方は十分に伝わったはずです。このサーバーはGETリクエストを受け付け、リクエストの属性をRackアプリへ渡し、レスポンスをブラウザへ送り返します。リクエストのストリーミングやPOSTリクエストなどには対応していませんが、理論上は他のRackアプリの配信にも利用できます。

以上で、RubyによるHTTPサーバー構築の入門編は終わりです。このサーバーで遊んでみたい方は、ぜひコードを試してみてください。さらに詳しく知りたい場合や質問がある場合は、@AppSignalまでお気軽にお問い合わせください。

この記事を楽しんでいただけたら、Ruby Magicニュースレターをご購読ください。毎月(だいたい)届く、Rubyの詰め合わせです。

  1. Rubyで新しいプログラミング言語を作る:インタプリタ編

    GitHubで完全なソースコードを公開中 Stoffleプログラミング言語の完全な実装はGitHubで公開しています。バグを見つけたり、疑問点がある場合は、ぜひIssueを立ててください。 この記事では、Rubyだけで作られたおもちゃのプログラミング言語「Stoffle」のインタプリタ実装を始めます。このプロジェクトについては、シリーズ第1回目の記事で詳しく紹介しているので、まだ読んでいない方はそちらもチェックしてみてください。 今回構築するのは、いわゆる「ツリーを辿るインタプリタ(tree-walk interpreter)」と呼ばれるものです。前回の記事では、フラットなトークンの

  2. Rubyでアプリケーションサーバーが必要な理由とは?Pumaの役割をわかりやすく解説

    rails server を実行したときに、自動的に起動してくる「Puma」という存在を見たことはありませんか? 実はあれは「アプリケーションサーバー」です。この記事では、アプリケーションサーバーとは何か、なぜ必要なのかを具体例とともにわかりやすく解説します。 アプリケーションサーバーとは何か まず、Rubyで新しいWebアプリケーションの開発を始めた場面を想像してみてください。 コードを一行も書く前に、ブラウザでページが表示されることを確認したいですよね。デフォルトの「Welcome」ページでもいいので、とりあえず表示させたいはずです。 そこでブラウザを開き、localhost:3000(S