Rack 官网对于 Rack 的介绍比较简单,只是介绍了 Rack 的作用和基本的使用。不过也可能因为不复杂,所以才用简单的几段话介绍了 Rack。虽然我们不用了解 middleware 的调用原理也可以开发出能使用的 middleware,但是总有点不知所以然的感觉,所以抽空总结了下 Rack 中 middleware 的调用原理。其中可能有不正确的地方,欢迎大家指出
配置 config.ru 文件,定义好要用到的 middleware 和要 run 的 middleware,为什么有些 middleware 需要 run,有些需要 use,下面再详细介绍。下面是一个简单的调用。
# ./config.ru
app = Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
run app
接着执行rackup
命令应用就可以跑起来了。
同时还可以把 middleware 定义成一个类,但是要在初始化实例的时候初始化@app和定义一个 call 方法,并且在 call 方法中需要调用@app.call(env)
,如下:
# rack_demo.rb
require 'rack'
class Timing
def initialize(app)
@app = app
end
def call(env)
ts = Time.now
status, headers, body = @app.call(env)
elapsed_time = Time.now - ts
puts "Timing: #{env['REQUEST_METHOD']} #{env['REQUEST_URI']} #{elapsed_time.round(3)}"
return [status, headers, body]
end
end
app = proc do |env|
['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
end
Rack::Handler::WEBrick.run(Timing.new(app), :Port => 9292, :Host => '0.0.0.0')
执行ruby rack_demo.rb
就可以跑起一个服务了。
middleware 的具体使用和返回格式要求这里不详细介绍,可以参考Ruby Rack 及其应用。同时上面的两种使用方式的作用原理是一样的。下面再详细分析。
定义好 config.ru 配置文件后在当前目录执行rackup
命令,会去到 ruby 对应的 bin 目录执行文件。一般在 ruby 安装好后都会有这个二进制文件的,在我本地的位置是 ~/.rvm/gems/ruby-2.5.3/bin/rackup
# ~/.rvm/gems/ruby-2.5.3/bin/rackup
require 'rubygems'
version = ">= 0.a"
...
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('rack', 'rackup', version)
else
gem "rack", version
load Gem.bin_path("rack", "rackup", version)
end
上面源码会执行后面的 rack gem 下面的 rackup 二进制文件,其中的源码为:
#!/usr/bin/env ruby
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/bin/rackup
require "rack"
Rack::Server.start
主要是为了启动 Rack Server,那启动的过程又做了什么东西呢?
调用栈从 def self.start
=> initialize
=> parse_options
这一系列的调用只是为了初始化一个 Server,然后加上一些默认的 Options 配置,初始化后主要是加了如下的默认配置:
{
:environment => "development",
:pid => nil,
:Port => 9292,
:Host => "localhost",
:AccessLog => [],
:config => "config.ru"
}
其中 config 中的值 config.ru 就是默认的配置文件,然后就是实例执行 run 方法了。run 方法中调用了 wrapped_app 方法,这个方法主要是把那些 middleware 汇总为一个 app 的方法。沿着方法调用栈继续查看,其中build_app
方法比较重要。源码如下:
def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass, *args = middleware
app = klass.new(app, *args)
end
app
end
middleware[options[:environment]]
求得的值是 Rack 中默认的 middleware。遍历的块中每个 middleware 都会去创建一个实例,以 app 变量作为参数传入,这就是为什么每个 middleware 在 initialize 的时候都需要传入一个 app 变量,并初始化赋值给@app实例变量。上面的迭代遍历过程最后方法返回的 app 会变成如下的链式反应:
#<Rack::ContentLength:0x00007ff72d568200
@app=
#<Rack::Chunked:0x00007ff72d568ef8
@app=
#<Rack::CommonLogger:0x00007ff72d569ce0
@app=
#<Rack::ShowExceptions:0x00007ff72e940c28
@app=
#<Rack::Lint:0x00007ff72e941ee8
@app=
#<Rack::TempfileReaper:0x00007ff72e943018
@app=
#<StatusLogger:0x00007ff72d4a0570
@app=
#<StatusLoggear:0x00007ff72d4a1088
@app=
#<Proc:0x00007ff72d4a2820@/Users/Cain/code/ruby/rack/config.ru:30>>>>
这样通过调用app.call
可以调用到所有 middleware 的 call 方法。
如下例子:
#config.ru
class FirstMidd
def initialize(app)
@app = app
end
def call(env)
puts "1"
status, head, body = @app.call(env)
puts "7"
[status, head, body]
end
end
class SecondMidd
def initialize(app)
@app = app
end
def call(env)
puts "2"
status, head, body = @app.call(env)
puts "6"
[status, head, body]
end
end
class ThirdMidd
def initialize(app)
@app = app
end
def call(env)
puts "3"
status, head, body = @app.call(env)
puts "5"
[status, head, body]
end
end
class Top
def call(env)
puts "4"
[200, {'Content-Type' => 'text/plain'}, ["hello, this is a test."]]
end
end
use FirstMidd
use SecondMidd
use ThirdMidd
run Top.new
发送一个请求curl http://localhost:9292
后,服务器会会按照 => 1, 2, 3, 4, 5, 6, 7
的顺序输出。上面的执行顺序体现了一个调用栈的调用过程。
同时上面还有一个 app 参数值的传入,求 app 过程如下:
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
def build_app_and_options_from_config
...
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
@options.merge!(options) { |key, old, new| old }
app
end
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/builder.rb
def self.parse_file(config, opts = Server::Options.new)
options = {}
if config =~ /\.ru$/
...
app = new_from_string cfgfile, config
else
...
end
return app, options
end
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
最后会调用 new_from_string 这个方法。这个方法就是通过 eval 执行之前 config.ru 中的内容。其中Builder#to_app
方法执行了 app = @use.reverse.inject(app) { |a,e| e[a] }
把 config.ru
中 run 方法调用的那些 middleware 实例逐个迭代作为参数嵌入到 app 变量中,app 最终的效果如上面build_app
方法中最后的 app 变量的形式一样。最终这个 app 值作为 build_app 方法的实参调用,最终合并成最终链条。
上面留了个问题,为什么在 config.ru
中有些 middleware 需要用 use,而最后会有个 run 方法的调用。因为 run 实例中的 call 方法是在 server 里面最后调用的方法,call 方法按照状态,头部,body 的形式返回。和其它的一些 middleware 初始化@app,然后在 call 方法中执行@app.call(env)
还是有些区别的,所以在 Rack 里的实现就把 run 的那个 middleware 实例作为最终的 [status, headers, body] 形式返回给前面的 middleware 逐层去处理逻辑了。
Rack 是应用服务器和应用的连接桥梁,具体是怎么实现的呢?其实 Rack 只是按照一定的规则去找出应用服务器,然后通过执行其中定义好的run
方法,把上面链接好的迭代 app 传入服务器中去执行,从而达到中间桥梁的作用,主要的代码在Rack::Server#start
的时候 run 了应用服务器。如下:
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
def start
...
server.run wrapped_app, options, &blk
end
这行代码中,server 的调用过程如下:
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
def server
@_server ||= Rack::Handler.get(options[:server])
unless @_server
@_server = Rack::Handler.default
# We already speak FastCGI
@ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
end
@_server
end
如果没有配置对应的:server 选项,调用Rack::Handler.get
会返回 nil,然后调用Rack::Handler.default
去查找对应的 server。会通过pick ['puma', 'thin', 'webrick']
按照默认服务器名字的顺序去查找对应的服务器 handler。
如果 require 了 puma,而 puma 因为定义了一个覆盖 Handler 中 default 的方法,如下:
# /Users/Cain/.rvm/gems/ruby-2.5.3/gems/puma-3.12.0/lib/puma/rack_default.rb
require 'rack/handler/puma'
module Rack::Handler
def self.default(options = {})
Rack::Handler::Puma
end
end
这时 Rack::Handler.default
返回的就是 Rack::Handler::Puma
这个类。这时调用server.run wrapped_app, options, &blk
就相当于调用了Rack::Handler::Puma.run
,从而在 puma 中接到可以处理的请求后再执行app.call(env)
,就会出现一系列的链式调用。从而保证 middleware 都可以被调用到 call 方法,然后再次返回处理逻辑。
总结:Rack 还做了很多的其它处理工作,把 app 和 app server 给串联起来只是其中的一部分。总的来说就是当一个请求过来时,可以通过一个个的 middleware 链式的调用,完成一些列的功能调用。