【问题标题】:What is Rack middleware?什么是机架中间件?
【发布时间】:2011-01-16 10:15:10
【问题描述】:

什么是 Ruby 中的 Rack 中间件?对于“中间件”的含义,我找不到任何好的解释。

【问题讨论】:

  • RailsGuide 也有一个指南,现在全面覆盖 Rack,包括中间件:guides.rubyonrails.org/rails_on_rack.html
  • 非常感谢 PhusionPassenger 团队,他们的博客上有一篇解释清楚的文章。 rubyraptor.org/…
  • 机架和机架中间件在THIS 文章中进行了解释。还解释了如何创建基于机架的应用程序。

标签: ruby-on-rails ruby http web-applications rack


【解决方案1】:

机架设计

机架中间件不仅仅是“一种过滤请求和响应的方法” - 它是使用 Rack 的 Web 服务器的 pipeline design pattern 的实现。

它非常清晰地将处理请求的不同阶段分开 - 关注点分离是所有设计良好的软件产品的关键目标。

例如,使用 Rack,我可以将管道的各个阶段进行:

  • Authentication:当请求到达时,用户的登录信息是否正确?如何验证此 OAuth、HTTP 基本身份验证、名称/密码?

  • 授权:“用户是否有权执行此特定任务?”,即基于角色的安全性。

  • 缓存:我已经处理了这个请求,我可以返回一个缓存的结果吗?

  • 装饰:如何增强请求以使下游处理更好?

  • 性能和使用情况监控:我可以从请求和响应中获得哪些统计信息?

  • 执行:实际处理请求并提供响应。

能够分离不同的阶段(并且可以选择包括它们)对于开发结构良好的应用程序有很大帮助。

社区

还围绕机架中间件开发了一个很棒的生态系统 - 您应该能够找到预构建的机架组件来完成上述所有步骤以及更多操作。见the Rack GitHub wiki for a list of middleware

什么是中间件?

中间件是一个可怕的术语,它指的是任何帮助但不直接参与执行某些任务的软件组件/库。非常常见的示例是日志记录、身份验证和其他常见的水平处理组件。这些往往是每个人都需要跨多个应用程序的东西,但没有太多人有兴趣(或应该)构建自己。

更多信息

【讨论】:

  • 我不清楚的一点是:所有中间件是否共享相同的数据?是否可以将它们分开(即沙箱之一)以确保安全?
  • Rack 是您应用程序的一部分,因此所有中间件构成请求的相同副本,并且每个中间件都可以按照他们想要的任何方式对其进行修改。 AFAIK,没有办法对它们进行沙箱处理,因为无法在同一进程中将一个对象与另一个对象进行沙箱处理(尽管尝试了 Ruby 沙箱处理)。
  • 请理解 Rack 与 Rake 不同。
  • 我喜欢将中间件视为位于我的应用程序中间的任何东西,介于我编码的内容和进出我的服务器的内容之间......托管在机架空间上。众所周知,“机架中间件”一词之所以令人困惑,是因为孔子在 2000 多年前编写了所有原始机架中间件。在法国。
【解决方案2】:

首先,Rack 就是两个东西:

  • 网络服务器接口约定
  • 一颗宝石

Rack - 网络服务器界面

机架的基础是一个简单的约定。每个机架兼容的网络服务器总是会在你给他的对象上调用一个调用方法,并提供该方法的结果。 Rack 准确地指定了这个调用方法的外观,以及它必须返回的内容。那是架子。

让我们试一试。我将使用 WEBrick 作为机架兼容的网络服务器,但它们中的任何一个都可以。让我们创建一个返回 JSON 字符串的简单 Web 应用程序。为此,我们将创建一个名为 config.ru 的文件。 config.ru 将由 rack gem 的命令 rackup 自动调用,该命令将简单地在机架兼容的网络服务器中运行 config.ru 的内容。因此,让我们将以下内容添加到 config.ru 文件中:

class JSONServer
  def call(env)
    [200, {"Content-Type" => "application/json"}, ['{ "message" : "Hello!" }']]
  end
end

map '/hello.json' do
  run JSONServer.new
end

按照惯例,我们的服务器有一个名为 call 的方法,该方法接受环境散列并返回一个格式为 [status, headers, body] 的数组供网络服务器服务。让我们通过调用 rackup 来尝试一下。默认的机架兼容服务器,可能是 WEBrick 或 Mongrel 将启动并立即等待请求服务。

