JQ Blog

Rack Application

Rack起動

Rack gemをインストール

1
$ gem install rack

config.ru

1
2
3
4
5
6
7
8
9
10
11
class ShowEnv
  def call(env)
    [
      200,
      { 'Content-Type' => 'text/plain' },
      env.keys.sort.map { |k| "#{k} = #{env[k]}\n" }
    ]
  end
end

run ShowEnv.new

起動

1
$ rackup

起動確認

  • ブラウザでlocalhost:9292で接続
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
GATEWAY_INTERFACE = CGI/1.2
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
HTTP_ACCEPT_ENCODING = gzip, deflate, br
HTTP_ACCEPT_LANGUAGE = ko,ja;q=0.9,en-US;q=0.8,en;q=0.7
HTTP_CACHE_CONTROL = max-age=0
HTTP_CONNECTION = keep-alive
HTTP_HOST = localhost:9292
HTTP_SAVE_DATA = on
HTTP_UPGRADE_INSECURE_REQUESTS = 1
HTTP_USER_AGENT = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.45 Safari/537.36
HTTP_VERSION = HTTP/1.1
PATH_INFO = /
QUERY_STRING = 
REMOTE_ADDR = ::1
REQUEST_METHOD = GET
REQUEST_PATH = /
REQUEST_URI = /
SCRIPT_NAME = 
SERVER_NAME = localhost
SERVER_PORT = 9292
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = puma 3.11.2 Love Song
puma.config = #<Puma::Configuration:0x007ff652a60680>
puma.socket = #<TCPSocket:0x007ff652a09cb8>
rack.after_reply = []
rack.errors = #<Rack::Lint::ErrorWrapper:0x007ff651039450>
rack.hijack = #<Proc:0x007ff6510396f8@/Users/jo/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/rack-2.0.4/lib/rack/lint.rb:525>
rack.hijack? = true
rack.input = #<Rack::Lint::InputWrapper:0x007ff651039478>
rack.multiprocess = false
rack.multithread = true
rack.run_once = false
rack.tempfiles = []
rack.url_scheme = http
rack.version = [1, 3]

Rack Applicationの仕様

Rackアプリケーションの形式

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

オブジェクト生成方法の種類

  • インスタンスメソッドとして定義する場合
  • 特異メソッドとして定義する場合
  • lambdaを用いる場合

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

1
2
3
4
5
6
7
class Foo
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end

run Foo.new

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

1
2
3
4
5
6
7
class Foo       # module Foo も可
  def self.call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end

run Foo

lambdaを用いる場合

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

lambda(またはproc)の実行メソッドがProc#callで、名前がRack仕様のcallと同じため実行できるという仕組みになっている。

Rack Applicationの構成

一般的には複数のRackアプリケーションを連結した構成を取る。典型的なRackアプリケーションの模式図は下記のようになる。

1
endpoint <-> middleware[s] ... <-> (Rack handler) <-> (server)
  • エンドポイント
  • ミドルウエア
  • Rackハンドラ

エンドポイント

エンドポイントは(サーバから一番離れた)末端のRackアプリケーション。典型的にはWebアプリケーションそのものになる。RailsアプリケーションもRackエンドポイントである。 下記はRailsアプリケーションのconfig.ru

1
2
3
・・・

run Rails.application

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 - 複数アプリケーションの分岐

なお「ミドルウエア」という用語はRackアプリケーション全般という意味でも使われている(広義のミドルウエア)。

Rackハンドラ

環境や用途の違いによりthin/puma/webrick/mongrelなど様々のサーバの選択がある。この環境の違いを吸収する部分がRackハンドラである。Rubyベースの環境で用いられるサーバにはRackハンドラが用意されており、それぞれ仕様の異なるサーバに対してRackの共通仕様を提供する。
開発環境でrackupを起動する時、rackup -s webrickのように立ち上げるサーバ種類を選択することもできる。
config.ruにサーバの種類やポート番号などを直接設定することも可能。

1
Rack::Handler::WEBrick.run Foo.new, Port: 9292

Rack DSL

Rackアプリケーションの構成はconfig.ruに記述するけど、この際用いられるのがRack DSLと呼ばれる記法である。これはRack::Builderモジュールで実装されている。 ここで次のような構成を持っているアプリケーションを作ってみる。

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

Rack::Directoryは指定するディレクトリを静的ファイルサーバとして立ち上げる。
Rack::Deflaterは圧縮エンコーディングに対応する。
Rack::ETagでHTTPヘッダにETagフィールドをセットする。

config.ruを修正する。

1
2
3
use Rack::ETag
use Rack::Deflater
run Rack::Directory.new 'public'

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

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

useを使わずにrunだけで記述することも可能だが、下記のようにコードが増える。この程度だとまだ十分だけど、Ruby on Railsのように多数のミドルウエアを直列接続することを考えたらuseを使った方が良いであろう。

1
2
3
4
5
6
# useをrunに書き換えた場合
run Rack::ETag.new(
  Rack::Deflater.new(
    Rack::Directory.new 'public'
  )
)

上記のアプリケーションはpublickディレクトリを静的ファイルサーバとして立ち上げる。
directory

Rack::Staticを用いたらディレクトリ(URLの最後が'/‘)に対するアクセスに対してもディレクトリリスティングではなく、静的ページに対応できる。

1
use Rack::Static, urls: [''], root: 'public', index: 'test1.txt'

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

1
2
3
4
5
6
7
8
9
class Static

    def initialize(app, options={})
      @app = app
      @urls = options[:urls] || ["/favicon.ico"]

  ・・・

end

rack/static.rb at master · rack/rack · GitHub

1
2
3
4
5
6
7
8
9
10
# 空のエンドポイント
class NullEndPoint
  def call(env)
  end
end

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

指定したtest1.txtが表示される。
Alt text

参考

Rack解説 - Rackの構造とRack DSL - Qiita