読者です 読者をやめる 読者になる 読者になる

ほげほげ(仮)

仮死状態

Rackについて調べてみた

Ruby

RubyRailsもド素人ですが、よくRackという言葉を聞くのでちゃんと調べてみました。

下記の記事を読みながら簡単にまとめてみました。あとは実際にコードを書いて確認しながらやってみました。ちょっと記事が古いせいか一部うまく動かないコードは修正してみました。

試した時の環境

アプリケーションフレームワーク

アプリケーションサーバ

参考:https://github.com/rack/rack/blob/master/README.rdoc

ハンドラを持っているもの

サーバーモジュールとして動作するもの

WSGI、Rack、PSGI

WSGI

PythonのためのWebサーバーとWebアプリケーション/フレームワーク間とのインターフェースを定める仕様。WSGIに対応しているWebサーバーとフレームワークを自由に組み合わせられる。

Rack

RackはWSGIに影響され開発されたもの。インターフェースが統一されていれば、サーバやフレームワークの組み合わせは自由である。

PSGI

WSGI、Rackに影響されたPerlWSGI

最初のRackアプリケーション

Rackアプリケーションの条件

  • callというメソッド持っている
  • callメソッドの引数としてWebサーバからのリクエストを受けること
  • callメソッドは次の要素を含むレスポンスを返すること

Rackアプリケーション作ってみる

simple_app.rb
# -*- encoding: utf-8 -*-

class SimpleApp
  def call(env)
    p env
    case env['REQUEST_METHOD']
    when 'GET'
      [
        200,
        {'Content-Type' => 'text/html'},
        ['<html><body><form method="POST"><input type="submit" value="見たい?" /></form></body></html>']
      ]
    when 'POST'
      [
        200,
        {'Content-Type' => 'text/html'},
        ['<html><body>何見てんだよ</body></html>']
      ]
    end
  end
end
config.ru
# -*- encoding: utf-8 -*-

require './simple_app.rb'
run SimpleApp.new
動かす

rackをインストール

$ gem install rack

起動

$ rackup

WEBrickが起動して http://localhost:9292/ で動作が確認できる

thinで動かす場合は -s オプションで指定する

$ rackup -s thin

rackupとRack::Builder

config.ru

rackupファイルと呼ばれる。拡張子のruはおそらくrackupの略とのこと。

config.ruなしで動かす

config.ruがなくてもRackアプリケーションは起動できる

さっきのsimple_app.rbに下記を追加する(元の記事のままではうまく動かなかったので修正してある)

if __FILE__ == $0
  require 'rack/handler/webrick'
  Rack::Server.new(:app => SimpleApp.new, :Port => 9292, :server => 'webrick').start
  # Rack::Handler::WEBrick.run SimpleApp.new, :Port => 9292  これだとCtrl-Cで終了できなかった…
end

起動は通常のRubyと同じ

$ ruby simple_app.rb
  • この形式ではサーバー依存コードが残ってしまう。
  • ThinやMongrelはRack本体がハンドラを持っている場合は動くがPassengerのようにWebサーバのモジュールとして起動するものは動かない
  • rackupコマンド、サーバー側にrackupファイルを読んで起動させて、そちら側に任せたほうが良い

Rack::Builder

rackupファイルではRack::BuilderのDSLでRackアプリケーションの構成を記述できる

simple_app.rbのコードを次のようにする

if __FILE__ == $0
  require 'rack'
  app = Rack::Builder.new {
    run SimpleApp.new
  }
  Rack::Server.new(:app => app, :Port => 9292, :server => 'webrick').start
end

Rack::Builderのブロックの中身がrackupファイルと同じになっている。rackupコマンドはrackupファイルをRack::Builderに渡している・

基本的にはconfig.ruに色々書いて,アプリケーション側にはサーバ依存のコードは書かない。

Rack::Request/Rack::Response

リクエスト、レスポンスを簡単に扱えるようにRack::RequestとRack::Responseというラッパーがある。

simple_app.rbのコードをRack::RequestとRack::Responseを使って書き換えてみる。

# -*- encoding: utf-8 -*-

require 'rack/request'
require 'rack/response'

class SimpleApp
  def call(env)
    req = Rack::Request.new(env)

    body = case req.request_method
           when 'GET'
             '<html><body><form method="POST"><input type="submit" value="見たい?" /></form></body></html>'
           when 'POST'
             '<html><body>何見てんだよ。</body></html>'
           end

    res = Rack::Response.new do |r|
      r.status = 200
      r['Content-Type'] = 'text/html;charset=utf-8'
      r.write body
    end
    res.finish
  end
end

Rack::MockRequest

MockRequestを利用してテストコードを書くことができる。

simple_app_test.rb
# -*- encoding: utf-8 -*-

require './simple_app'
require 'test/unit'
require 'rack/mock'

class SimpleAppTest < Test::Unit::TestCase
  def setup
    @app = SimpleApp.new
    @mr = Rack::MockRequest.new(@app)
  end

  def test_get
    res = nil
    assert_nothing_raised('なんか失敗した') { res = @mr.get('/', :lint => true) }
    assert_not_nil res, 'レスポンス来てない'
    assert_equal 200, res.status, 'ステータスコードが変'
    assert_equal 'text/html;charset=utf-8', res['Content-Type'], 'Content-Typeが変'
    assert_match /見たい?/, res.body, '本文が変'
  end

  def test_post
    res = nil
    assert_nothing_raised('なんか失敗した') { res = @mr.post('/', :lint => true) }
    assert_not_nil res, 'レスポンス来てない'
    assert_equal 200, res.status, 'ステータスコードが変'
    assert_equal 'text/html;charset=utf-8', res['Content-Type'], 'Content-Typeが変'
    assert_match /何見てんだよ。/, res.body, '本文が変'
  end
