你可能听说过 Rails、Sinatra 这些 Ruby Web 框架,也可能尝试过其中一、两个,但如果你还不了解 Rack 甚至根本没听说过它,那么你的 Ruby Web 开发还停留在表面:Ruby Rack 是前面这些 Ruby Web 框架的基础,Rails 和 Sinatra 都建立在它之上;不了解 Rack 的原理就无法真正理解你的 Ruby Web 应用的架构与工作机制、对一些复杂的问题也无能无力。任何一个正经的 Ruby Web 开发者都应该了解、掌握 Rack。
本文将深入浅出地介绍 Ruby Rack 和它的一些典型应用,如 Profiler、Logger 和 Session 等。
我在《Web 全栈技术指南》中简单介绍过 Ruby Rack[^1]。下面的内容是它的扩充与深入。
Ruby Rack 是一个接口,用于 Ruby Web 应用与应用服务器之间的交互,如图所示:
最左边的 User Agent 就是浏览器等客户端,它发起 HTTP 请求;中间的 Rack Server 是应用服务器 [^2],它响应 HTTP 请求,并调用我们的 Rack 应用;最右边是我们的应用程序——它可能是一个 Rails 或者 Sinatra 应用。Rack 服务器和 Rack 应用程序之间通过 Rack 接口交互。
那么 Rack 接口是怎样的?就像这样:
# hello.rb - v0
app = proc do |env|
['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
end
这是一个最小的可以工作的 Rack 应用程序,它揭示了 Rack 接口:
call
方法的对象(任何类型的对象都可以,上面只是以 proc 为例)each
方法的对象,其结果将作为 HTTP 应答消息的主体(body)很简单不是么?(难的我都放在后面了,^_-)
只要再加两行代码,这个迷你的 Web 服务就能正式运行起来:
# hello.rb - v1
require 'rack'
app = proc do |env|
['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
end
Rack::Handler::WEBrick.run(app, :Port => 8090, :Host => '0.0.0.0')
在最后一行,我们用 Webrick[^3] 这个 Rack 服务器来 run 我们的 Rack 应用。
要运行上面的代码,先安装 Gem rack(如过你安装过 Rails 或者 Sinatra 那么它已经作为依赖被安装过了):
gem install rack
假设以上代码保存在文件hello.rb
中,执行
ruby hello.rb
就把我们的迷你服务器启动了。在浏览器中访问http://localhost:8090
,快试试!
Rack 不是那么简单:现在让我们了解一下强大的 Rack 中间件(middleware)。
以下是一个中间件的例子 [^4]:
# timing.rb - v1
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
我们可以这么使用它 [^5]:
# hello.rb - v2
require 'rack'
require './timing.rb'
app = proc do |env|
['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
end
Rack::Handler::WEBrick.run(Timing.new(app), :Port => 8090, :Host => '0.0.0.0')
快试试!看看现在我们的 Rack 应用有什么变化。
现在我来解释一下上面的程序。
Rack 中间件就是一个类,如上面的Timing
,其对象响应一个call
方法,这个方法的输入、输出规格与一般 Rack 应用一样。因此Timing.new(app)
可以作为一个 Rack 应用直接传递给Rack::Handler::WEBrick.run
。实际上,中间件可以这样一层套一层地层层嵌套下去,最后仍得出一个可以 call 的 Rack 应用。
Rack 中间件可以实现非常强大的功能。在上面的例子中,我们的 Timing 中间件为每一次调用计时,并把结果打印出来。这相当于一个 profiler。实际上中间件能做的事情更多:它可以检查内嵌应用程序@app
的输入、输出,还可以修改它们。因此它还可以用于鉴权(authentication/authorization)、日志,或者给内嵌应用提供一些额外的功能,如 Session 等等。稍后我们会看到两个实际的例子。
rackup 和 Rack::Builder 都是 Gem rack 提供的工具,方便我们使用、构造 Rack 应用。
仍以前面的 hello Rack 和 Timing 中间件为例,实际上,我们一般这样定义我们的 Rack 应用:
# config.ru
require './hello.rb'
require './timing.rb'
use Timing
run Hello
以上代码保存在一个名为config.ru
的文件中——它是 rackup 工具的缺省配置文件。
其中 hello.rb 的内容是:
# hello.rb - v3
class Hello
def self.call(env)
['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
end
end
这里我们定义了一个类Hello
,它有一个call
方法(回忆一下 Rack 的定义:任何响应call
方法的对象)。
我们不需要再编写初始化 Rack 中间件和启动 Rack 服务器的代码——rackup 工具会为我们完成。
一切就绪以后,在命令行执行(要在包含 config.ru 的目录下):
rackup
啊哈,我们的迷你服务器又启动了!
rackup 默认使用 Webrick 服务器,你也可以通过参数指定其他服务器。了解更多参数选项:
rackup -h
如果你想知道 rackup 是如何构造 Rack 应用、配置中间件的,你需要了解 Rack::Builder(Gem rack 安装目录下的 lib/rack/builder.rb)。具体代码这里就不做分析了。下面再举几个例子说明一下 config.ru 如何配置 Rack 应用和中间件。
如果你要使用多个中间件,可以:
# config.ru - multi-middlewares
require './app.rb'
require './middleware1.rb'
require './middleware2.rb'
require './middleware3.rb'
use Middleware1
use Middleware2
use Middleware3
run App
Rack::Builder 将依次应用这些中间件到App
上,得出一个最终的 Rack 应用,效果如同以下代码:
rack_app = Middleware1.new(Middleware2.new(Middleware3.new(App)))
你还可以在 config.ru 中配置路由,如:
# config.ru - routes
require './main.rb'
require './admin.rb'
require './m1.rb'
require './m2.rb'
map '/' do
use m1
run Main
end
map '/admin' do
use m2
run Admin
end
这样所有以/admin/
开头的请求都会交由Admin
处理,其余则由Main
处理。这种配置实际上开启了一种“Rack 组合”模式——由几个不同的 Rack 应用组成一个新的 Rack 应用。比如说:把一个 Rails 应用和一个 Sinatra 应用(它们都是标准的 Rack 应用)组合成一个新的 Rack 应用——脑洞很大,但完全可行!
另外,Rack 中间件是可以接受参数的——甚至可以带有 code block,比如:
# config.ru
require './hello.rb'
require './timing.rb'
use Timing, :pid => true, { puts "Timing is being initialized!" }
run Hello
这里的 timing.rb 内容如下:
# timing.rb - v2
class Timing
def initialize(app, opts = {}, &b)
@app = app
@pid = opts[:pid]
yield if block_given?
end
def call(env)
ts = Time.now
status, headers, body = @app.call(env)
elapsed_time = Time.now - ts
puts "Timing: #{Process.pid if @pid} #{env['REQUEST_METHOD']} #{env['REQUEST_URI']} #{elapsed_time.round(3)}"
return [status, headers, body]
end
end
Rails 和 Sinatra 都是标准的 Rack 应用框架——你可能已经注意到了,它们的项目根目录下一般都有一个 config.ru 文件。你可能会想:我从没编辑过这个文件,大概也就没有使用过中间件吧?错了!Rails 和 Sinatra 都可以在它们的应用程序内配置中间件,并且在缺省情况下已经为你配置了一大堆:
在一个 Rails 项目的根目录下运行:
bin/rails middleware
看看 Rails 为你配置的中间件栈有多深 [^6]:
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
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 Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Rails.application.routes
相比而言 Sinatra 要轻便许多:它有条件地配置了 4~7 个中间件(针对版本 v1.4.7),在 lib/sinatra/base.rb 中:
def setup_default_middleware(builder)
builder.use ExtendedRack
builder.use ShowExceptions if show_exceptions?
builder.use Rack::MethodOverride if method_override?
builder.use Rack::Head
setup_logging builder
setup_sessions builder
setup_protection builder
end
但是,不论这些 Rack 应用框架如何组织、定义自己的中间件栈,你都可以在 config.ru 中使用 Rack::Builder 所支持的标准语法来配置你的中间件——虽然一般情况下你不必这么做,但这样做有一个好处:你在 config.ru 中配置的中间件处于你的中间件栈顶部 [^7],也就是说,它最先响应服务器的请求、最后给出答案,因此具有最大的权威。
以为 Rack 就这么结束了?并没有!——我之前说过,Rack 没那么简单。前面我们只提了一下call
接受一个环境变量env
作为输入,并提到它包含了全部的 HTTP 请求信息,但并没有仔细讲讲它。现在是时候了——它很重要!
让我们检查一下 env 都包含些啥:
require 'rack'
app = proc do |env|
env.to_a.sort_by {|e| e[0] }.each {|k, v| puts %Q(#{k}=#{v}) }
[200, {}, []]
end
Rack::Handler::WEBrick.run(app, :Port => 8090)
这个简单的 Rack 应用会把 env 的内容都打印出来。照前面的样子启动它,然后访问它,你就能看到:
GATEWAY_INTERFACE=CGI/1.1
HTTP_ACCEPT=*/*
HTTP_HOST=localhost:8090
HTTP_USER_AGENT=curl/7.35.0
HTTP_VERSION=HTTP/1.1
PATH_INFO=/
QUERY_STRING=
REMOTE_ADDR=127.0.0.1
REMOTE_HOST=127.0.0.1
REQUEST_METHOD=GET
REQUEST_PATH=/
REQUEST_URI=http://localhost:8090/
SCRIPT_NAME=
SERVER_NAME=localhost
SERVER_PORT=8090
SERVER_PROTOCOL=HTTP/1.1
SERVER_SOFTWARE=WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25)
rack.errors=#<IO:0x0000000220dad0>
rack.hijack=#<Proc:0x000000024176c8@/home/user/.rvm/gems/ruby-2.3.0/gems/rack-1.6.4/lib/rack/handler/webrick.rb:76 (lambda)>
rack.hijack?=true
rack.hijack_io=
rack.input=#<StringIO:0x000000024180a0>
rack.multiprocess=false
rack.multithread=true
rack.run_once=false
rack.url_scheme=http
rack.version=[1, 3]
这是我从 localhost 上用 curl 访问的输出,你的也应该差不多。除了那些大写的 CGI[^8] 变量,还有一些 rack.xxx 变量,这些都是由 Rack 服务器设置并传递给 Rack 应用程序的。
CGI 变量大都可以顾名思义,前面的 Timing 中间件作为一个示例也用到了REQUEST_METHOD
和REQUEST_URI
,这里就不详细介绍了,感兴趣的读者可以参考脚注 [^8]。下面对 rack.xxx 变量做一些介绍:
rack.input
一个 IO 对象,可以读取 raw HTTP request。rack.errors
一个 IO 对象,用于错误输出。一般地,Rack 服务器会把它输出到服务器日志文件。它也是 Rack::Logger 和 Rack::CommonLogger 的输出对象。rack.hijack
、rack.hijack?
和rack.hijack_io
可以实现 websocket。rack.multiprocess
和rack.multithread
: 这两个对象指示了 Rack 应用的运行环境是否是多进程、多线程。这里需要着重说明一下:Rack 服务器可以根据负载情况同时启用 Rack 应用的多个实例,既有可能通过多进程(每个进程一个实例),也有可能通过多线程(一个进程,多个线程,每线程一个实例),还可能把二者结合起来(多进程,同时每个进程内多线程实例)。服务器具体通过什么方式启动应用,每种服务器都不一样,你需要查看服务器的文档说明。比如 Phusion Passeger 可以使用多进程或者混合模式(在企业版中);Unicorn 多进程;Thin 多线程(可配置)。一般来说使用多进程方式比较安全:如果要使用多线程,你不但要保证你的 Rack 应用是线程安全的,还要保证你用到的所有中间件都是线程安全的。rack.run_once
这个变量说明服务器是否只运行你的 Rack 应用实例一次就把它释放掉。这就是说服务器会对每个 HTTP 请求构造一个新的 Rack 应用实例(包括所有的中间件初始化工作)。一般来说只有 CGI 服务器会这样做 [^9](你肯定听说过 CGI 服务器效率不高吧?)。rack.url_scheme
http 或 httpsrack.version
Rack Spec 的版本(不是 Gem Rack 的版本)。我一直还没告诉你:Rack 不但是一个接口、一个 Gem 的名字,还是一个规范。一般而言你不必直接操作这些 rack.xxx 变量(也不应该这么做,除非你十分清楚这么做的后果,像作者这样^_-),但是你应该清楚它们的意义,这有助于你深刻理解 Rack 以及处理一些复杂问题。另外,Rack env 不但可以用于从 Rack 服务器向 Rack 应用和中间件传递一些信息,还可以用于在 Rack 中间件之间或者中间件与应用之间传递消息。在本文的下半部我们将看到这是如何实现的,以及这样做的意义。
Rack 的介绍到此为止,本文下半部将介绍一些 Rack 技术的常见应用,如 Profiler、Logger 和 Session 等中间件,了解它们是如何工作的。同时,欢迎你关注我的博客,获得更多技术资讯。
[^1]: Web 服务器->编程语言与技术->Ruby [^2]: 常见的 Rack 应用服务器有 Phusion Passenger,Unicorn,Thin,Puma 和 Webrick 等等。 [^3]: Webrick 一般用于开发环境,你的生产环境应该使用 Phusion Passenger 或者 Unicorn 等高性能的 Rack 服务器。 [^4]: 我喜欢直接从代码开始,我也建议读者手工输入并运行全部的代码示例,并且反复强调一点:技术文章不要只去读,要做! [^5]: 一般我们不这么用,后面的 rackup 一节会展示通常的用法,但二者的本质是一样的,只是表现形式不同。 [^6]: 更多关于 Rails on Rack 的信息可参考:Rails on Rack [^7]: 实际上 Rack 服务器也可以(而且并不少见)给 Rack 应用加上一些额外的中间件,用于输出 DEBUG 日志等一些工作。这些中间件在所有中间件栈的位置比 config.ru 中的还要“高”。 [^8]: CGI 即通用网关接口(Common Gateway Interface),我在《Web 全栈技术指南》的Web 服务器->编程语言与技术->CGI一节做过介绍。 [^9]: 细心的读者可能会问:Rack 应用难道不是对每个请求构造一个新的实例么?比如一个从 Sinatra::Base 继承来的类,对每次请求都会生成新的实例,成员变量也都重新初始化了。其实并没有!以 Sinatra 为例,它只是每次从初始化好的、无状态的 Rack 应用对象 dup 一个实例,用完就释放,下次再 dup 一个新的。具体你要看看 Sinatra 或者 Rails 的代码是如何做到的。