LoginSignup
47
49

More than 5 years have passed since last update.

サーバー側から見たRack

Posted at

Rackについて調べても、「ではまず簡単なRackアプリケーションを作ってみましょう。config.ruを開いて……」みたいな資料しか出てこなくて、サーバー側からみたRackインターフェースがどうなってるかという記事が全然見当たらなかった。んだけど、何やら外国の人のブログでちょうど望んでる内容のものがあったのでざっくり翻訳と紹介します(許可とった)。言い回しとかで解釈わかんないところがあって飛ばしたりしているので本文とニュアンスだいぶ変わってしまっているかもなのですが、まあ最低限の内容が伝われば……ご了承ください。
元記事:http://www.blrice.net/blog/2015/05/31/make-your-own-rack-server/

自分のRackサーバーを作ってみよう

Ruby触ってたら必然Rackについて学ぶよね。RailsとかSinatraとかRubyのWAFの心臓部分。Rackアプリケーションの作成の資料は山ほどあるんだけど、逆側についての資料を見かけたことがない。webサーバーはどうやってRackAppと会話してるんだろ。というわけでSinatraのために最小限のサーバーを書いてみた。

# my_server is the server I want to write
set :server, :my_server

get '/' do
  'Hello world!'
end

これをそのまま走らせると当然エラーになる。なぜかというとSinatraがRackにmy_serverを使わせてくれと頼んでも、Rackはそんなサーバーを知らないからだ。なので、まずRackにこのmy_serverを教えてあげる必要がある。

require 'rack'

# Stub out the server we're making
class MyServer
  def initialize(app)
    @app = app
  end

  def start
    # Handle requests
  end
end

module Rack
  module Handler
    class MyServer
      def self.run(app, options = {})
        server = ::MyServer.new(app)
        server.start
      end
    end
  end
end
Rack::Handler.register('my_server', 'Rack::Handler::MyServer')

Rackにサーバーについて教えるには、Handlerにサーバーの開始方法を書いたクラスを登録してあげれば良い。それはrunというRackアプリケーションのオブジェクトとサーバー設定値のハッシュを受け取る一つのメソッドを持つ。

さあ、あとはServerの中身の実装(これが一番大変)。

class MyServer
  STATUS_CODES = {200 => 'OK', 500 => 'Internal Server Error'}

  attr_reader :app, :tcp_server

  def initialize(app)
    @app = app
  end

  def start
    @tcp_server = TCPServer.new('localhost', 8080)

    loop do
      socket   = tcp_server.accept
      request  = socket.gets
      response = ''

      env = new_env(*request.split)
      status, headers, body = app.call(env)

      response << "HTTP/1.1 #{status} #{STATUS_CODES[status]}\r\n"
      headers.each do |k, v|
        response << "#{k}: #{v}\r\n"
      end
      response << "Connection: close\r\n"

      socket.print response
      socket.print "\r\n"

      if body.is_a?(String)
        socket.print body
      else
        body.each do |chunk|
          socket.print chunk
        end
      end

      socket.close
    end
  end

  def new_env(method, location, *args)
    {
      'REQUEST_METHOD'   => method,
      'SCRIPT_NAME'      => '',
      'PATH_INFO'        => location,
      'QUERY_STRING'     => location.split('?').last,
      'SERVER_NAME'      => 'localhost',
      'SERVER_POST'      => '8080',
      'rack.version'     => Rack.version.split('.'),
      'rack.url_scheme'  => 'http',
      'rack.input'       => StringIO.new(''),
      'rack.errors'      => StringIO.new(''),
      'rack.multithread' => false,
      'rack.run_once'    => false
    }
  end
end

もしあなたが普通のHTTPサーバーを書いたことがあれば上のコードの大体は同じようなものだとわかるだろう。ループしながらTCP接続を待って、接続が始まったら諸々必要な環境設定と受け取ったRequestとRackアプリケーションに投げて、アプリケーションから帰ってくるResponseをクライアントに返して接続を閉じる。もちろんこのサーバーは色々と簡素で実際の使用に耐えるものではない。

上のコードの中でRackに対応するためのコードはhashを返すnew_envメソッドだ。Rackアプリケーションはcallメソッドに返事をするただのオブジェクトにすぎない。このメソッドは現在の環境を表すハッシュの引数を一つ取る。この例では最低限の情報しかこのハッシュ煮詰めていないが、Rackの仕様を見ればより詳細なオプションがあることがわかる。キーポイントはRackアプリケーションは環境(を表現する)ハッシュを期待しているということと、それを提供するのがサーバー側の仕事ということ

このサーバーは明らかに簡素で機能は足りてないが、Rackを学ぶ足がかりには十分だろう。ちょっとRackのソースを読んだ後、なんて簡単なんだ、と思ったものだ。サーバー側から見ても、RackはRubyのwebアプリケーションの世界に対して本当にシンプルなインターフェースを提供してくれている。

47
49
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
49