理解 Rack 应用及其中间件

jopen 9年前

理解 Rack 应用及其中间件

大多数 web 开发者都是基于高度抽象出来的接口基础上编码,很多时候我们知其然但不知其所以然,特别是使用 Rails 框架开发时。

你是否研究过 Rails 内部对请求/响应周期是如何运作的?近期我意识到对于 Rack 和 middlewares 内部机制知之甚少,所以我花了一点时间来研究它。在这篇文章中分享了我的研究成果。

什么是 Rack

你知道 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 的工作机制

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 运行起来!

现在我们已经可以创建一个 Rack app 了,那我们该怎么去做让它起作用呢?第一步就要考虑添加一些中间件。

什么是中间件?

Rack 这么好是因为其易于添加一个连锁的中间件组件,它是在 web 服务器和 app 间通过你自定义的 request/response 方式添加的。但是什么是中间件组件呢?

中间件组件被设置在客户端和服务器之间,处理入站的请求和出站的回应。为什么你会想要做这些呢?Rack 有很多可用的中间件组件,比如推测可用的缓存,验证,捕获垃圾邮件等其他功能。

使用 Rack 中间件

在 Rack 应用中使用中间件,你所需要做的仅仅是告诉 Rack 使用它。使用多个中间件时,每个中间件将会改变请求或响应体,然后传递到下一个中间件。这一系列的中间件称之为中间件堆栈。

Warden

我们来看看如何增加 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 基本认证

一个很有用的中间件是 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

在 rails 中使用中间件

现在,那又怎样, Rack 是相当酷,并且我们知道 rails 是基于 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 Dispatch

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。

曾经我这样做过我的工作,读这篇文章会让你获得类似的经验。

附言:一个简单的 Rack 应用是怎么满足您的业务需求的? 在你的更大的应用中有什么其他方式集成进 Rack 应用。我们希望听你的战斗故事!给我们评论!