end

テスト実行

$ ruby simple_app_test.rb

これを利用するとサーバーを起動することなく単体テストが可能となる。

Rackのテストをより簡単にできるようなRack::Testのようなライブラリもある。

Rack::URLMap

Rack::URLMapはRackの機能ではなく標準添付のアプリケーション。パスとアプリケーションのマッピングを保持してパスに応じてアプリケーションを振り分けてくれる。

config.ruを次のようにする

# -*- encoding: utf-8 -*-

require './simple_app'
require 'rack/lobster'

map '/simple' do
  run SimpleApp.new
end

map '/lobster' do
  # Rackをインストールすると
  # サンプルとして付いてくるアプリケーション
  # ちょっと面白い
  run Rack::Lobster.new
end

rackupで起動してhttp://localhost:9292/http://localhost:9292/simplehttp://localhost:9292/lobsterにアクセスして確認する。対応していないパスはNot Foundになる。

Rack::URLMapでは複雑なURLの解決は難しい。通常はアプリケーション、フレームワーク側で実装することになる。

ミドルウェアとは

別なアプリケーションをラップして、リクエストやレスポンスを加工したり、処理を切り換えたりするRackアプリケーション。

リクエスト、レスポンスをアプリケーションの実行前後で加工をする。URLの書き換え、エンコーディング変換、Cookie処理など。サーバーとアプリケーションの中間で処理を行う。

ミドルウェアの作りかた

ミドルウェアが満たさなければいけない条件

  • Rackアプリケーションの仕様を満たしていること
  • newの第一引数に他のRackアプリケーションを取ること
neco_filter.rb
# -*- encoding: utf-8 -*-

class NecoFilter
  def initialize(app)
    @app = app
  end

  def call(env)
    res = @app.call(env)
    length = 0
    res[2].each do |body|
      body.gsub!(/|||/) { "にゃ#{$&}" }
      length += body.bytesize
    end
    # 元の記事には無いが、Content-Lenghtを再設定しないとエラーになる
    res[1]['Content-Length'] = length.to_s
    res
  end
end
config.ru
# -*- encoding: utf-8 -*-

require './simple_app'
require './neco_filter'

use NecoFilter
run SimpleApp.new

アプリケーション、サーバーには一切手を加えずに汎用的な処理を行うことができる。アプリケーションとは独立して付けたり外したりすることが可能。

ミドルウェアもRackアプリケーション。ミドルウェアを複数重ねることもできる。

Rackベースのフレームワークで実装したアプリケーションに対しても使える。

config.ruは次のように書き換えることもできる。

# -*- encoding: utf-8 -*-

require './simple_app'
require './neco_filter'

run NecoFilter.new(SimpleApp.new)

useはRack::Builderで用意されたDSLの記法。後続のミドルウェア、アプリケーションを引数にとってnewしてHandlerの引数にする。

ミドルウェアとRack::URLMap

以下のようなconfig.ruがあった場合

# ここでuseしたミドルウェアはURLMapより前に処理を行なう
use MiddlewareA
use MiddlewareB

map '/a' do
  # ここでuseしたミドルウェアは
  # そのパスへのリクエストの時だけ処理を行なう
  use MiddlewareC
  run AppA.new
end

map '/b' do
  use MiddlewareD
  run AppB.new
end
  • /aへのリクエスト
    • MiddlewareA → MiddlewareB → Rack::URLMap → MiddlewareC → AppA
  • /bへのリクエスト
    • MiddlewareA → MiddlewareB → Rack::URLMap → MiddlewareD → AppB

これをRack::Builderを使わないで書く場合

MiddlewareA.new(
  MiddlewareB.new(
    Rack::URLMap.new(
      '/a' => MiddlewareC.new( AppA.new ),
      '/b' => MiddlewareD.new( AppB.new ),
    )
  )
)

複雑になるのでrackupファイルで記述したほうがスマート

便利なミドルウェア達

  • Rack::MethodOverride
    • POSTメソッドでDELETEやPUTの代用をしているようなクライアントからのリクエストをそのメソッドでのリクエストであるかのように書き換えてアプリケーションに渡してくれる
  • Rack::Static
    • 特定のパスへのリクエストをアプリケーションに渡さずに静的ファイルの内容を返す
  • Rack::Deflater
    • レスポンスをgzip圧縮する
  • Rack::Lint
    • リクエスト/レスポンスがRackの仕様に沿っているかをチェックしてくれる
  • Rack::ShowExceptions
    • アプリケーションが例外を出した際に発生箇所や例外の内容やリクエストの中身などを整形してくれる
  • Rack::Reloader
    • サーバ起動中にファイルが変更されたときに自動的に再読み込みしてくれる
  • Rack::Auth
    • 簡単な認証機構を提供してくれる
  • Rack::Session
    • 簡単なセッション機構を提供してくれる

ミドルウェアのすすめ

Rackの仕組みはWeb開発における基礎になる部分。Rackベースのフレームワークの中身を理解する足掛かりになる。

Rackのメリットはインターフェースが統一されることでWebサーバー/フレームワーク間の組み合わせを自由にできる。

環境に縛られた場合でもミドルウェアが充実すれば柔軟に対応できる。適材適所でサーバーやフレームワークを選択できる。

Rackいじりに興味がある人はまずはミドルウェアからやってみるのおすすめ。

まとめ

色々調べてみるとあまり難しくない仕様で、簡単なものはすぐ動かすことが出来ました。

元記事のままでは動かないコードがあって相当時間がかかりましたが…