Help us understand the problem. What is going on with this article?

Rack解説 - Rackの構造とRack DSL

More than 5 years have passed since last update.

Rackとは

RackはRubyによるWebアプリケーション開発のHTTP送受信処理を担当するモジュール(gem)で、Ruby on Railsを始めとする多くのWebフレームワークの一番下のレベルで利用されています。

https://github.com/rack/rack

http://rack.github.io/

本稿ではRackの基本的な部分を中心に説明します。

簡単なRackアプリケーション

まず基本を理解するため説明用の簡単なアプリケーションを作成します。

最初にrack gemのインストールが必要ですが、Ruby on Railsをインストールしている場合はすでに必須モジュールとして入っています。単独でインストールする場合は次を参考にして下さい。

http://qiita.com/higuma/items/b23ca9d96dac49999ab9#2-3

次にconfig.ruという名前の小さなファイルを作ります。これはRackのサーバ起動コマンドrackupの設定ファイルで、中身はRubyで記述します。

config.ru
class ShowEnv
  def call(env)
    [ 200,                                          # ステータス(Integer)
      { 'Content-Type' => 'text/plain' },           # レスポンスヘッダ(Hash)
      env.keys.sort.map {|k| "#{k} = #{env[k]}\n" } # body(StringのArray)
    ]
  end
end

run ShowEnv.new

これは私が以前Rails 3 Wayという本を勉強していた時に学習用として作ったもので、今でも動作確認用に時々使っています(Rackを本格的に扱う人の多くがこのようなものを一度は作っていると思います)。

操作方法は次の通りです。

  • config.ruがあるディレクトリからrackupを起動
  • ブラウザからlocalhost:9292にアクセス

するとブラウザには次のようなテキスト文書が表示されます。

GATEWAY_INTERFACE = CGI/1.1
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
HTTP_ACCEPT_ENCODING = gzip,deflate,sdch
HTTP_ACCEPT_LANGUAGE = ja,en;q=0.8,en-US;q=0.6
HTTP_CONNECTION = keep-alive
HTTP_HOST = localhost:9292
HTTP_USER_AGENT = Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/33.0.1750.152 Chrome/33.0.1750.152 Safari/537.36
HTTP_VERSION = HTTP/1.1
PATH_INFO = /
QUERY_STRING = 
REMOTE_ADDR = 127.0.0.1
REMOTE_HOST = localhost
REQUEST_METHOD = GET
REQUEST_PATH = /
...以下略...

見ての通りこれはサーバから渡されたCGI環境変数の一覧です。試しにブラウザからlocalhost:9292/foo/bar?abc=xyzと入力すると次のように応答します。

...
PATH_INFO = /foo/bar
QUERY_STRING = abc=xyz
...
REQUEST_PATH = /foo/bar
REQUEST_URI = http://localhost:9292/foo/bar?abc=xyz
...

Rackアプリケーション仕様

それではconfig.ruについて説明します。これは最小限ですがRackアプリケーションの仕様を満たしています。最初にRackアプリケーション仕様書のURLを示します。

http://rubydoc.info/github/rack/rack/master/file/SPEC

和訳もあります。これは初期の版の訳で(Rack 1.5追加の)hijack APIは含まれていませんが、WebSocketやストリーミングなどを使わない人にはかえってこの方が余分な記述がなく分かりやすいと思います。

http://w.koshigoe.jp/study/?%5BRuby%5D+Rack%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB%BB%C5%CD%CD

Rackアプリケーションは次の形式のオブジェクトです。

  • callメソッドに応答するオブジェクト
  • callの引数envはCGI環境変数(ハッシュ)
  • callの戻り値は[HTTPステータス番号, レスポンスヘッダ, ボディ]の形式
    • ステータス番号は3桁の整数
      • より正確にはto_iに応答してステータスを返すオブジェクト
    • レスポンスヘッダはハッシュ
      • より正確にはeachイテレータに応答しkey, valueの組をyieldするオブジェクト
    • ボディは次の3つのバリエーションのどれかひとつ
      • 文字列が要素の配列([文字列, 文字列, ... ])(上の例ではこれを使用)
      • to_pathメソッドを持つオブジェクト(ファイルを返す)
      • ストリーム(File-like)オブジェクト(ストリームから読んで返す)

