Rubyで30行のHTTPサーバーを構築する
Webサーバー、および一般的なHTTPは、理解するのが難しいように思われるかもしれません。ブラウザはどのようにリクエストをフォーマットし、どのようにレスポンスをユーザーに送信しますか?このRubyMagicのエピソードでは、30行のコードでRubyHTTPサーバーを構築する方法を学習します。完了すると、サーバーが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リクエストとレスポンスがどのように見えるかを見てみましょう。
基本HTTPGETリクエスト
最も基本的なHTTPGETリクエストは、追加のヘッダーやリクエスト本文のないリクエスト行です。
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\n\Hello world!
この応答は次のもので構成されます:
- ステータス行:プロトコルバージョン( "HTTP / 1.1")、スペース、応答のステータスコード( "200")、CRLF(
\r\n
で終了) ) - オプションのヘッダー行。この場合、ヘッダー行は1つ( "Content-Type:text / html")だけですが、複数ある可能性があります(CRLFで区切られています:
\r\n
) - ステータス行とヘッダーを本文から分離するための改行(または二重CRLF):(
\r\n\r\n
) - 本文:「Helloworld!」
最小限のRubyHTTPサーバー
十分な話。 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
を使用します クライアントにメッセージを送り返すには:メッセージだけでなく、応答の前にステータス行、ヘッダー、改行を付けます:
- ステータス行(
HTTP 1.1 200\r\n
)HTTPバージョンが1.1で、応答コードが「200」であることをブラウザに通知します - 応答にtext/htmlコンテンツタイプがあることを示すヘッダー(
Content-Type: text/html\r\n
) - 改行(
\r\n
) - 本文:「Helloworld!…」
以前と同様に、メッセージの送信後に接続を閉じます。リクエストはまだ読んでいないので、今のところコンソールに出力するだけです。
サーバーを起動してブラウザでhttp:// localhost:5678を開くと、以前にTCPクライアントから受け取ったように、現在の時刻の「Helloworld!…」行が表示されます。 🎉
ラックアプリの提供
これまで、サーバーはリクエストごとに1つの応答を返してきました。もう少し便利にするために、サーバーに応答を追加することができます。これらをサーバーに直接追加する代わりに、Rackアプリを使用します。サーバーはHTTPリクエストを解析し、それらをRackアプリに渡します。その後、Rackアプリは、サーバーがクライアントに送り返すための応答を返します。
Rackは、RubyをサポートするWebサーバーと、RailsやSinatraなどのほとんどのRubyWebフレームワークとの間のインターフェースです。最も単純な形式では、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」をヘッダーに渡し、本文は文字列を含む配列です。
サーバーがこのアプリからの応答を提供できるようにするには、返されたトリプレットを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アプリから受け取った応答を提供するために、サーバーにいくつかの変更を加えます。
-
app.call
によって返されたトリプレットからステータスコード、ヘッダー、本文を取得します 。 - ステータスコードを使用してステータスラインを作成します
- ヘッダーをループして、ハッシュ内のキーと値のペアごとにヘッダー行を追加します
- 改行を印刷して、ステータス行とヘッダーを本文から分離します
- 体をループして、各部分を印刷します。 body配列には1つの部分しかないため、セッションを閉じる前に、「Helloworld」メッセージをセッションに出力するだけです。
リクエストの読み取り
これまで、サーバーは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=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アプリに送信するには、リクエスト文字列を分割してRackアプリに送信します。
- リクエスト文字列をメソッドとフルパスに分割します
- フルパスをパスとクエリに分割します
- それらをRack環境ハッシュでアプリに渡します。
たとえば、GET /?flip=left HTTP/1.1\r\n
のようなリクエスト 次のようにアプリに渡されます:
{
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/',
'QUERY_STRING' => '?flip=left'
}
サーバーを再起動し、http:// localhost:5678にアクセスして、「flip!」リンクをクリックすると、ロブスターが反転し、「crash!」をクリックします。リンクによってWebサーバーがクラッシュします。
HTTPサーバーの実装の表面をかじったところです。コードはわずか30行ですが、基本的な考え方を説明しています。 GETリクエストを受け入れ、リクエストの属性をRackアプリに渡し、ブラウザにレスポンスを送り返します。リクエストストリーミングやPOSTリクエストなどは処理しませんが、理論的には、他のRackアプリにもサービスを提供するためにサーバーを使用できます。
これで、RubyでHTTPサーバーを構築する方法について簡単に説明しました。私たちのサーバーで遊んでみたい場合は、ここにコードがあります。詳細を知りたい場合、または具体的な質問がある場合は、@AppSignalまでお知らせください。
この記事を楽しんだら、Ruby Magicニュースレターを購読してください:Rubyの(おおよそ)毎月の記事。
-
Rubyでの新しいプログラミング言語の構築:インタープリター
Githubのフルソース Stoffleプログラミング言語の完全な実装は、GitHubで入手できます。バグを見つけたり質問がある場合は、遠慮なく問題を開いてください。 このブログ投稿では、完全にRubyで構築されたおもちゃのプログラミング言語であるStoffleのインタープリターの実装を開始します。このプロジェクトの詳細については、このシリーズの最初の部分をご覧ください。 これから作成するインタプリタは、一般にツリーウォークインタプリタと呼ばれます。このシリーズの前回の投稿では、トークンのフラットシーケンスをツリーデータ構造(抽象構文木、または略してAST)に変換するパーサーを構築しま
-
Rubyでアプリケーションサーバーが必要なのはなぜですか? (プーマのように)
rails serverを実行すると実行を開始するこの「Puma」とは何ですか ? アプリサーバーです! アプリケーションサーバーとは何か、なぜそれらが必要なのかを例を挙げて説明しましょう。 アプリケーションサーバーについて Rubyで新しい光沢のあるWebアプリケーションの構築を開始したとします。 そして、コードを書く前に… デフォルトの「ようこそ」ページを見るためだけでも、ブラウザに読み込まれることを確認したい。 したがって、ブラウザを開いて、localhost:3000を指定します。 、または多分localhost:4567 シナトラを使用している場合。 それではどうなりま