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

Sinatraの仕組みを徹底解説!初期化からリクエスト処理までの内部構造

Sinatraは、Rubyで書かれた軽量Webフレームワークです。

Railsの弟分のような存在で、シンプルなDSLで手軽にWebアプリケーションを構築できます。

この記事では、Sinatraの内部構造を詳しく探っていきます

  • プロジェクトで require "sinatra" を実行すると何が起こるのか?
  • ルートマッチングはどのように動作するのか?
  • リクエストとレスポンスはどのように処理されるのか?

疑問は山ほどあるのに、時間は限られています…。

ご安心ください!面倒な調査はすでに済ませてあります。この記事でこれらの疑問に答え、より効率的に学べるようにまとめました。

Sinatraの初期化プロセス

すべては1つのファイル、sinatra.rbから始まります。

このファイルがやっているのはmain.rbの読み込みだけ。地味に思えますよね?

しかし、ここからが本番です!

main.rbの中にはbase.rbのrequireがあり、さらにオプション解析(ポート番号、環境、クワイエットモードなど)のコードも含まれています。

SinatraはRuby標準ライブラリのoptparseを利用しているのです。

ほかに注目すべき点として、次のat_exitブロックがあります。

at_exit { Application.run! if $!.nil? && Application.run? }

これはプログラム終了時に実行されるコードです。

Rubyは書かれたコードをすべて読み込みますが、無限ループやsleepなどがないため、プログラムは自然に終了します。

…しかし、その直前にat_exitブロックが発火するのです!

そしてSinatraが制御を引き継ぎ、リクエストを処理するためにWebサーバーを起動します。

実際のコードはこちらです

begin
  start_server(handler, server_settings, handler_name, &block)
rescue Errno::EADDRINUSE
  $stderr.puts "== Someone is already performing on port #{port}!"
  raise
end

# Part of base.rb `run!` method

さらに、ここでもう1つ重要な処理が行われています。

extend Sinatra::Delegator

Sinatra::Delegatorは、getpostsetといったSinatraのDSLメソッドを定義するモジュールです。

だからこそ、次のようなコードが書けるわけです。

get '/' do
  puts "Hello World!"
end

Sinatraはこのモジュールを使って、グローバルなmainオブジェクトを拡張しています。

リクエストとレスポンスの処理

ここまでで、新しい接続を受け付けられる状態のサーバーが起動しました。

では、新しい接続を受信したときには何が起こるのでしょうか?

実はSinatraは、Railsをはじめとする他のRuby Webフレームワークと同様に、低レベルの処理をRackに委譲しています。

Rackは、アプリケーション側にcallメソッドが定義されていることを期待します。これはRackの初期化時に渡すオブジェクトのことです。

Sinatraの場合、このオブジェクトはSinatra::Baseクラスにあたります。

そのメソッドがこちらです

# Rack call interface.

def call!(env)
  @env      = env
  @request  = Request.new(env)
  @response = Response.new

  invoke { dispatch! }
  invoke { error_block!(response.status) } unless @env['sinatra.error']

  @response.finish
end

# Modified version of Sinatra's call method (for clarity)

リクエストがどう処理されるのかを知るには、次にdispatch!メソッドを調べる必要がありそうです。

そのメソッドはこちらです

def dispatch!
  invoke do
    static! if settings.static? && (request.get? || request.head?)
    filter! :before
    route!
  end
rescue ::Exception => boom
  invoke { handle_exception!(boom) }
ensure
  filter! :after unless env['sinatra.static_file']
end

# Edited down to the important parts

リクエスト処理は、以下の4ステップに分けられます

  1. まず静的ファイル(CSS、JavaScript、画像など)の有無をチェック。「public」という名前のディレクトリが存在すれば、この設定はデフォルトで有効になります
  2. beforeフィルターが実行される
  3. ルートマッチングが行われる
  4. afterフィルターが実行される

それでは、各ステップの詳細を順番に見ていきましょう。

静的ファイルの配信

static!メソッドは非常にシンプルです。

def static!(options = {})
  return if (public_dir = settings.public_folder).nil?
  path = File.expand_path("#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}" )
  return unless File.file?(path)

  cache_control(*settings.static_cache_control) if settings.static_cache_control?
  send_file(path, options)
end

このコードは、リクエストされたファイルの存在を確認し、「Cache Control」HTTPヘッダーを設定します。

最後の行ではsend_fileを呼び出しています。まさに名前の通りの動作ですね。

Beforeフィルター

beforeフィルターを使うと、一致するルートを探索する前にコードを実行できます。

フィルターの追加は次のように実装されています。

# Define a before filter.

# Runs before all requests within the same context as route handlers
# and may access/modify the request and response.

@filters = {:before => [], :after => []}

def before(path = /.*/, **options, &block)
  add_filter(:before, path, options, &block)
end

def after(path = /.*/, **options, &block)
  add_filter(:after, path, options, &block)
end

def add_filter(type, path = /.*/, **options, &block)
  filters[type] << compile!(type, path, block, options)
end

ご覧のとおり、filtersは2つのキーを持つハッシュで、それぞれがフィルタータイプに対応しています。

では、compile!とは何でしょうか?