仕様上はcallに応答するオブジェクトであることだけが必要条件です。そのためcallメソッドの定義とオブジェクトの生成方法にはいくつかバリエーションがあります。それぞれ簡単なコード例と共に示します。

インスタンスメソッドとして定義する場合

これが最もポピュラーです。この形式のRackアプリケーションは後述するRack DSLを使って操作することができます。

config.ru
class Foo
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end

run Foo.new     # newが必要

runはRackアプリケーションを起動する関数です(後で詳しく説明します)。

特異メソッドとして定義する場合

あまり用いられませんがこれも可能です。

config.ru
class Foo       # module Foo も可
  def self.call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end

run Foo         # newは付けない

lambdaを用いる場合

最後の例はたった一行ですが、通常のRubyプログラムでは見慣れないものを使っています。

config.ru
run lambda {|env| [200, {'Content-Type' => 'text/plain'}, ['Hello']] }
# run proc {|env| ... } でも同じ

lambda(またはproc)はRubyのグローバル関数で、手続きオブジェクト(Procのインスタンス)を生成します。手続きオブジェクトの実行メソッドがProc#callで、名前がRack仕様のcallと同じため実行できるという仕組みです。詳しくはRubyリファレンスをご覧下さい。

Rack設計者は最初からこのlambdaを使う事を考えてメソッド名にcallを使う仕様にしています。Rackリファレンスにはlambdaを使った文例がたくさん出てきますが、この点を把握していれば不自由なく読むことができます。

Rackアプリケーションの実行

Rackアプリケーションの実行はrun RACK_APPの形式で記述します(これはRack DSLと呼ばれる記法で、後で詳しく説明します)。callをクラスのインスタンスメソッドとして定義している場合はnewでインスタンスを生成します。

run ShowEnv.new

最後にRackに含まれているサンプルアプリケーションを紹介します(ロブスターのAAを表示します)。動作確認用にも便利です。

config.ru
require 'rack/lobster'
run Rack::Lobster.new

Rackアプリケーションの構成

最初は単独で動作するRackアプリケーションを示しましたが、より一般的には複数のRackアプリケーションを連結した構成を取ります。典型的なRackアプリケーションの模式図を示します。

endpoint <-> middleware[s] ... <-> (Rack handler) <-> (server)

ここで次の用語が出てきました。順番に説明します。

  • エンドポイント
  • ミドルウエア
  • Rackハンドラ

エンドポイント

エンドポイントは(サーバから一番離れた)末端のRackアプリケーションで、典型的にはWebアプリケーションそのものです。前章のShowEnvは簡単なエンドポイントの例ですが、RailsアプリケーションもRackエンドポイントです。

Ruby on Railsでrails newを用いて新しいアプリケーションを生成するとconfig.ruが作られます。最後の一行だけ示します。

config.ru
...
run Rails.application   # これが最終行

Rails.applicationはRackアプリケーション仕様に準拠し、サーバと直接ではなくRackという抽象インターフェースを介してHTTP送受信処理を行います。サーバの種類による違いは全てRackが吸収し、Rails側はどのサーバでも同じコードで記述できます。

Railsの内部でもRack仕様が採用されており、ルーティング設定(config/routes.rb)にRackエンドポイントを使うことができます。詳しくはRails Guidesをご覧下さい。

http://guides.rubyonrails.org/routing.html#routing-to-rack-applications

Rackに含まれているエンドポイントアプリケーションをいくつか示します。

  • Rack::File - 静的ファイルサーバ(ディレクトリリスティングなし)
  • Rack::Directory - 静的ファイルサーバ(ディレクトリリスティング付き)
  • Rack::Lobster - 動作確認用サンプル

ミドルウエア

エンドポイントとサーバの中間に位置するRackアプリケーションをミドルウエアと言います。典型的なミドルウエアとは次のようなものです。

  • callはクラスのインスタンスメソッドとして記述する
  • コンストラクタ引数に上流(Webアプリケーション側)Rackアプリケーションを取る

