Rails Rails 对请求的处理过程

realwol · 2015年01月07日 · 最后由 jasontang168 回复于 2015年04月27日 · 8475 次阅读

一个请求从开始到结束,Rails 对他做了什么?

请求从 Rack 到 Rails

Rack

详细介绍可以去这里,简单来说就是 A Ruby Webserver Interface,这里有一些链接,对于感兴趣的可以了解一下(下边的例子来自 rack 官方,我加入了一些自己的修改和理解性记录)。 github-wiki railscast 看完这些对 rack 就会有一个初步的认识。总结一下,rack 就是一个 ruby 写成的提供给支持网络请求的框架和服务程序的一个规范性程序。在 rack 的源代码里有一些针对在没有指定服务器的设定,详细的我们讲到 rack 源码的时候再进行分析。
下边代码就是一个示例:

# config.ru
require 'rack'
require 'rack/lobster'

run Rack::Lobster.new 

Middleware

Sample

或多或少了解过 rack,你就会知道上边的那个 Lobster 的例子,当然也会知道如何在 rack 中添加一个 middleware,暂且将名字命名为 shrimp.rb:

# shrimp.rb
class Shrimp
  SHRIMP_STRING = "it was supposed to be a walking shrimp..."

  def initialize(app)
    @app = app
    p 'in shrimp initialize '
  end 

  def call(env)
    puts SHRIMP_STRING
    @app.call(env) 
  end
end

# config.ru
require 'rack'
require 'rack/lobster'
require_relative 'shrimp'

use Shrimp
run Rack::Lobster.new 

这样,Shrimp 就被当作 middleware 来使用了,而相对的,Rack::Lobster 被当作是 app 来处理,当然,文件中 use 属于 rack 的命令,不过虽然 use 写在 run 之前,不过只有写 run 了,才会执行 use,因为 middleware 毕竟是机遇 app 之上运行的。我的实际代码中加入了 pry 进行调试,有兴趣也可以加入等价调试工具进行分步骤调试,这样一边看清楚清楚在这个 middleware 和 app 层的流动状况。这里的结果是:

"in shrimp initialize"
[2014-12-02 10:28:49] INFO  WEBrick 1.3.1
[2014-12-02 10:28:49] INFO  ruby 2.0.0 (2014-11-13) [x86_64-darwin14.0.0]
[2014-12-02 10:28:49] INFO  WEBrick::HTTPServer#start: pid=37492 port=9292

那么打开本地 9292 端口看看:

"in shrimp initialize"
[2014-12-02 10:28:49] INFO  WEBrick 1.3.1
[2014-12-02 10:28:49] INFO  ruby 2.0.0 (2014-11-13) [x86_64-darwin14.0.0]
[2014-12-02 10:28:49] INFO  WEBrick::HTTPServer#start: pid=37492 port=9292
it was supposed to be a walking shrimp...
127.0.0.1 - - [02/Dec/2014 10:33:33] "GET / HTTP/1.1" 200 592 0.0007
it was supposed to be a walking shrimp...
127.0.0.1 - - [02/Dec/2014 10:33:33] "GET /favicon.ico HTTP/1.1" 200 592 0.0004

这里就看到请求调用了 shrimp 中 call 方法,这就表示每个请求是首先经过 middleware 再到 app 中去的,而且,rack 会自动寻找 call 方法做响应,最后的@app.call(env) 就会将请求继续传给 app 来处理。 接下来,我们看 config.ru 修改成这样子,加入我们自己定义的一个继承自 rack::server 的 ServerTest 类来试试看,当然,config.ru 也会做出适当的变更:

# server_test.rb
require 'rack'

class ServerTest < ::Rack::Server
  def initialize
  end

  def default_options
    super.merge({
      Port: 9293
    })
  end

  def call(env)
    p 'i am in the server test'
  end
end

#config.ru
require 'rack'
require 'rack/lobster'
require_relative 'shrimp'
require_relative 'server_test'

use Shrimp
run ServerTest.new 

这样,再运行 rackup config.ru,注意,这次我们需要打开的是本地的9293端口,因为我们的 default_options 已经将默认 9292 端口重新设定,这样的设置在 rails 源码中也可以找到(全局搜索 3000?),运行之后,发现也是正常,只不过在打开页面之后会报状态的错误,这也就证明启动时可以的,不过还需要一些设置,这里就不做了,证明他是能够启动的就达到目的了。
总结起来就是说:

  1. 我们的 server_test 跟 Rack::Lobster 在本质上是等价的。
  2. shrimp 被作为 middleware 来使用,调用顺序在 app 之前。
  3. Rails 中的对应实现

这里仅用 webrick 举例说明,其他 server 等价,因为 rack 本身就是用来消除这种差异的标准化东西。
从启动命令开始:rails server,这里调用的是 railties/lib/rails/commands/commands_tasks.rb 中定义的 server 方法:

def server
  set_application_directory!
  require_command!("server")

  Rails::Server.new.tap do |server|
    # We need to require application after the server sets environment,
    # otherwise the --environment option given to the server won't propagate.
    require APP_PATH
    Dir.chdir(Rails.application.root)
    server.start
  end