このメソッドは、「パターン」「条件の配列」「ラッパー」の3要素からなる配列を返します。

同じメソッドが、ルート生成時(getpostブロックを使うとき)にも利用されています。

def get(path, opts = {}, &block)
  route('GET', path, opts, &block)
end

def route(verb, path, options = {}, &block)
  signature = compile!(verb, path, block, options)

  (@routes[verb] ||= []) << signature

  signature
end

# Methods edited for clarity

つまり、Sinatraのフィルターはルートとまったく同じ仕組みで動作していることがわかります。

ルートマッチング

リクエスト処理サイクルの次のステップは、ルートマッチングです。

def route!(base = settings, pass_block = nil)
  routes = base.routes[@request.request_method]

  routes.each do |pattern, conditions, block|
    process_route(pattern, conditions)
    route_eval
  end

  route_missing
end

# Edited method

このコードは、リクエストメソッド(getpostなど)に対応するすべてのルートを順番に走査します。

ルートマッチングはprocess_routeメソッド内で行われます。

def process_route(pattern, keys, conditions, block = nil, values = [])
  route = @request.path_info
  route = '/' if route.empty? and not settings.empty_path_info?

  return unless match = pattern.match(route)
end

ここでpatternは正規表現です。

パスと条件の両方に一致するルートが見つかるとroute_evalが呼び出され、ブロック(get/postルートの本体)が評価されて、ルートマッチングの処理が終了します。

# Run a route block and throw :halt with the result.
def route_eval
  throw :halt, yield
end

ここでは、フロー制御に一般的ではないcatch/throwの仕組みが使われています。

コードの流れを追いづらくなるため、日常的な開発では使用をお勧めしませんが、実際のプロダクトでこの機能が活用されている例を見られるのは興味深いですね。

レスポンスの構築

リクエストサイクルの最後のステップは、レスポンスの準備です。

では、レスポンスはどこへ向かうのでしょうか?

invokeメソッドは、次のようにレスポンスを収集します。

res = catch(:halt) { yield }

この結果は、bodyメソッドを使ってレスポンスボディに代入されます。

body(res)

さて、出発点であるcallメソッドに目を戻すと、次の1行が見つかります。

@response.finish

これは@responseRack::Responseオブジェクト)に対してfinishメソッドを呼び出しています。

つまり、この瞬間に初めてレスポンスがクライアントへ送信されるのです。

おまけ:setメソッドの仕組み

setメソッドはSinatraのDSL(ドメイン固有言語)の一部で、アプリケーションの任意の場所で設定オプションを指定できます。

set :public_folder, '/var/www'

setを使うたびに、Sinatraはメタプログラミングによって3つのメソッドを動的に生成します。

define_singleton("#{option}=", setter) if setter
define_singleton(option, getter)       if getter
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"

生成される3つのメソッドは、public_folderを例に挙げると次の通りです。

  • public_folder
  • public_folder=
  • public_folder?

また、セッターメソッド(public_folder=)がすでに存在する場合は、それを呼び出す処理も行われます。

if respond_to?("#{option}=") && !ignore_setter
  return __send__("#{option}=", value)
end

ただし、メタプログラミングにはコストが伴うことを忘れないでください。筆者としては、素直にoptionsハッシュを使うことをお勧めします。凝ったメソッドは必須ではありません。

まとめ

この記事では、Sinatraの初期化の流れ、リクエストの処理方法、そしてレスポンスが生成されるまでの一連のステップを学びました。これらの知識は、Rubyの便利なテクニックの習得にも役立ち、Sinatraへの理解を深めてくれるはずです。

ぜひこの記事をシェアして、他のRuby開発者の方々にも学びの機会を提供してくださいね。

  1. Excelにデータ分析(Analysis ToolPak)をインストールする方法|Windows・Mac対応

    Microsoft Excelは、さまざまな種類のデータ分析に幅広く活用されています。複雑な統計解析やエンジニアリング分析を自力で行うには、多くの時間と手間がかかります。しかし、Excelの「データ分析」機能を活用すれば、こうした負担を大幅に軽減できます。ただし、この機能は初期状態では有効になっていないため、事前にインストール(有効化)が必要です。本記事では、Excelにデータ分析ツールを導入する手順を詳しく解説します。 データ分析ツール(Analysis ToolPak)とは? 「データ分析」ツール(Analysis ToolPak)は、Excelに標準搭載されているアドインの一つです。こ

  2. エンドポイントセキュリティとは?その仕組みと重要性を徹底解説

    エンドポイントセキュリティは、多様なセキュリティ対策を包含する広範な概念ですが、最も一般的にはネットワークセキュリティと密接に関連付けられています。エンドポイント保護(エンドポイントセキュリティとも呼ばれます)とは、企業のネットワークを、タブレット、ノートパソコン、スマートフォンなどのモバイルデバイスやリモート環境から守るための手法です。2018年には約1億件ものサイバー侵害が報告されており、サイバーセキュリティは決して軽視できない重要な課題となっています。本記事では、以下のトピックについて詳しく解説します。エンドポイントデバイスとは?エンドポイントセキュリティソフトウェアとは?Systwea