ミドルウエアは中間の各種フィルタ処理を担当します。Rack gemに含まれるRackアプリケーションの大部分はこのミドルウエアです。これも主なものからいくつか紹介します。

  • Rack::Static - 静的ファイルサーバ(各種フィルタ機能付き)
  • Rack::Deflater - 圧縮エンコーディング対応
  • Rack::ETag - HTTP ETagを処理
  • Rack::ConditionalGet - If-None-Match及びIf-Modified-Sinceの対応
  • Rack::MockRequest - テスト用モック(実際のHTTPを使わない)
  • Rack::Lint - プロトコルの実行時チェック(開発時は自動的に挿入される)
  • Rack::ShowExceptions - 例外時に詳細情報を表示(開発時は自動的に挿入される)
  • Rack::ContentLength - Content-Lengthをセットする
  • Rack::Cascade - 複数アプリケーションの分岐

Ruby on Railsアプリケーションは自分自身がRackエンドポイントですが、内部に独自のミドルウエアスタックを持っておりたくさんのミドルウエアが動作しています。これはrake middlewareで確認できます。

Railsの内部でRackミドルウエアがどのように利用されているかはRails Guidesに詳しく書かれています。

http://guides.rubyonrails.org/rails_on_rack.html

なお「ミドルウエア」という用語はRackアプリケーション全般という意味でも使われています(広義のミドルウエア)。本家サイトの'Rack Documentation'にも'Available middleware'の一覧中に(エンドポイントの)Rack::Fileが含まれています。

しかしエンドポイントとミドルウエアではRack DSLの書式がはっきりと異なります。本稿では以下両者を厳密に区別します。

Ruby on Railsの場合は違いがはっきりしており、ミドルウエアスタックの中にあるのがミドルウエア、ルーティング設定やコントローラアクションの戻り値に用いられる「末端」オブジェクトがエンドポイントです。

Rackハンドラ

環境や用途の違いによりサーバソフトウエアには様々な選択があります。開発中は主にWEBrickを用いますが、デプロイ先ではMongrelやThinなど様々な種類のサーバが用いられていることでしょう。

この環境の違いを吸収する部分がRackハンドラです。Rubyベースの環境で用いられるサーバにはRackハンドラが用意されており、それぞれ仕様の異なるサーバに対してRackの共通仕様を提供します。

開発環境ではrackupを起動すると自動的にWEBrickサーバとWEBrick用Rackハンドラが立ち上がります。またrackup -s thinのようにサーバ種類をthin/puma/webrick/mongrelから選択することもできます(詳しくはrackup -hでヘルプをご覧下さい)。

Ruby on Railsでの設定方法はRails Guidesをご覧下さい。

http://guides.rubyonrails.org/configuring.html

デプロイ先での設定はご利用のサービスが提供する情報を参照して下さい。ちなみに(私が利用している)HerokuはRackに完全対応しており、開発環境に用いたconfig.ruの設定でそのままgit pushすれば自動的に処理してくれます。

Rackハンドラは基本的に自分で設定するものではありませんが、config.ruにサーバの種類やポート番号などを直接設定することも可能です。ハンドラを直接指定した場合のコード例を示します。これはrun Rack::Lobster.newの内部動作とほぼ同じで、開発中はこれでも動きます。

config.ru
require 'rack/lobster'
Rack::Handler::WEBrick.run Rack::Lobster.new, Port: 9292

しかし環境が変わるといちいち修正しなければならず、さらにデプロイ先でどういうサーバを使っているか分からない場合もあります。config.ruにはサーバ依存情報は極力書かず、外部に任せるのが賢い選択です。

もっと詳しい解説は次をご覧下さい。

http://gihyo.jp/dev/serial/01/ruby/0024

Rack DSL

Rackアプリケーションの構成はconfig.ruに記述しますが、この際用いられるのがRack DSLと呼ばれる記法です。これはRack::Builderモジュールで実装されています。

DSL(ドメイン固有言語)という名前が付いてはいますが、もちろん実体はRubyコードに他なりません。

ここでは簡単なRackアプリケーションを作ります。構成は次の通りです。

Rack::Directory <-> Rack::Deflater <-> Rack::ETag <-> (Rack handler/server)

Rack::Directoryを使ってpublicディレクトリを静的ファイルサーバとして立ち上げます。ミドルウエアのRack::Deflaterで圧縮エンコーディングに対応し、Rack::ETagでHTTPヘッダにETagフィールドをセットします。先にコードを示します。

config.ru
use Rack::ETag
use Rack::Deflater
run Rack::Directory.new 'public'

ここでミドルウエアとエンドポイントの違いが重要になります。

  • ミドルウエアは接続順にuseで記述する
  • エンドポイントは最後にrunで記述する

