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

ランニングラック:RubyHTTPサーバーがRailsアプリを実行する方法

Ruby Magicシリーズでは、ソフトウェアを分解して、内部でどのように機能するかを学ぶのが大好きです。それはすべてプロセスに関するものです。最終結果は、本番環境で使用するものではありません。Ruby言語とその人気のあるライブラリーの内部動作について学びます。新しい記事は月に1回程度発行されますので、このようなことに興味がある場合は、必ずニュースレターを購読してください。

Ruby Magicの以前のエディションでは、Rubyに30行のHTTPサーバーを実装しました。多くのコードを記述する必要なしに、HTTP GETリクエストを処理し、単純なRackアプリケーションを提供することができました。今回は、自家製のサーバーをもう少し取り上げます。完了すると、投稿の作成、更新、削除を可能にするRailsの有名な15分のブログを提供できるWebサーバーができあがります。

中断したところ

前回は、サンプルアプリケーションとしてRack::Lobsterを提供するのに十分なサーバーを実装しました。

  1. 私たちの実装はTCPサーバーを開き、リクエストが来るのを待ちました。
  2. それが起こったとき、リクエストライン(GET /?flip=left HTTP/1.1\r\n )は、リクエストメソッド(GET)を取得するために解析されました )、パス(/ )、およびクエリパラメータ(flip=left
  3. リクエストメソッド、パス、クエリ文字列がRackアプリに渡され、Rackアプリはステータス、一部のレスポンスヘッダー、レスポンス本文を含むトリプレットを返しました。
  4. これらを使用して、新しいリクエストが着信するのを待つために接続を閉じる前に、ブラウザに送り返すHTTP応答を作成することができました。
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
 
app = Rack::Lobster.new
server = TCPServer.new 5678
 
#1
while session = server.accept
  request = session.gets
  puts request
 
  #2
  method, full_path = request.split(' ')
  path, query = full_path.split('?')
 
  #3
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query
  })
 
  #4
  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

前回書いたコードを続けていきます。フォローしたい場合は、これが最終的なコードです。

ラックとレール

RailsやSinatraなどのRubyフレームワークは、Rackインターフェースの上に構築されています。 Rack::Lobsterのインスタンスと同じように 現在、サーバーのテストに使用しています。RailsのRails.application Rackアプリケーションオブジェクトです。理論的には、これは、サーバーがすでにRailsアプリケーションにサービスを提供できるはずであることを意味します。

それをテストするために、私は簡単なRailsアプリケーションを用意しました。サーバーと同じディレクトリにクローンを作成しましょう。

$ ls
http_server.rb
$ git clone https://github.com/jeffkreeftmeijer/wups.git blog
Cloning into 'blog'...
remote: Counting objects: 162, done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 162 (delta 32), reused 162 (delta 32), pack-reused 0
Receiving objects: 100% (162/162), 29.09 KiB | 0 bytes/s, done.
Resolving deltas: 100% (32/32), done.
Checking connectivity... done.
$ ls
blog           http_server.rb

次に、サーバーで、rackの代わりにRailsアプリケーションの環境ファイルを要求します およびrack/lobsterRails.applicationを配置します appRack::Lobster.newの代わりに変数 。

# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
 
app = Rails.application
server = TCPServer.new 5678
# ...

サーバーを起動します(ruby http_server.rb )そしてhttp:// localhost:5678を開くと、まだそこにいないことがわかります。サーバーはクラッシュしませんが、ブラウザの内部サーバーエラーが表示されます。

サーバーのログを確認すると、rack.inputというものが欠落していることがわかります。 。前回サーバーを実装するのが面倒だったことがわかったので、このRailsアプリケーションを機能させるにはまだやるべきことがたくさんあります。

$ ruby http_server.rb
GET / HTTP/1.1
Error during failsafe response: Missing rack.input
  ...
  http_server.rb:15:in `<main>'

ラック環境

サーバーを実装したとき、Rack環境を詳しく調べ、Rackアプリケーションを適切に提供するために必要な変数のほとんどを無視しました。最終的にはREQUEST_METHODのみを実装することになりました。 、PATH_INFO 、およびQUERY_STRING 変数は、単純なRackアプリには十分でした。

新しいアプリケーションを起動しようとしたときの例外からすでに見てきたように、Railsにはrack.inputが必要です。 、生のHTTPPOSTデータの入力ストリームとして使用されます。それ以外にも、サーバーのポート番号やCookieデータのリクエストなど、渡す必要のある変数がいくつかあります。

幸い、RackはRack::Lintを提供します Rack環境のすべての変数が存在し、有効であることを確認するのに役立ちます。これを使用して、Rack::Lint.newを呼び出してRailsアプリをラップすることで、サーバーをテストできます。 Rails.applicationを渡します 。

# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
 
app = Rack::Lint.new(Rails.application)
server = TCPServer.new 5678
# ...

Rack::Lint 環境内の変数が欠落しているか無効である場合、例外をスローします。現在、サーバーを再起動してhttp:// localhost:5678を開くと、サーバーがクラッシュし、Rack::Lint 最初のエラーを通知します:SERVER_NAME 変数が設定されていません。

~/Appsignal/http-server (master) $ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env missing required key SERVER_NAME (Rack::Lint::LintError)
        ...
        from http_server.rb:15:in `<main>'

