Rails Rails 对请求的处理过程

realwol · 发布于 2015年1月07日 · 最后由 jasontang168 回复于 2015年4月27日 · 4201 次阅读
4933

一个请求从开始到结束,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 路由系统源码探索。跟本文配合起来看,会让你觉得更加顺畅。

共收到 14 条回复
2564

学习了

4933

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

14957

学习了, 支持一下西安的

4933

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

2564

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

4933

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

2564

错别字 机遇app

4933

#9楼 @kikyous 没看懂。

2564

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

4933

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

2564

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

18464

这个分析好

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