大多数 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' dorun 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, :basicmanager.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 douse Rack::Session::Cookie, secret: "MY_SECRET"use Warden::Manager do |manager|manager.default_strategies :password, :basicmanager.failure_app = failure_append
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 MyApplicationclass 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 MyAppclass Application < Rails::Applicationconfig.middleware.insert_before 0, "Rack::Cors" doallow doorigins '*'resource '*',:headers => :any,:expose => ['X-User-Authentication-Token', 'X-User-Id'],:methods => [:get, :post, :options, :patch, :delete]endendend
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]

理解 Rack 应用及其中间件相关推荐

  1. 理解面向消息的中间件和JMS

    第二章:理解面向消息的中间件和jms 2.1企业级消息简介 企业级消息是在分布式系统之间进行传递的数据.当前有很多方法可以完成这一目的: 1.远程过程调用,例如COM.CORBA.DCE和EJB 2. ...

  2. 理解koa-router 路由一般使用

    阅读目录 一:理解koa-router一般的路由 二:理解koa-router命名路由 三:理解koa-router多个中间件使用 四:理解koa-router嵌套路由 五:分割路由文件 回到顶部 一 ...

  3. 深入理解分布式技术 - 消息幂等性如何保障不重复消费

    文章目录 概述 如何理解幂等 各类中间件对幂等性的处理 远程服务调用的幂等问题 消息消费中的重试问题 消息投递的几种语义 At most once At least once Exactly once ...

  4. django框架--中间件系统

    目录 零.参考 一.中间件的基本理解 二.中间件的系统定位 三.中间件的配置 四.中间件的执行流程 五.中间件与装饰器之间的思考 六.中间件的应用场景 七.内置中间件 八.总结 零.参考 https: ...

  5. TCP系列23—重传—13、RACK重传

    一.RACK概述 RACK(Recent ACKnowledgment)是一种新的基于时间的丢包探测算法,RACK的目的是取代传统的基于dupthresh门限的各种快速重传及其变种.前面介绍的各种基于 ...

  6. django中间件小知识

    浅谈django中间件小知识 下面这幅图是我从网上随便扒了一幅图,主要让读者能够大致知道django请求的生命周期. 下面这幅图主要用来说明从客户端请求进来到中间件之后的一个先后顺序以及一些原理性的东 ...

  7. 【微型Web框架(Ruby) Sinatra】

    Sinatra:一个优雅地包装了Web开发的DSL Sinatra程序的三个基本组成部分: 路由(route): '/' 就是路由.路由可以是单一的路径,或者带有参数的路径(比如 /:name),甚至 ...

  8. 我们真的缺前端工程师吗

    前言 这两天在好几个地方都看到了一篇关于为什么整个互联网行业都缺前端工程师?的文章,文章本身是去年的,中心思想是:其实我们并不缺前端工程师,我们缺的是优秀的前端工程师.我还是比较同意作者观点的,不过略 ...

  9. Express 搭建web服务器

    原文发表于我的个人博客,欢迎访问 安装Nodejs windows 1.打开node官网(nodejs.org) 2.直接点击 LTS 版本,下载安装包,Current版本是开发版,不要下载,如图 下 ...

  10. 爱玛士关于爬虫的scrapy框架的心得

    2019独角兽企业重金招聘Python工程师标准>>> 前言 Scrapy是爬虫必须学会的一个框架!一般人确实很难搞的透彻!不过他的功能方面确实特别好用. scrapy scrapy ...

最新文章

  1. 如何运行具有奇点的NGC深度学习容器
  2. GO语言教程1:Linux--debian/ubuntu下Go语言的安装
  3. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等...
  4. Burpsuite技巧之MD5加密密码爆破、带验证码爆破
  5. LeetCode 1570. 两个稀疏向量的点积(哈希)
  6. HDU 5919 分块做法
  7. Swift基础语法: 23 - Swift的Trailing闭包, 捕获, 闭包是引用类型
  8. leetcode28 Implement strStr() 在字符串中寻找目标字符串
  9. mysql传参为数组,将数组传递给MySQL存储例程
  10. Exchange Server 2007邮箱存储服务器的容量规划和性能调优(下)
  11. springmvc使用rest风格的404错误
  12. linux运维实战案例,Linux运维实战练习案例20151220~20151231
  13. struts2通配符的使用
  14. iftop流量实时查看
  15. 安装淘宝镜像cnpm时报错!npm ERR! enoent ENOENT: no such file or directory, rename
  16. kali liunx安装拼音输入法,必须成功,只需5步
  17. 大学物理实验长度的测量实验报告_大学物理实验长度测量
  18. 下载、预览PDF报错问题排查
  19. 通达oa显示服务器错误,服务器监控
  20. 游戏鼠标的dpi测试软件,自己就可以测试鼠标的DPI

热门文章

  1. 僵尸网络是什么;僵尸网络有什么特点
  2. windows 定时清理指定目录文件bat
  3. Google Admob 广告快速集成(并集成Firebase统计)
  4. 读文章《新阶级论:寒门难贵,豪门难收》
  5. librosa提取mel_fbank
  6. 在企业中TPM管理如何开展?
  7. 解决vs编译后运行提示“系统找不到指定的文件”的问题
  8. 【UOJ#60】【UR #5】怎样提高智商
  9. 爬虫-用xpath爬取豆瓣图书的短评
  10. 支付宝退款申请PHP,使用:4、退款查询