$ rackup
[2012-02-19 22:39:26] INFO  WEBrick 1.3.1
[2012-02-19 22:39:26] INFO  ruby 1.9.3 (2012-01-17) [x86_64-darwin11.2.0]
[2012-02-19 22:39:26] INFO  WEBrick::HTTPServer#start: pid=16121 port=9292

让我们通过 curl 或访问 url http://localhost:9292/hello.json 来测试我们的新 JSON 服务器,瞧:

$ curl http://localhost:9292/hello.json
{ message: "Hello!" }

它有效。伟大的!这是每个 Web 框架的基础,无论是 Rails 还是 Sinatra。在某些时候,他们实现了一个调用方法,遍历所有框架代码,最后以典型的 [status, headers, body] 形式返回响应。

例如,在 Ruby on Rails 中,机架请求命中 ActionDispatch::Routing.Mapper 类,如下所示:

module ActionDispatch
  module Routing
    class Mapper
      ...
      def initialize(app, constraints, request)
        @app, @constraints, @request = app, constraints, request
      end

      def matches?(env)
        req = @request.new(env)
        ...
        return true
      end

      def call(env)
        matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
      end
      ...
  end
end

所以基本上 Rails 会检查,如果有任何路由匹配,则依赖于 env 哈希。如果是这样,它会将 env 哈希传递给应用程序以计算响应,否则它会立即以 404 响应。因此,任何符合机架接口约定的 Web 服务器都能够为完全成熟的 Rails 应用程序提供服务。

中间件

Rack 还支持创建中间件层。他们基本上拦截一个请求,用它做一些事情并传递它。这对于多功能任务非常有用。

假设我们想向我们的 JSON 服务器添加日志记录,该服务器还测量请求花费的时间。我们可以简单地创建一个中间件记录器来执行此操作:

class RackLogger
  def initialize(app)
    @app = app
  end

  def call(env)
    @start = Time.now
    @status, @headers, @body = @app.call(env)
    @duration = ((Time.now - @start).to_f * 1000).round(2)

    puts "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} - Took: #{@duration} ms"
    [@status, @headers, @body]
  end
end

当它被创建时,它会为自己保存一份实际机架应用程序的副本。在我们的例子中,这是我们的 JSONServer 的一个实例。 Rack 自动调用中间件上的 call 方法,并期望返回一个 [status, headers, body] 数组,就像我们的 JSONServer 返回一样。

所以在这个中间件中,以起点为起点,然后使用@app.call(env) 进行对JSONServer 的实际调用,然后记录器输出记录条目,最后返回响应为[@status, @headers, @body]

要让我们的小 rackup.ru 使用这个中间件,像这样添加一个 use RackLogger:

class JSONServer
  def call(env)
    [200, {"Content-Type" => "application/json"}, ['{ "message" : "Hello!" }']]
  end
end

class RackLogger
  def initialize(app)
    @app = app
  end

  def call(env)
    @start = Time.now
    @status, @headers, @body = @app.call(env)
    @duration = ((Time.now - @start).to_f * 1000).round(2)

    puts "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} - Took: #{@duration} ms"
    [@status, @headers, @body]
  end
end

use RackLogger

map '/hello.json' do
  run JSONServer.new
end   

重新启动服务器,瞧,它会在每个请求上输出一个日志。 Rack 允许您添加多个按添加顺序调用的中间件。这只是在不更改机架应用程序核心的情况下添加功能的好方法。

机架 - 宝石

虽然 rack - 首先 - 是一种约定,但它也是一个提供强大功能的 gem。其中之一我们已经用于我们的 JSON 服务器,rackup 命令。但还有更多! rack gem 为许多用例提供了很少的应用程序,例如提供静态文件甚至整个目录。让我们看看我们如何提供一个简单的文件,例如一个位于 htmls/index.html 的非常基本的 HTML 文件:

<!DOCTYPE HTML>
  <html>
  <head>
    <title>The Index</title>
  </head>

  <body>
    <p>Index Page</p>
  </body>
</html>

我们可能想从网站根目录提供这个文件,所以让我们将以下内容添加到我们的 config.ru:

map '/' do
  run Rack::File.new "htmls/index.html"
end

如果我们访问http://localhost:9292,我们会看到我们的 html 文件完美呈现。这很容易,对吧?