end

最终调用的是 Rails::Server.new.start,这是不是很像 server_test.rb 文件中的 ServerTest.new.start? 结果就是这样的,我们通过这个方式启动了 Rails::Server,接下来,rack 就会主动去找加载路径下的 config.ru 文件,我们也可以在我们新建的 rails 项目中找到 config.ru 文件,这个文件的大致内容是这样的:

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)

# Start of the app server
run Rails.application

首先加载 config/environment 文件,再运行 Rails.application,跟例子中对比,就可以确定这个 Rails::application 是我们前边提到的 app。而 config/environment 文件是这样的:

# Load the Rails application.
require File.expand_path('../application', __FILE__)

# Initialize the Rails application when run Rails.application.
Blog::Application.initialize!

这里是加载 application 文件,然后再初始化 Rails.application。这里是初始化和加载很多的 middleware 类 app。而在 application 文件中有一部分内容是这样的:

module Blog
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

  end
end

这里就是文件里用到很多的 Blog::Application 的出处,同时这样也会出发 Rails::Application 的 inherited 方法:

class << self
  def inherited(base)
    super
    Rails.app_class = base
  end
end

这里 Rails.app_class 就被赋值为 Blog::Application,也就相当于在我们例子中被确立了 app 的位置。接下来 middleware 将请求交给你的时候(middleware 的运行原理跟 app 相同),由于 Blog::Application 中并没有 call 这个实例方法,那么 Rails::Application 的 call 方法就会被调用。

换言之,虽然我不知道是否推荐,不过如果我们在 Blog::Application 中定义一个实例方法 call(env),那么请求会先到这个 call 方法中,我们或许可以在里边做一些想要的方法,然后再 super 就不会影响既有功能,前提是你不要在加入的功能中影响后边进行。

# Implements call according to the Rack API. It simply
# dispatches the request to the underlying middleware stack.
def call(env)
  env["ORIGINAL_FULLPATH"] = build_original_fullpath(env)
  env["ORIGINAL_SCRIPT_NAME"] = env["SCRIPT_NAME"]
  super(env)
end

而 Rails::Application 的父类 Engine 的 call 方法也被调用:

# Define the Rack API for this engine.
def call(env)
  env.merge!(env_config)
  if env['SCRIPT_NAME']
    env["ROUTES_#{routes.object_id}_SCRIPT_NAME"] = env['SCRIPT_NAME'].dup
  end
  app.call(env)
end

# Returns the underlying rack application for this engine.
def app
  @app ||= begin
    config.middleware = config.middleware.merge_into(default_middleware_stack)
    config.middleware.build(endpoint)
  end
end

# Returns the endpoint for this engine. If none is registered,
# defaults to an ActionDispatch::Routing::RouteSet.
def endpoint
  self.class.endpoint || routes
end

# Defines the routes for this engine. If a block is given to
# routes, it is appended to the engine.
def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new
  @routes.append(&Proc.new) if block_given?
  @routes
end

上边也同时列举出了 call 方法中涉及的其他方法,在 routes 方法中,我们就很显然的发现了 ActionDispatch::Routing::RouteSet.new 这个身影的存在,这显然是在 middleware 都被运行完的时候,就把请求发送到 RouteSet 实例中,当然,我们能找到 RouteSet 也有一个 call 方法,且以 env 为唯一参数,ActionDispatch::Routing::RouteSet 的 initialize 和 call 方法代码如下:

def initialize(named_route, options, recall, set)
  @named_route = named_route
  @options     = options.dup
  @recall      = recall.dup
  @set         = set

  normalize_recall!
  normalize_options!
  normalize_controller_action_id!
  use_relative_controller!
  normalize_controller!
  normalize_action!
end

def call(env)
  req = request_class.new(env)
  req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
  @router.serve(req)
end

Rails 对我们的请求做了些什么?

这里的叙述建立在对 rack 有了解的基础之上,如果没有,这里你能得到一些基本的知识。 被发起的请求借助 rack 进入我们的 rails 系统,首先它进入了所谓的 middleware,在 rails 项目根目录下,rake middleware 可以帮助我们查看 rake 到底使用了哪些 middleware:

use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fdbb16875b0>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Blog::Application.routes

关键词"use"后边的,都是目前正在使用的 middleware,最后一行则表示运行的 app,middleware 和 app 的明显区别在于依附关系:middleware 依附于 app(执行顺序上的区别并不是那么明显,因为这取决于内部调用的顺序);将一个 middleware 设定为 endpoint 的时候,它就变成了一个 app。 关于以上的 middleware 的用途,这里是一篇 Ruby On Rails Guide 里关于 rack 的讨论,在其后半部分,有涉及。总体来说,就是做一些提前的处理和准备工作。还需要说明的是,middleware 虽然取名如此,可是并不是所有的 middleware 都是夹在中间来执行的,起码实际运行起来不是这样的,虽然严格意义上来说,app 是在最中间运行,运行结果会被一层层反馈上来直到 rack。画个图表示一下,大概就是这样:

# Middleware and app
def rack
  def middleware1
    # Do something in middleware1 before app
    def middleware2
      def app
        # Do something
      end
      # Do something in middleware2 after app
    end
  end
end

请求是怎么进入 controller 的?

我是个新手,经常有这种“I don't know, but it just worked"的感觉,这种感觉让人压力山大,同样的感觉我在看到这里的时候就有出现了:这个请求到底是怎么进入我写的 controller 里的呢? 根据已知条件,我们的请求在穿过层层 middleware 之后,到达了 Blog::Application.routes,这个 app 入口,而他调用的就是其祖先类 Engine 的 routes 方法:

# engine.rb
def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new
  @routes.append(&Proc.new) if block_given?
  @routes
end

至于为什么到这儿了,参考这篇文章的前半部分,相信你会明白。目前,我们到达了这里:ActionDispatch::Routing::RouteSet,源代码之:

# route_set.rb
def initialize(request_class = ActionDispatch::Request)
  self.named_routes = NamedRouteCollection.new
  self.resources_path_names = self.class.default_resources_path_names.dup
  self.default_url_options = {}
  self.request_class = request_class

  @append                     = []
  @prepend                    = []
  @disable_clear_and_finalize = false
  @finalized                  = false

  @set    = Journey::Routes.new
  @router = Journey::Router.new(@set, {
    :parameters_key => PARAMETERS_KEY,
    :request_class  => request_class})
  @formatter = Journey::Formatter.new @set
end

其中@router又被 Journey:Router.new 初始化:再源代码查看 Journey::Router:

# router.rb
def initialize(routes, options)
  @options       = options
  @params_key    = options[:parameters_key]
  @request_class = options[:request_class] || NullReq
  @routes        = routes
end

这里的@routes = routes,也就是传进来的参数@set,即:Journey::Routes.new,源代码如下:

# routes.rb
def initialize
  @routes             = []
  @named_routes       = {}
  @ast                = nil
  @partitioned_routes = nil
  @simulator          = nil
end

好了,代码暂且放在这儿,是时候梳理一下上边这几位的关系了。 或许观察仔细的玩家会发现我们运行 rails s 之后,会做很多准备工作,包括 routes 的生成,这些都是在启动过程中比运行一次,后续请求中不再重新编译的内容,这也就是为什么对 routes 等的改变需要重启才能生效的原因。而我们后续的请求都是在生成好的 routes 基础之上去匹配。 rails s 启动之后,从进入 server 开始,会有 engine based class 运行,其中最后一个就是生成 route_set.draw,接下来就生成 routes,然后保存起来。 做好这些准备,我们就开始等待请求送上门了,看,请求来了:首先 middleware 冲上去,等到了小红花被传到我们的 app 中时候,第一个接棒的是 route_set,第二个是 router,找到他的 call 方法:

# route_set
def call(env)
@router.call(env)
end

# Router
def call(env)
env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO'])

find_routes(env).each do |match, parameters, route|
  script_name, path_info, set_params = env.values_at('SCRIPT_NAME',
                                                     'PATH_INFO',
                                                     @params_key)

  unless route.path.anchored
    env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/')
    env['PATH_INFO']   = match.post_match
  end

  env[@params_key] = (set_params || {}).merge parameters

  status, headers, body = route.app.call(env)

  if 'pass' == headers['X-Cascade']
    env['SCRIPT_NAME'] = script_name
    env['PATH_INFO']   = path_info
    env[@params_key]   = set_params
    next
  end

  return [status, headers, body]
end

return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
end

# Find_route
def find_routes env
  puts "Hacked in find_routes"
  puts "#{__FILE__}/#{__LINE__}"
  req = request_class.new(env)

  routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
    r.path.match(req.path_info)
  }
  routes.concat get_routes_as_head(routes)

  routes.sort_by!(&:precedence).select! { |r| r.matches?(req) }

  routes.map! { |r|
    match_data  = r.path.match(req.path_info)
    match_names = match_data.names.map { |n| n.to_sym }
    match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) }
    info = Hash[match_names.zip(match_values).find_all { |_, y| y }]

    [match_data, r.defaults.merge(info), r]
  }
end

然后再加上负责查找的 find_route 方法,这下大家都看的很清楚了吧,就是这么通过 call 方法来传递的。然后,再加上我们从 router 的 call 方法中找到的金子般的一句:

status, headers, body = route.app.call(env)

最后

这里推荐一下 Rails 路由系统源码探索。跟本文配合起来看,会让你觉得更加顺畅。

#1 楼 @kikyous 谢谢支持。你是西安的?

学习了,支持一下西安的

#3 楼 @cifery 跟地域无关吧,觉得多少有点用就行。

#2 楼 @realwol 我在西安工作,我不是西安的

#7 楼 @kikyous 什么公司啊,聊聊?

错别字 机遇 app

#8 楼 @realwol 西安绿天生物技术有限公司

#11 楼 @kikyous 给个邮箱详谈。

#12 楼 @realwol 点我的名字,里面有邮箱

这个分析好

需要 登录 后方可回复, 如果你还没有账号请 注册新账号