大多数 web 开发者都是基于高度抽象出来的接口基础上编码,很多时候我们知其然但不知其所以然,特别是使用 Rails 框架开发时。
你是否研究过 Rails 内部对请求/响应周期是如何运作的?近期我意识到对于 Rack 和 middlewares 内部机制知之甚少,所以我花了一点时间来研究它。在这篇文章中分享了我的研究成果。
你知道 Rails 是一个 Rack 应用吗?同时 Sinatra 亦然。那么请问什么是 Rack 呢?总而言之 Rack 就是对 Ruby 的 Net::HTTP 库的封装为一个 Ruby 包,这个包能够让开发者方便易用 Net::HTTP。
使用 Rack 能够快速新建一个简单的 web 应用。
首先,你需要一个能够响应 call 方法的对象,这个对象以一个环境变量哈希作为参数并且返回一个数组,返回的数组元素中包含 HTTP 响应码,响应头已经响应体。此时使用一个Ruby 服务器(例如Rack::Handler::WEBrick)即可启动服务端代码;或者你也可以把它放到一个单独的 config.ru 文件中,然后通过 rackup config.ru 命令启动服务。
很酷吧?那么 Rack 内部到底做了些什么呢?
Rack 实际上是为开发者提供开发服务器应用的一种途径,避免编写 boilerplate code,否则需要应用 Net::HTTP 底层库。如果你编写符合 Rack 规范的代码,那么可以通过 Ruby 服务器(WEBrick,Mongrel,Thin)来启动服务,以此来接收请求和响应请求。
Rack 提供多个方法启动,你可以在 config.ru 文件直接调用这些方法。
run
run 方法以一个应用程序(响应 call 方法的对象)为参数,下面这段代码是 Rack 官网中的例子
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
译者注:拷贝上述代码到 config.ru 文件中,然后在运行 rackup config.ru。同时 ruby 默认的服务器是 WEBrick,服务端口是 9292。服务启动之后运行 curl -X GET localhost:9292,启动的服务即能接收到请求并响应。
map
map 方法能处理一个指定的请求路径,如果请求路径符合指定路径,那么块中 Rack 应用程序代码将会执行。
map '/posts' do
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['first_post', 'second_post', 'third_post']] }
end
译者注:服务启动方法如上,此时请求路径为 curl -X GET localhost:9292/posts 服务能接收请求并响应。
use
use 方法告诉 Rack 使用指定的 middleware。
所以接下来你需要了解一些什么样的知识点呢?让我们接下来具体了解环境哈希和响应数组。
Rack 服务对象接收一个环境哈希,包含如下部分:
REQUEST_METHOD:HTTP 请求方法
PATH_INFO:相对于应用程序的请求路径
QUERY_STRING:请求URL中"?"问号后面的字符串
SERVER_NAME 和 SERVER_PORT:服务器地址和端口
rack.version:使用的 rack 版本号
rack.url_scheme:是 http 或者是 https?
rack.input:一个包含原生 HTTP POST 数据的 IO-like 对象
rack.errors:一个能够响应 puts,write 和 flush 的对象
rack.session:一个保存请求会话的健值对
rack.logger:一个提供打印日志接口的对象。包含 info,debug,ware,error 和 fatal 方法。
很多基于 rack 的框架把 env 哈希封装在 Rack::Request 对象中。这个对象提供了很多便于使用的方法,例如,request_method,query_string,session 和 logger,这些方法都返回上述列表列出来键的值。同时还允许开发者获取用户请求中的一些有用信息,例如请求参数,HTTP scheme 或者后台服务使用开启了 ssl? 查看源码https://github.com/rack/rack/blob/master/lib/rack/request.rb 可以完整的方法。
Rack 服务器对象响应一个请求,必须包含三个部分:响应状态,响应头和响应体。正如请求一样,Rack 内置的 Rack::Response对象同样也提供了方便易用的方法,譬如 write,set_cookie,finish 等方法。或者你也可以使用一个数组包含这三个必要元素。
就是 HTTP 状态码,例如 200,404
响应头的格式必须能够被 each 方法遍历,被 each 遍历出来的值应为一个健值对,键必须遵循 RFC7230 标准。例如在响应头中可以设置 Content-Type 和 Content-Length。
译者注:可参考上文 rack 工作机制的示例代码 {'Content-Type' => 'text/html'}
响应体就是服务器对用户请求发送的数据。响应体的格式必须能够被 each 方法遍历,并且 each 遍历出来的值应为字符串。
译者注:可参考上文 rack 工作机制中的示例代码 ['first_post', 'second_post', 'third_post']
现在我们已经可以创建一个 Rack app 了,那我们该怎么去做让它起作用呢?第一步就要考虑添加一些中间件。
Rack 这么好是因为其易于添加一个连锁的中间件组件,它是在 web 服务器和 app 间通过你自定义的 request/response 方式添加的。但是什么是中间件组件呢?
中间件组件被设置在客户端和服务器之间,处理入站的请求和出站的回应。为什么你会想要做这些呢?Rack 有很多可用的中间件组件,比如推测可用的缓存,验证,捕获垃圾邮件等其他功能。
在 Rack 应用中使用中间件,你所需要做的仅仅是告诉 Rack 使用它。使用多个中间件时,每个中间件将会改变请求或响应体,然后传递到下一个中间件。这一系列的中间件称之为中间件堆栈。
我们来看看如何增加 Warden 到一个项目中。Warden 在中间件堆栈中是在某种会话中间件之后被调用的位置,因此,我们在 Warden 之前使用 Rack::Session::Cookie 这个会话中间件。首先,增加代码: gem "warden" 到你的项目等 Gemfile 文件中,然后执行 bundle install。然后再添加以下代码到你的 config.ru 文件中。
require "warden"
use Rack::Session::Cookie, secret: "MY_SECRET"
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] }
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = failure_app
end
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
最后,执行命令 rackup 启动你的rack服务。Rack 将会找到你的 config.ru 文件启动服务,默认监听 9292 端口。
注意,想要使用 Warden 来作为应用的身份验证还需要更多的步骤,这里只是举例说明如何添加中间件到 Rack 程序的中间件堆栈中。想要查看更多典型的 Warden 集成的例子可以查看 代码片段 。
除了在 config.ru 文件中直接调用 use 命令来定义中间件堆栈,还有另一种方法。你可以使用 Rack::Builder 来包裹一系列的中间件或者代码块来生成一个应用。例如:
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] }
app = Rack::Builder.new do
use Rack::Session::Cookie, secret: "MY_SECRET"
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = failure_app
end
end
run app
一个很有用的中间件是 Rack::Auth::Basic,你可以通过它来使用 HTTP basic authentication 保护任何的 Rack 应用。它非常轻量级,非常便利。例如,Ryan Bates 就是使用它来保护 Resque 服务。参考:this episode of Railscasts.
以下是非常简单的配置代码:
use Rack::Auth::Basic, "Restricted Area" do |username, password|
[username, password] == ['admin', 'abc123']
end
现在,那又怎样, Rack 是相当酷,并且我们知道 rails 是基于 rack 构建的。但是我们仅仅知道它是什么,又不会在中实际中使用它写生产的应用程序。
你有没有注意到在 rails 项目文件中的根目录下有个名叫 config.ru 的文件。你有没有看过里面的内容,下面代码是它内容:
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application
很简单的几句代码。它只是加载 config/environment 文件,然后启动 rails 程序。等等, 那是什么?看一下 config/environment 里面的内容,我们可以看见它已经定义在 config/application.rb 文件里。config/environment 文件只是调用 initialize! 方法。
接下来 config/application.rb 文件又是干什么的呢?如果我们看了代码,它加载了 从config/boot.rb 文件里读取已经 bundled 的 gem 包,加载 rails 所有包,加载当前程序运行的环境(测试,开发,生产,等等),还定义了应用程序命名空间的版本号。它看起来像这样的:
module MyApplication
class Application < Rails::Application
...
end
end
那按照我的理解那意味着 rails 程序一定是 rack 应用了?果然是的,如果我们检出 rails 的源码。它响应 call!方法。
接下来是怎么使用中间件?我看它是自动加载了 rails/application/default_middleware_stack
这个文件,把这个文件拉下来,它看起来已经定义了在 ActionDispatch 模块里。ActionDispatch 是从哪来的呢?ActionPack 包吗?
Action Pack 是处理请求响应的 Rails 框架。它是Rails中为数不多的非常精密的组件,类似的还有:routing,虚拟控制器,页面渲染器。
大多数AP相关的讨论在这里 Action Dispatch。它提供了一系列的中间件来处理类似 ssl,cookies,调试,静态文件的问题。
去了解每一个 Action Dispatch 中间件,你就会发现它们都遵循着Rack规范:它们都提供 call 方法,接受 app 请求,返回 status, headers, 以及body。它们中的大部分还会使用Rack::Request,以及Rack::Response 对象。
通过阅读这些Rails组件的源码,揭开了Rails程序的神秘面纱。当我意识到Rails框架只是一群的遵循Rack规范的Ruby对象彼此间传递着请求和响应实体,这么看来Rails也就没有这么神秘了。
现在我们已经了解了Rack中间件的一些原理,下面我们来看看如何在Rails程序中引入自定义的中间件。
假设你在 Engine Yard 上部署了一个应用。你有一套 Rails API 跑在一个服务器上,基于 JavaScript 的客户端跑在另一个服务器。API 的地址为: https://api.example.com,客户端的是: https://app.example.com。
这时你将面临一个问题,根据 同源策略 你的 JS 客户端无法访问 api.example.com 的资源。你也许知道,这个问题的解决方案是开启 跨域资源共享 (CORS)。有很多种方法可以在你的应用中开启 CORS,最简单的莫过于使用 Rack::Cors middleware 这个 Gem。
在 Gemfile 中指定:
gem "rack-cors", require: "rack/cors"
Rails 提供了非常简单的方式去加载中间件。虽然我们也可以如前文所诉,在 config.ru 文件中用 Rack::Builder 块来加载,然而 Rails 的约定是写在 config/application.rb 文件中。代码如下:
module MyApp
class Application < Rails::Application
config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins '*'
resource '*',
:headers => :any,
:expose => ['X-User-Authentication-Token', 'X-User-Id'],
:methods => [:get, :post, :options, :patch, :delete]
end
end
end
end
注意,这里我们使用 insert_before 来确保 Rack::Cors 在 ActionPack 引入的中间件(以及你使用到的其他中间件)之前被调用。
重启服务之后,你的客户端应用就可以正常访问 api.example.com。
如果你希望了解更多关于 Rack in Rails 如何路由 HTTP 请求,我建议你看看这个部分 Rails 代码 ,这里很详细地说明 Rails 如何处理请求。
在这篇博文中,我们深入 Rack 的内部结构,并且扩展,请求(request)/回应(response)基于几个 Ruby 的 Web 框架,这也包括 Rails。
幸运的是,理解当一个请求到达服务器并且应用程序接收响应这个过程,你就会觉得这个过程少了一些魔法(magical)。我不了解你是怎么做的,但当我的事情出错,故障排除时,我明白发生了什么,这涉及到魔法(magical)。在那种情况下,我会说“哦,那就是 Rack 的回应”,并且靠这个来修复 bug。
曾经我这样做过我的工作,读这篇文章会让你获得类似的经验。
文章转载自 开源中国社区[https://www.oschina.net]