スローされた各エラーを修正することで、Rack::Lintまで変数を追加し続けることができます。 サーバーのクラッシュを停止します。各変数Rack::Lintを見てみましょう 必要です。

  • SERVER_NAME :サーバーのホスト名。現在、このサーバーはローカルでのみ実行されているため、「localhost」を使用します。
  • SERVER_PORT :サーバーが実行されているポート。ポート番号(5678)をハードコーディングしたので、それをRack環境に渡します。
  • rack.version :対象のラックプロトコル 整数の配列としてのバージョン番号。 [1,3] 執筆時点で。
  • rack.input :生のHTTPPOSTデータを含む入力ストリーム。これについては後で説明しますが、空のStringIOを渡します。 今のところインスタンス(ASCII-8BITエンコーディングを使用)。
  • rack.errorsRack::Loggerのエラーストリーム 書き込む。 $stderrを使用しています 。
  • rack.multithread :サーバーはシングルスレッドであるため、これをfalseに設定できます 。
  • rack.multiprocess :サーバーは単一のプロセスで実行されているため、これをfalseに設定できます 同様に。
  • rack.run_once :サーバーは1つのプロセスで複数のシーケンシャルリクエストを処理できるため、これはfalse
  • rack.url_scheme :SSLはサポートされていないため、「https」ではなく「http」に設定できます。

不足しているすべての変数を追加した後、Rack::Lint 環境にもう1つ問題があることを通知します。

$ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env variable QUERY_STRING has non-string value nil (Rack::Lint::LintError)
        ...
        from http_server.rb:18:in `<main>'

リクエストにクエリ文字列がない場合は、nilを渡します。 QUERY_STRINGとして 、許可されていません。その場合、Rackは代わりに空の文字列を期待します。不足している変数を実装し、クエリ文字列を更新すると、環境は次のようになります。

# http_server.rb
# ...
  method, full_path = request.split(' ')
  path, query = full_path.split('?')
 
  input = StringIO.new
  input.set_encoding 'ASCII-8BIT'
 
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query || '',
    'SERVER_NAME' => 'localhost',
    'SERVER_PORT' => '5678',
    'rack.version' => [1,3],
    'rack.input' => input,
    'rack.errors' => $stderr,
    'rack.multithread' => false,
    'rack.multiprocess' => false,
    'rack.run_once' => false,
    'rack.url_scheme' => 'http'
  })
 
  session.print "HTTP/1.1 #{status}\r\n"
# ...

サーバーを再起動してhttp:// localhost:5678に再度アクセスすると、Railsの「You'reon Rails!」ページが表示されます。これは、自家製のサーバーで実際のRailsアプリケーションを実行していることを意味します。

HTTPPOSTボディの解析

このアプリケーションは、単なるインデックスページではありません。 http:// localhost:5678 / postsにアクセスすると、空の投稿リストが表示されます。新しい投稿フォームに入力して[投稿を作成]を押して新しい投稿を作成しようとすると、ActionController::InvalidAuthenticityTokenが表示されます。 例外。

信頼性トークンは、フォームを投稿するときに送信され、リクエストが信頼できるソースからのものであるかどうかを確認するために使用されます。現在、サーバーはPOSTデータを完全に無視しているため、トークンは送信されず、リクエストを確認できません。

HTTPサーバーを最初に実装したときは、session.getsを使用していました。 最初の行(Request-Lineと呼ばれる)を取得し、そこからHTTPメソッドとパスを解析します。 Request-Lineの解析に加えて、残りのリクエストは無視しました。

POSTデータを抽出できるようにするには、最初にHTTPリクエストがどのように構造化されているかを理解する必要があります。例を見ると、構造がHTTP応答に似ていることがわかります。

POST /posts HTTP/1.1\r\n
Host: localhost:5678\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: en-us\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Origin: https://localhost:5678\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14\r\n
Cookie: _wups_session=LzE0Z2hSZFNseG5TR3dEVEwzNE52U0lFa0pmVGlQZGtZR3AveWlyMEFvUHRPeXlQUzQ4L0xlKzNLVWtqYld2cjdiWkpmclZIaEhJd1R6eDhaZThFbVBlN2p6QWpJdllHL2F4Z3VseUZ6NU1BRTU5Y1crM2lLRVY0UzdSZkpwYkt2SGFLZUQrYVFvaFE0VjZmZlIrNk5BPT0tLUpLTHQvRHQ0T3FycWV0ZFZhVHZWZkE9PQ%3D%3D--4ef4508c936004db748da10be58731049fa190ee\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
Referer: https://localhost:5678/posts/new\r\n
Content-Length: 369\r\n
\r\n
utf8=%E2%9C%93&authenticity_token=3fu7e8v70K0h9o%2FGNiXxaXSVg3nZ%2FuoL60nlhssUEHpQRz%2BM4ZIHjQduQMexvXrNoC2pjmhNPI4xNNA0Qkh5Lg%3D%3D&post%5Btitle%5D=My+first+post&post%5Bcreated_at%281i%29%5D=2017&post%5Bcreated_at%282i%29%5D=1&post%5Bcreated_at%283i%29%5D=23&post%5Bcreated_at%284i%29%5D=18&post%5Bcreated_at%285i%29%5D=47&post%5Bbody%5D=It+works%21&commit=Create+Post

応答と同様に、HTTPリクエストは次のもので構成されます。

  • リクエストライン(POST /posts HTTP/1.1\r\n )、メソッドトークン(POSTで構成されます )、リクエストURI(/posts/ )、およびHTTPバージョン(HTTP/1.1 )、その後にCRLF(キャリッジリターン:\ r、ラインフィード:\ n)が続き、行の終わりを示します
  • ヘッダー行(Host: localhost:5678\r\n )。ヘッダーキー、コロン、値、CRLFの順。
  • リクエスト行とヘッダーを本文から分離するための改行(またはダブルCRLF):(\r\n\r\n
  • URLエンコードされたPOST本文

session.getsを使用した後 リクエストの最初の行(Request-Line)を取得するために、いくつかのヘッダー行と本文が残っています。ヘッダー行を取得するには、改行が見つかるまでセッションから行を取得する必要があります(\r\n

ヘッダー行ごとに、最初のコロンで分割します。コロンの前のすべてがキーであり、後のすべてが値です。 #strip 末尾から改行を削除する値。

本文を取得するためにリクエストから読み取る必要のあるバイト数を知るには、リクエストの送信時にブラウザが自動的に含める「Content-Length」ヘッダーを使用します。

# http_server.rb
# ...
  headers = {}
  while (line = session.gets) != "\r\n"
    key, value = line.split(':', 2)
    headers[key] = value.strip
  end
 
  body = session.read(headers["Content-Length"].to_i)
# ...

ここで、空のオブジェクトを送信する代わりに、StringIOを送信します。 リクエストで受け取ったボディのインスタンス。また、リクエストのヘッダーからCookieを解析しているので、HTTP_COOKIEのRack環境にCookieを追加できます。 リクエストの信頼性チェックに合格するための変数。

# http_server.rb
# ...
  status, headers, body = app.call({
    # ...
    'REMOTE_ADDR' => '127.0.0.1',
    'HTTP_COOKIE' => headers['Cookie'],
    'rack.version' => [1,3],
    'rack.input' => StringIO.new(body),
    'rack.errors' => $stderr,
    # ...
  })
# ...

そこに行きます。サーバーを再起動してフォームを再送信しようとすると、ブログに最初の投稿が正常に作成されたことがわかります。

今回はWebサーバーを真剣にアップグレードしました。 RackアプリからのGETリクエストを受け入れるだけでなく、POSTリクエストを処理する完全なRailsアプリを提供するようになりました。そして、まだ合計50行を超えるコードを記述していません!

新しく改良されたサーバーを試してみたい場合は、次のコードを使用してください。詳細を知りたい場合、または具体的な質問がある場合は、@AppSignalまでお知らせください。


  1. MacOS で未検証のアプリを実行する方法

    Apple は、承認されたアプリのみを App Store からダウンロードすることを望んでいますが、それが常に可能であるとは限りません。インストールが承認されていない適切なアプリをオンラインで見つけた場合、macOS はそのアプリの起動をブロックします。このセキュリティ機能は意図的なものですが、サードパーティ アプリをインストールするにはバイパスする必要があります。 ありがたいことに、検証されていないアプリを Mac で実行するのは非常に簡単なプロセスです。始める前に、このセキュリティ対策には理由があることを思い出してください。信頼できるソースからのアプリのみをインストールすることを検討し

  2. Windows 11 で Android アプリを実行する方法

    Windows 11 PC で Android アプリを実行できるかどうか知りたいですか?さて、私たちの夢がついに実現したようです。サードパーティのソフトウェアやエミュレーターを使用せずに、Windows 11 で Android アプリを簡単に実行できます。 Windows の「Amazon アプリストア」アプリを使用すると、お気に入りのアプリやゲームにアクセスできます。 Microsoft はついに、PC で Amazon アプリストアを使用して、厳選されたカタログから Android アプリを参照およびインストールできるようにする新しい更新プログラムをリリースしました。ただし、ここに問題