Ruby Rack及其应用(上)
原文链接 http://huiming.io/2016/11/10/ruby-rack.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
- 目录 {:toc}
前言
你可能听说过Rails、Sinatra这些Ruby Web框架,也可能尝试过其中一、两个,但如果你还不了解Rack甚至根本没听说过它,那么你的Ruby Web开发还停留在表面:Ruby Rack是前面这些Ruby Web框架的基础,Rails和Sinatra都建立在它之上;不了解Rack的原理就无法真正理解你的Ruby Web应用的架构与工作机制、对一些复杂的问题也无能无力。任何一个正经的Ruby Web开发者都应该了解、掌握Rack。
什么是Ruby Rack
Ruby Rack是一个接口,用于Ruby Web应用与应用服务器之间的交互,如图所示:
最左边的User Agent就是浏览器等客户端,它发起HTTP请求;中间的Rack Server是应用服务器1,它响应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为例) - 接受一个Hash类型的环境变量作为输入参数(它包含了全部的HTTP请求信息)
- 返回一个包含三个元素的数组,依次是:
- HTTP应答代码(status code)
- 一个Hash类型的对象,包含HTTP应答头部信息(header)
- 一个响应
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')
在最后一行,我们用Webrick2这个Rack服务器来run我们的Rack应用。
要运行上面的代码,先安装Gem rack(如过你安装过Rails或者Sinatra那么它已经作为依赖被安装过了):
gem install rack
假设以上代码保存在文件hello.rb
中,执行
ruby hello.rb
就把我们的迷你服务器启动了。在浏览器中访问http://localhost:8090
,快试试!
Rack Middleware
Rack不是那么简单:现在让我们了解一下强大的Rack中间件(middleware)。
以下是一个中间件的例子3:
# 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
我们可以这么使用它4:
# 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
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 on Rack
Rails和Sinatra都是标准的Rack应用框架——你可能已经注意到了,它们的项目根目录下一般都有一个config.ru文件。你可能会想:我从没编辑过这个文件,大概也就没有使用过中间件吧?错了!Rails和Sinatra都可以在它们的应用程序内配置中间件,并且在缺省情况下已经为你配置了一大堆:
在一个Rails项目的根目录下运行:
bin/rails middleware
看看Rails为你配置的中间件栈有多深5:
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中配置的中间件处于你的中间件栈顶部6,也就是说,它最先响应服务器的请求、最后给出答案,因此具有最大的权威。
Rack env
以为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访问的输出,你的也应该差不多。除了那些大写的CGI7变量,还有一些rack.xxx变量,这些都是由Rack服务器设置并传递给Rack应用程序的。
CGI变量大都可以顾名思义,前面的Timing中间件作为一个示例也用到了REQUEST_METHOD
和REQUEST_URI
,这里就不详细介绍了,感兴趣的读者可以参考脚注7。下面对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服务器会这样做8(你肯定听说过CGI服务器效率不高吧?)。rack.url_scheme
http或httpsrack.version
Rack Spec的版本(不是Gem Rack的版本)。我一直还没告诉你:Rack不但是一个接口、一个Gem的名字,还是一个规范。
一般而言你不必直接操作这些rack.xxx变量(也不应该这么做,除非你十分清楚这么做的后果,像作者这样^_-),但是你应该清楚它们的意义,这有助于你深刻理解Rack以及处理一些复杂问题。另外,Rack env不但可以用于从Rack服务器向Rack应用和中间件传递一些信息,还可以用于在Rack中间件之间或者中间件与应用之间传递消息。在本文的下半部我们将看到这是如何实现的,以及这样做的意义。
Rack的介绍到此为止,本文下半部将介绍一些Rack技术的常见应用,包括Auth、Session和Log等中间件,了解它们是如何工作的。同时,欢迎你关注我的博客和这个微博,获取更多技术资讯。
-
常见的Rack应用服务器有Phusion Passenger,Unicorn,Thin,Puma和Webrick等等。 ↩
-
Webrick一般用于开发环境,你的生产环境应该使用Phusion Passenger或者Unicorn等高性能的Rack服务器。 ↩
-
我喜欢直接从代码开始,我也建议读者手工输入并运行全部的代码示例,并且反复强调一点:技术文章不要只去读,要做! ↩
-
一般我们不这么用,后面的rackup一节会展示通常的用法,但二者的本质是一样的,只是表现形式不同。 ↩
-
更多关于Rails on Rack的信息可参考:Rails on Rack ↩
-
实际上Rack服务器也可以(而且并不少见)给Rack应用加上一些额外的中间件,用于输出DEBUG日志等一些工作。这些中间件在所有中间件栈的位置比config.ru中的还要“高”。 ↩
-
CGI即通用网关接口(Common Gateway Interface),我在《Web全栈技术指南》的Web服务器->编程语言与技术->CGI一节做过介绍。 ↩
-
细心的读者可能会问:Rack应用难道不是对每个请求构造一个新的实例么?比如一个从Sinatra::Base继承来的类,对每次请求都会生成新的实例,成员变量也都重新初始化了。其实并没有!以Sinatra为例,它只是每次从初始化好的、无状态的Rack应用对象dup一个实例,用完就释放,下次再dup一个新的。具体你要看看Sinatra或者Rails的代码是如何做到的。 ↩