useを使わずにrunだけで記述することも可能ですが、コードは次のようになります(この場合は一番内側がエンドポイント)。この程度ならまだ十分対応できますが、Ruby on Railsのように多数のミドルウエアを直列接続すると入れ子の山になります。

config.ru
run Rack::ETag.new(
  Rack::Deflater.new(
    Rack::Directory.new 'public'
  )
)

これを緩和するのがuse構文(実体はただの関数)で、ミドルウエアをサーバ側から順に記述します。useにより接続順序が登録され、その後Rack::Builderにより上記のrunだけを用いたコードと同機能のランタイムコードに変換して処理されます(後でどう変換されるか実際に示します)。

それでは少し改良してみましょう。ディレクトリは非公開に変更し、ディレクトリ(URLの最後が/)に対するアクセスに対してそのディレクトリのindex.htmlで応答します。これはRack::DirectoryをRack::Staticに変更すれば対応できます。

しかしここで一つ問題があります。Rack::Staticはエンドポイントではなくミドルウエア扱いのためrunでは直接実行できません。

これは前章でも説明しましたが、複数Rackアプリケーションの中間に位置するのが(狭義の)ミドルウエアで、コンストラクタの(デフォルト値のない必須の)第一引数としてアプリケーション側接続先のRackアプリケーションを取ります。

Rack::Staticは一部のサブディレクトリだけを静的サーバで対応する場合を想定し、エンドポイントではなくミドルウエアとして作られています。コンストラクタの仕様は次の通りです。

Rack::Static.new(app, options = {})

オプションハッシュの設定方法は次のリファレンスをご覧下さい。

http://rubydoc.info/github/rack/rack/Rack/Static

詳細についてはソースを直接読むことをお勧めします。実質80行足らずで難解なコードはありませんから経験のあるRubyプログラマなら分かると思います。内部でRack::Fileを使っていることもソースを見れば分かります。

ここでは詳細には立ち入りませんが、とにかくこれを見ると次のように記述すればよいと書いてあります。

use Rack::Static, urls: [''], root: 'public', index: 'index.html'

このuse構文にはコンストラクタの第一引数であるappがないことに注意して下さい。一般的なuseとrunの構文は次の通りです。

use MIDDLEWARE1 [, ARG, ...]
use MIDDLEWARE2 [, ARG, ...]
...
run ENDPOINT.new [ARG, ...]

MIDDLEWAREの後に引数を取る場合は直後に,が必要です。useは何か特殊な文のように見えてしまうため私もいまだによく間違えますが、useの実体は接続順序を登録するRubyの関数です。

Rack::Builderはこれをだいたい次のように変換して処理しています。生成が逆順になることと、ミドルウエア生成時に第一引数として上流Rackアプリケーションが自動挿入される点に注意して下さい。

app = ENDPOINT.new([ARG, ...])
...
app = MIDDLEWARE2.new(app [, ARG, ...])
app = MIDDLEWARE1.new(app [, ARG, ...])
Rack::Handler::[SERVER_NAME].run(app, ...)

それではRack::Staticに戻ります。Rack::Staticは実質的にエンドポイントの機能を持っていますが、コンストラクタの第一引数がappなのでエンドポイントにはなりません。そこで空のエンドポイントを作って対応します。

config.ru
# 空のエンドポイント
class NullEndPoint
  def call(env)
  end
end

use Rack::ETag
use Rack::Deflater
use Rack::Static, urls: [''], root: 'public', index: 'index.html'
run NullEndPoint.new

これでも動きますがもっとよい方法があります。エンドポイントはもっと簡単にlambdaで生成できます。最終版は次の通りです。

config.ru
use Rack::ETag
use Rack::Deflater
use Rack::Static, urls: [''], root: 'public', index: 'index.html'
run lambda {|env|}    # run proc {|env|} も可

この最後のlambdaが初心者泣かせの部分です(私も最初は何が何だかさっぱり...)。

個人的感想ですがlambdaを使う特殊記法があるのにこれをDSLと呼ぶのは抵抗があります。いっそ(test/unitライブラリのように)runを省略可能にして、省略した場合はRack::Builderが自動的にlambda {|env|}を追加して処理する仕様にした方が分かりやすかったと思います(ENDブロックを使えば可能)。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away