Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: SinatraのWeb Serverが起動後にURLにアクセスして処理が実行されるまでのコードを読んでみた。

Sinatraコードリーディングの第二回目です🎩 前回は、ruby app.rb実行後にServerが起動するまでのコードを読んでみましたが、今回はServerが起動したあとに特定のURLにアクセス際にapp.rbで定義した処理が実行されるまで、どのような動きになっているのかを追ってみました🕵️‍♀️

👇ちなみに前回の内容はこちらです。

madogiwa0124.hatenablog.com

今回のサンプルアプリケーション

ではまず、今回のサンプル実装を見ていきます。今回はなるべくシンプルにルート/にアクセスしたらHello Worldが標準出力されるようなアプリケーションで実際にルートパスにアクセスして標準出力されるまでを見ていきます。

require 'sinatra'

get '/' do
  puts "Hello World"
end

routingの定義

ではまずrouting(URLと処理の紐付け)を行っている。#getの実装を見てみることにします。このメソッドで実行してるのは下記の2つ。

  • 条件の取得・設定
  • routeの定義
# Defining a `GET` handler also automatically defines
# a `HEAD` handler.
def get(path, opts = {}, &block)
  # 条件の取得
  conditions = @conditions.dup
  # routeの定義
  route('GET', path, opts, &block)

  @conditions = conditions
  route('HEAD', path, opts, &block)
end

