详细介绍可以去这里,简单来说就是 A Ruby Webserver Interface,这里有一些链接,对于感兴趣的可以了解一下(下边的例子来自 rack 官方,我加入了一些自己的修改和理解性记录)。
github-wiki
railscast
看完这些对 rack 就会有一个初步的认识。总结一下,rack 就是一个 ruby 写成的提供给支持网络请求的框架和服务程序的一个规范性程序。在 rack 的源代码里有一些针对在没有指定服务器的设定,详细的我们讲到 rack 源码的时候再进行分析。
下边代码就是一个示例:
# config.ru
require 'rack'
require 'rack/lobster'
run Rack::Lobster.new
或多或少了解过 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?),运行之后,发现也是正常,只不过在打开页面之后会报状态的错误,这也就证明启动时可以的,不过还需要一些设置,这里就不做了,证明他是能够启动的就达到目的了。
总结起来就是说:
这里仅用 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
这里的叙述建立在对 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
我是个新手,经常有这种“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 路由系统源码探索。跟本文配合起来看,会让你觉得更加顺畅。