让我们通过在 /javascripts 下创建一些 javascript 文件并将以下内容添加到 config.ru 来添加整个 javascript 文件目录:

map '/javascripts' do
  run Rack::Directory.new "javascripts"
end

重新启动服务器并访问http://localhost:9292/javascript,您将看到现在可以从任何地方直接包含的所有 javascript 文件的列表。

【讨论】:

  • 如果您不知道机架是什么,那么您将在阅读这篇博文后确切地知道它是什么以及如何使用它。很不错。然而,具有讽刺意味的是,文章末尾的官方机架文档的链接不再可用!
  • 你的权利,谢谢。我将内容包含在帖子中并删除了死链接。
  • 我会说这不是惯例。它是一个接口,一个为请求-响应模型定义良好的契约
【解决方案3】:

在很长一段时间内我都无法理解 Rack。我在自己制作miniature Ruby web server 之后才完全理解它。我在我的博客上分享了我对 Rack 的了解(以故事的形式):http://blog.gauravchande.com/what-is-rack-in-ruby-rails

非常欢迎您提供反馈。

【讨论】:

  • Link-only answers are discouraged on Stack Overflow,因为如果链接指向的资源将来变得不可用,答案就变得毫无用处。请至少总结您博客文章的相关要点并将其添加到此答案中。
  • 感谢您的帖子。我是一个非常初学者的 Rails 程序员,我通过你的清晰帖子理解了机架的概念。
  • 很棒的博文。 IMO 的其他答案似乎更令人费解。
  • 多么棒的解释。谢谢,高拉夫。
【解决方案4】:

什么是机架?

Rack 在支持 Ruby 和 Ruby 框架的网络服务器之间提供了一个最小接口。

使用 Rack 你可以编写一个 Rack 应用程序。

Rack 会将环境哈希(一个哈希,包含在来自客户端的 HTTP 请求中,由类似 CGI 的标头组成)传递给您的 Rack 应用程序,该应用程序可以使用此哈希中包含的内容来做任何想做的事情。

什么是机架应用程序?

要使用 Rack,您必须提供一个“应用程序”——一个以环境哈希作为参数(通常定义为 env)响应 #call 方法的对象。 #call 必须返回一个恰好包含三个值的数组:

  • 状态代码(例如“200”),
  • 标头哈希
  • 响应正文(必须响应 Ruby 方法 each)。

您可以编写一个返回此类数组的 Rack 应用程序 - 这将由 Rack 在 Response 内发送回您的客户端(这实际上是一个 instanceRack::Response [点击转到文档])。

一个非常简单的机架应用程序:

  • gem install rack
  • 创建一个config.ru 文件 - Rack 知道要查找此文件。

我们将创建一个微型 Rack 应用程序,它返回一个响应(Rack::Response 的实例),其响应正文是一个包含字符串的数组:"Hello, World!"

我们将使用命令rackup 启动本地服务器。

在浏览器中访问相关端口时,我们会看到“Hello, World!”在视口中渲染。

#./message_app.rb
class MessageApp
  def call(env)
    [200, {}, ['Hello, World!']]
  end
end

#./config.ru
require_relative './message_app'

run MessageApp.new

使用rackup 启动本地服务器并访问localhost:9292,您应该会看到“Hello,World!”渲染。

这不是一个全面的解释,但基本上这里发生的是客户端(浏览器)通过本地服务器向 Rack 发送 HTTP 请求,Rack 实例化 MessageApp 并运行 call,传入环境哈希作为方法的参数(env 参数)。

Rack 获取返回值(数组)并使用它创建Rack::Response 的实例并将其发送回客户端。浏览器使用magic 打印“Hello, World!”到屏幕上。

顺便说一句,如果您想查看环境哈希的样子,只需将puts env 放在def call(env) 下方即可。

虽然很小,但您在这里编写的是一个 Rack 应用程序!

使机架应用程序与传入环境哈希交互

在我们的小 Rack 应用中,我们可以与 env 哈希进行交互(有关环境哈希的更多信息,请参见 here)。

我们将实现用户将自己的查询字符串输入到 URL 中的功能,因此,该字符串将出现在 HTTP 请求中,封装为环境哈希的键/值对之一中的值。

我们的 Rack 应用将从环境哈希中访问该查询字符串,并通过响应中的正文将其发送回客户端(在本例中为我们的浏览器)。