今回は条件を指定していないので、routeの定義を行っている#route見ていきます。 ここで行われているのは下記です。

  • pathに空文字が指定されていた場合に、singleton_classにempty_path_infoを定義
  • #compile!を実行してurlと処理を解決するためのsignature(URLと処理を持つもの)を取得して@routesに登録
  • 拡張機能(http://sinatrarb.com/extensions-wild.html)の実行
  • signatureを返却
def route(verb, path, options = {}, &block)
  # pathに空文字が指定されていた場合に、singleton_classに`empty_path_info`を定義
  enable :empty_path_info if path == "" and empty_path_info.nil?
  # sigature(URLパターンと処理の組み合わせ)の取得
  signature = compile!(verb, path, block, options)
  # routesへの追加
  (@routes[verb] ||= []) << signature
  # 拡張機能の実行
  invoke_hook(:route_added, verb, path, block)
  signature
end

@routessignatureが追加されることによりSinatraにapp.rbで定義したroutingが追加されるようですね👀

では実際のsignatureを取得している#compile!中身を見ていきます。 行っているのは下記です。ざっくりいうとオプションを解決して、URLと処理を持つオブジェクトを返しているようです。

  • host名やURLパターン、その他のオプションの反映
  • URLパターン解決用のオブジェクトを生成
  • verbとpathでメソッド名を生成(例: GET /)
  • メソッド名とapp.rbで定義したblockからメソッドを作成
  • メソッドを実行するProcオブジェクトをwrapperとして定義
  • URLパターン解決用のオブジェクト、条件、wrapperを返却
def compile!(verb, path, block, **options)
  # オプション周りの処理
  # Because of self.options.host
  host_name(options.delete(:host)) if options.key?(:host)
  # Pass Mustermann opts to compile()
  route_mustermann_opts = options.key?(:mustermann_opts) ? options.delete(:mustermann_opts) : {}.freeze

  options.each_pair { |option, args| send(option, *args) }

  # URLパターンマッチ用のオブジェクトの作成
  # https://github.com/sinatra/mustermann/tree/master/mustermann
  pattern                 = compile(path, route_mustermann_opts)
  # blockとHTTPメソッド、URLからメソッドを生成してwrapperに登録
  method_name             = "#{verb} #{path}"
  unbound_method          = generate_method(method_name, &block)
  conditions, @conditions = @conditions, []
  wrapper                 = block.arity != 0 ?
    proc { |a, p| unbound_method.bind(a).call(*p) } :
    proc { |a, p| unbound_method.bind(a).call }

  # URLパターンと条件とwrapper(処理)を配列にして返却
  [ pattern, conditions, wrapper ]
end

def compile(path, route_mustermann_opts = {})
  Mustermann.new(path, mustermann_opts.merge(route_mustermann_opts))
end

ここまでで、app.rbgetで定義した内容がGET /メソッドで実行されるようになり、URLパターンを解決するオブジェクトも作成されました🙌 Sinatraは、URLパターンの解析にMustermannという独自のGemを使っているようですね👀

github.com

リクエストの取得、処理の開始

ではここから具体的にリクエストが発生してどう定義したメソッドが実行されるのか見ていきます🕵️‍♀️ Rackが起動しているときに実行される処理が#callなので、そこから見ていきましょう。

To use Rack, provide an "app": an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements: https://rack.github.io/

色々やっているのですが、今回のURLの解決と処理の実行に関するところで#callでやっていることは下記の通りです。

  • Rackからの引数(env)を取得
  • リクエストのオブジェクト(Request < Rack::Request)を生成
  • dispatch!でURLパターンに紐づく処理を実行
  • responseを返却
# Rack call interface.
def call(env)
  dup.call!(env)
end

def call!(env) # :nodoc:
  # Rackから渡されたリクエストの内容を取得
  @env      = env
  @params   = IndifferentHash.new
  # Responseオブジェクトの生成
  @request  = Request.new(env)
  @response = Response.new
  template_cache.clear if settings.reload_templates

  @response['Content-Type'] = nil
  # 処理の実行
  invoke { dispatch! }
  invoke { error_block!(response.status) } unless @env['sinatra.error']

  unless @response['Content-Type']
    if Array === body and body[0].respond_to? :content_type
      content_type body[0].content_type
    else
      content_type :html
    end
  end

  @response.finish
end

@requestにURL等のリクエストの情報が格納され、#dispatch!で処理が実行されるようです👀 ちなみにenvに渡される値は下記のようなhashです。

{"GATEWAY_INTERFACE"=>"CGI/1.1",
 "PATH_INFO"=>"/",
 "QUERY_STRING"=>"",
 "REMOTE_ADDR"=>"::1",
 "REMOTE_HOST"=>"::1",
 "REQUEST_METHOD"=>"GET",
 "REQUEST_URI"=>"http://localhost:4567/",
 "SCRIPT_NAME"=>"",
 "SERVER_NAME"=>"localhost",
 "SERVER_PORT"=>"4567",
 "SERVER_PROTOCOL"=>"HTTP/1.1",
 "SERVER_SOFTWARE"=>"WEBrick/1.4.2 (Ruby/2.6.3/2019-04-16)",
 "HTTP_HOST"=>"localhost:4567",
 "HTTP_CONNECTION"=>"keep-alive",
 "HTTP_CACHE_CONTROL"=>"max-age=0",
 "HTTP_UPGRADE_INSECURE_REQUESTS"=>"1",
 "HTTP_USER_AGENT"=>
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
 "HTTP_ACCEPT"=>
  "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
 "HTTP_ACCEPT_ENCODING"=>"gzip, deflate, br",
 "HTTP_ACCEPT_LANGUAGE"=>"ja,en-US;q=0.9,en;q=0.8",
 "HTTP_COOKIE"=>
  "_dogfeeds_session=jU5sXy4p7LtgpehIempTj%2BTlernru9duDYax8nYGoPo%2BRTOkOK2hB0SY19cYYlCz32yoHKBx%2FFok8bGRc6oUZ%2BS51S2DqpnvvpU6ZB65looxi1wqJVm15tMJLw9BZa4Hon67ewTt7RxsgXDlNxM%3D--ZK%2FIVj5c7IQkes2%2F--9Tv%2Bcdzo8OAPtAGO%2BaofLw%3D%3D",
 "rack.version"=>[1, 3],
 "rack.input"=>#<StringIO:0x00007f86651ba308>,
 "rack.errors"=>#<IO:<STDERR>>,
 "rack.multithread"=>true,
 "rack.multiprocess"=>false,
 "rack.run_once"=>false,
 "rack.url_scheme"=>"http",
 "rack.hijack?"=>true,
 "rack.hijack"=>
  #<Proc:0x00007f86651b9cc8@sinatora_sample/vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/handler/webrick.rb:74 (lambda)>,
 "rack.hijack_io"=>nil,
 "HTTP_VERSION"=>"HTTP/1.1",
 "REQUEST_PATH"=>"/",
 "sinatra.commonlogger"=>true,
 "rack.logger"=>
  #<Logger:0x00007f86651b99d0
   @default_formatter=
    #<Logger::Formatter:0x00007f86651b9930 @datetime_format=nil>,
   @formatter=nil,
   @level=1,
   @logdev=
    #<Logger::LogDevice:0x00007f86651b9868
     @dev=#<IO:<STDERR>>,
     @filename=nil,
     @mon_count=0,
     @mon_mutex=#<Thread::Mutex:0x00007f86651b9750>,
     @mon_mutex_owner_object_id=70107599326260,
     @mon_owner=nil,
     @shift_age=nil,
     @shift_period_suffix=nil,
     @shift_size=nil>,
   @progname=nil>}

URL解決と処理の実行

では実際に処理が行われる#dispatch!を見ていきます。実行されるのは下記のような内容です。(Errorハンドリング等は一旦無視します😇)

  • requestのparamsをparamsに追加
  • #route!を呼び出し紐づく処理の実行
def dispatch!
  # requestのparamsをparamsに追加
  @params.merge!(@request.params).each { |key, val| @params[key] = force_encoding(val.dup) }

  invoke do
    # 静的ファイルへのアクセスに関する処理
    static! if settings.static? && (request.get? || request.head?)
    # こういうbefore_action的なやつの実行
    # before do
    # @note = 'Hi!'
    # request.path_info = '/foo/bar/baz'
    # end
    filter! :before
    # URLの解決と処理の実行
    route!
  end
rescue ::Exception => boom
  invoke { handle_exception!(boom) }
ensure
  begin
    filter! :after unless env['sinatra.static_file']
  rescue ::Exception => boom
    invoke { handle_exception!(boom) } unless @env['sinatra.error']
  end
end

#route!で実際に処理が実行されているので中身を見ていきます。下記が実行されているようです👀

  • request.request_method(GET etc...)を持つrouteを取得
    • routeの中身はcompile!で作ったURL解決用のオブジェクトと条件と処理の配列
    • [[#<Mustermann::Sinatra:"/">,[], #<Proc:0x00007ff033a1e7a0...]]
  • envの上書きと処理の実行を行うblock、URLパターン、条件を引数に#process_routeを呼び出す。
def route!(base = settings, pass_block = nil)
  # GETとかリクエストメソッド単位でrouteの一覧を取得
  if routes = base.routes[@request.request_method]
    routes.each do |pattern, conditions, block|
      取得したrouteの数だけ実行
      returned_pass_block = process_route(pattern, conditions) do |*args|
        env['sinatra.route'] = "#{@request.request_method} #{pattern}"
        route_eval { block[*args] }
      end
      # don't wipe out pass_block in superclass
      pass_block = returned_pass_block if returned_pass_block
    end
  end
end

def route_eval
  throw :halt, yield
end

いよいよ最後です!#process_route内のblock ? block[self, values] : yield(self, values)の部分でURLパターンの解決と処理の実行がされます!🎉

    def process_route(pattern, conditions, block = nil, values = [])
      # リクエストのURLの取得
      route = @request.path_info
      # routeが空文字だったら`/`に置換する。
      route = '/' if route.empty? and not settings.empty_path_info?
      # 最後に/がついてたら除去(-2は最後以外と取得する書き方)
      route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/')
      # パターンに一致しなかったらreturn
      return unless params = pattern.params(route)
      params.delete("ignore") # TODO: better params handling, maybe turn it into "smart" object or detect changes
      # encodingの設定
      force_encoding(params)
      # パラメーターの取得
      original, @params = @params, @params.merge(params) if params.any?
      # なんかパラメーターオブジェクトの属性をチェックして処理を切り替えてる?よくわからなかった・・・。
      regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? {|subpattern| subpattern.is_a?(Mustermann::Regular)} )
      if regexp_exists
        captures           = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c }
        values            += captures
        @params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty?
      else
        values += params.values.flatten
      end

      catch(:pass) do
        # 条件をクリアしなったら処理を抜ける
        conditions.each { |c| throw :pass if c.bind(self).call == false }
        # yieldで#routeから渡されたblockを実行
        block ? block[self, values] : yield(self, values)
      end
    rescue
      @env['sinatra.error.params'] = @params
      raise
    ensure
      @params = original if original
    end

ここで実際にapp.rbで定義した処理が実行されました!🎊

おさらい

大まかな流れをおさらいすると、

  • まずapp.rbにgetメソッドでURLと実行する処理をblockで定義する
  • #getメソッドで#routeメソッド呼び出し、#complile!メソッドが実行される。
  • complile!メソッド内で、URL解決用のオブジェクト、条件、Procオブジェクトを@routeに登録される。
  • Rackの仕組みでServerへリクエストが発生した際に#callが実行され、@request等の取得と#dispatch!メソッドが実行される
  • #dispatch!メソッドで、パラメーターが処理され、#route!メソッドが呼ばれる。
  • #route!は、@requestのメソッドからrouteの一覧を取得し、#route_eval!メソッドにblockを渡して実行するblock、URLパターン、条件を引数に#process_routeを呼び出す。
  • #process_routeが実際にURLの解決と条件のチェック、そして処理の実行を行う🎉

こんな感じのようですね!🕵️‍♀️

おわりに

実際に処理が実行される流れをみてみましたが、Server起動よりも結構複雑ですね💦(特にURL解決周り)
ですがRackを継承したSinatra独自のリクエストクラスを使っていたものの、リクエストの取得等がRackの部分で処理が行われていて以外とシンプルでした(Rackすごい・・・!)👀 引き続きRackは理解を深めていきたいですね💪

しかしSinatraでも結構複雑だからrailsはどうなってるんだろう🙄

次回はView(erb)の解決とResponseのbodyの生成部分を読んでみようと思います(これで一通り動きの概要は理解出来そうです)🙋

それでは👋

参考

sinatrarb.com

rack.github.io

qiita.com