来自环境哈希的机架文档: “QUERY_STRING:请求 URL 中 ? 之后的部分(如果有)。可能为空,但始终是必需的!”

#./message_app.rb
class MessageApp
  def call(env)
    message = env['QUERY_STRING']
    [200, {}, [message]]
  end
end

现在,rackup 并访问 localhost:9292?hello?hello 是查询字符串),您应该会在视口中看到“hello”。

机架中间件

我们会:

  • 在我们的代码库中插入一个 Rack Middleware - 一个类:MessageSetter
  • Environment 哈希将首先命中此类,并将作为参数传入:env
  • MessageSetter 将在 env 哈希中插入一个 'MESSAGE' 键,如果 env['QUERY_STRING'] 为空,则其值为 'Hello, World!'env['QUERY_STRING'] 如果没有,
  • 最后,它将返回 @app.call(env) - @app 成为“堆栈”中的下一个应用程序:MessageApp

首先,'长手'版本:

#./middleware/message_setter.rb
class MessageSetter
  def initialize(app)
    @app = app
  end

  def call(env)
    if env['QUERY_STRING'].empty?
      env['MESSAGE'] = 'Hello, World!'
    else
      env['MESSAGE'] = env['QUERY_STRING']
    end
    @app.call(env)
  end
end

#./message_app.rb (same as before)
class MessageApp
  def call(env)
    message = env['QUERY_STRING']
    [200, {}, [message]]
  end
end

#config.ru
require_relative './message_app'
require_relative './middleware/message_setter'

app = Rack::Builder.new do
  use MessageSetter
  run MessageApp.new
end

run app

Rack::Builder docs 我们看到Rack::Builder 实现了一个小的DSL 来迭代构建Rack 应用程序。这基本上意味着您可以构建一个由一个或多个中间件和一个“底层”应用程序组成的“堆栈”来调度。通过您的底层应用程序的所有请求将首先由您的中间件处理。

#use 指定要在堆栈中使用的中间件。它将中间件作为参数。

机架中间件必须:

  • 有一个将堆栈中的下一个应用程序作为参数的构造函数。
  • 响应将 Environment 哈希作为参数的 call 方法。

在我们的例子中,“中间件”是 MessageSetter,“构造函数”是 MessageSetter 的 initialize 方法,堆栈中的“下一个应用程序”是 MessageApp

所以在这里,由于 Rack::Builder 在幕后所做的事情,MessageSetterinitialize 方法的 app 参数是 MessageApp

(在继续之前先了解一下上面的内容)

因此,每个中间件本质上都将现有的环境哈希“传递”到链中的下一个应用程序 - 因此您有机会在中间件中改变该环境哈希,然后再将其传递给堆栈中的下一个应用程序.

#run 接受一个参数,该参数是一个响应#call 的对象并返回一个机架响应(Rack::Response 的一个实例)。

结论

使用Rack::Builder,您可以构建中间件链,并且对您的应用程序的任何请求都将依次由每个中间件处理,然后最终由堆栈中的最后一个部分(在我们的例子中为MessageApp)处理。这非常有用,因为它将处理请求的不同阶段分开。就“关注点分离”而言,它再干净不过了!

您可以构建一个由多个中间件组成的“请求管道”,这些中间件处理诸如:

  • 身份验证
  • 授权
  • 缓存
  • 装饰
  • 性能和使用情况监控
  • 执行(实际处理请求并提供响应)

(高于此线程另一个答案的要点)

您会经常在专业的 Sinatra 应用程序中看到这一点。 Sinatra 使用 Rack!请参阅 here 了解 Sinatra IS!的定义!

最后一点,我们的config.ru 可以写成简写形式,产生完全相同的功能(这是您通常会看到的):

require_relative './message_app'
require_relative './middleware/message_setter'

use MessageSetter
run MessageApp.new

为了更明确地展示 MessageApp 正在做什么,这里是它的“长手”版本,它明确地表明 #call 正在创建一个 Rack::Response 的新实例,并带有所需的三个参数。

class MessageApp
  def call(env)
    Rack::Response.new([env['MESSAGE']], 200, {})
  end
end

有用的链接

【讨论】:

  • 非常感谢您的详细解释。
  • 哈哈谢谢@ThomasDeranek!我不得不承认我有时想知道为什么它没有更多的赞成票大声笑
【解决方案5】:

config.ru 最小可运行示例

app = Proc.new do |env|
  [
    200,
    {
      'Content-Type' => 'text/plain'
    },
    ["main\n"]
  ]
end

class Middleware
  def initialize(app)
    @app = app
  end

  def call(env)
    @status, @headers, @body = @app.call(env)
    [@status, @headers, @body << "Middleware\n"]
  end
end

use(Middleware)

run(app)

运行rackup 并访问localhost:9292。输出是:

main
Middleware

所以很明显Middleware 包装并调用了主应用程序。因此它能够对请求进行预处理,并以任何方式对响应进行后处理。

正如http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack 所解释的,Rails 使用 Rack 中间件来实现它的很多功能,您也可以使用 config.middleware.use 系列方法添加自己的中间件。

在中间件中实现功能的优势在于,您可以在任何 Rack 框架上重用它,因此可以在所有主要的 Ruby 框架上重用它,而不仅仅是 Rails。

【讨论】:

    【解决方案6】:

    Rack 是一个 gem,它提供了一个简单的接口来抽象 HTTP 请求/响应。 Rack 位于 web 框架(Rails、Sinatra 等)和 web 服务器(unicorn、puma)之间,作为适配器。从上图中,这使 unicorn 服务器完全独立于了解 rails 和 rails 不了解 unicorn。这是loose couplingseparation of concerns 的一个很好的例子。

    上图来自于机架https://youtu.be/3PnUV9QzB0g 上的此 Rails 会议演讲,我建议您观看它以加深理解。

    【讨论】:

      【解决方案7】:

      机架中间件是一种过滤进入应用程序的请求和响应的方法。中间件组件位于客户端和服务器之间,处理入站请求和出站响应,但它不仅仅是可用于与 Web 服务器通信的接口。它用于对模块(通常是 Ruby 类)进行分组和排序,并指定它们之间的依赖关系。机架中间件模块必须: – 具有将堆栈中的下一个应用程序作为参数的构造函数 – 响应“调用”方法,该方法将环境哈希作为参数。此调用的返回值是一个数组:状态码、环境哈希和响应正文。

      【讨论】:

        【解决方案8】:

        我使用 Rack 中间件解决了几个问题:

        1. Catching JSON parse errors with custom Rack middleware 并在客户端提交破坏的 JSON 时返回格式正确的错误消息
        2. Content Compression via Rack::Deflater

        它在这两种情况下都提供了相当优雅的修复。

        【讨论】:

        • 这个答案虽然有点用,但实际上并没有解决什么是机架中间件的问题。
        • 这也是一个相当仅限链接的答案......:P
        【解决方案9】:

        Rack - 黑白 Web 和应用服务器接口

        Rack 是一个 Ruby 包,它为 Web 服务器提供与应用程序通信的接口。在 Web 服务器和应用程序之间添加中间件组件以修改请求/响应的行为方式很容易。中间件组件位于客户端和服务器之间,处理入站请求和出站响应。

        通俗地说,它基本上只是一组关于服务器和 Rails 应用程序(或任何其他 Ruby Web 应用程序)应该如何相互通信的指南

        要使用 Rack,请提供一个“app”:一个响应调用方法的对象,以环境哈希作为参数,并返回一个包含三个元素的 Array:

        • HTTP 响应代码
        • 标头哈希
        • 响应正文,必须响应每个请求

        更多解释,您可以点击以下链接。

        1. https://rack.github.io/
        2. https://redpanthers.co/rack-middleware/
        3. https://blog.engineyard.com/2015/understanding-rack-apps-and-middleware
        4. https://guides.rubyonrails.org/rails_on_rack.html#resources
        

        在 Rails 中,我们有 config.ru 作为机架文件,您可以使用 rackup 命令运行任何机架文件。默认端口是9292。要对此进行测试,您只需在 rails 目录中运行 rackup 并查看结果。您还可以分配要在其上运行它的端口。在任何特定端口上运行机架文件的命令是

        rackup -p PORT_NUMBER
        

        【讨论】:

          猜你喜欢
          • 2013-06-27
          • 1970-01-01
          • 1970-01-01
          • 2016-03-22
          • 1970-01-01
          • 1970-01-01
          • 2019-06-22
          • 2012-05-09
          • 2015-03-23
          相关资源
          最近更新 更多