什么是机架中间件?

Ruby中的机架中间件是什么?对于他们所说的“中间件”,我找不到任何好的解释。

78448 次浏览

机架作为设计

机架中间件不仅仅是“一种过滤请求和响应的方式”;-它是使用的web服务器的管道设计模式的实现。

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

例如,使用Rack,我可以有管道的单独阶段:

  • 身份验证:当请求到达时,用户登录详细信息正确吗?我如何验证这个OAuth, HTTP基本身份验证,名称/密码?

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

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

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

  • 性能,使用监控:我可以从请求和响应中得到什么统计信息?

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

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

社区

还有一个围绕机架中间件开发的很好的生态系统——你应该能够找到预先构建的机架组件来完成上面所有的步骤。看到机架GitHub维基的中间件列表

中间件是什么?

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

更多的信息

首先,Rack就是两件事:

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

机架- web服务器接口

机架的基本原理是一个简单的约定。每一个机架兼容的web服务器将总是调用一个调用方法在你给他的对象上,并提供该方法的结果。Rack精确地指定了这个调用方法必须是什么样子,以及它必须返回什么。这是架。

让我们简单地试一试。我将使用WEBrick作为机架兼容的web服务器,但它们中的任何一个都可以。让我们创建一个返回JSON字符串的简单web应用程序。为此,我们将创建一个名为config.ru的文件。config.ru将自动被机架gem的命令rackup调用,该命令将简单地在机架兼容的web服务器中运行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]的数组供web服务器服务。让我们通过简单地调用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

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

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

它的工作原理。太棒了!这是每个web框架的基础,无论是Rails还是Sinatra。在某种程度上,它们实现了一个调用方法,遍历所有框架代码,最后以典型的[状态,标题,正文]形式返回响应。

例如,在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自动调用中间件上的调用方法,并期望返回[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允许您添加多个按照添加顺序调用的中间件。这是一种在不改变机架应用程序核心的情况下添加功能的好方法。

机架-宝石

虽然机架-首先-是一个惯例,它也是一个宝石,提供了伟大的功能。其中一个我们已经在JSON服务器上使用过,即rackup命令。但还有更多!rack gem为很多用例提供了小的应用程序,比如服务静态文件甚至整个目录。让我们看看我们是如何提供一个简单的文件的,例如一个非常基本的HTML文件,位于htmlls /index.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文件来添加一个完整的javascript文件目录,并将以下内容添加到config.ru:

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

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

在很长一段时间里,我自己都无法理解Rack。我只有在自己制作这个小型Ruby web服务器后才完全理解它。我在我的博客http://blog.gauravchande.com/what-is-rack-in-ruby-rails上分享了我对Rack的了解(以故事的形式)

非常欢迎反馈。

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

  1. 使用自定义Rack中间件捕获JSON解析错误和返回格式化良好的错误消息时,客户端提交的JSON
  2. 内容压缩通过机架::Deflater

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

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家族方法添加你自己的中间件。

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

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

机架是什么?

Rack提供了支持Ruby和Ruby框架的web服务器之间的最小接口。

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

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

什么是机架应用程序?

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

  • 状态码(例如'200'),
  • 标头哈希,
  • 身体的反应(必须响应Ruby方法each)。

你可以写一个Rack应用程序来返回这样一个数组——这将被Rack在响应中发送回你的客户端(这实际上是Rack::Response类的实例[点击进入文档])。

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

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

我们将创建一个小型机架应用程序,它返回一个响应(Rack::Response的一个实例),其响应体是一个包含String: "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!的呈现。

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

Rack获取返回值(数组)并使用它创建Rack::Response的实例并将其发送回客户端。浏览器使用魔法打印'Hello, World!对着屏幕说。

顺便说一句,如果你想看看环境哈希是什么样的,只要把puts env放在def call(env)下面。

尽管非常简单,但您在这里编写的是一个机架应用程序!

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

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

我们将实现用户向URL中输入自己的查询字符串的能力,因此,该字符串将出现在HTTP请求中,封装为Environment散列的一个键/值对中的值。

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

从机架文档的环境哈希: 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'呈现在视口中。

架的中间件

我们将:

  • 插入一个机架中间件到我们的代码库-类:MessageSetter
  • Environment哈希将首先碰到这个类,并将作为参数传入:
  • MessageSetter将插入一个'MESSAGE'键到env散列中,如果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

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

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

机架中间件必须:

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

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

因此,在这里,由于Rack::Builder在底层所做的工作,MessageSetterinitialize方法的app参数是MessageApp

(在继续前进之前先想想上面的内容)

因此,中间件的每个部分本质上都是将现有的环境哈希传递给链中的下一个应用程序——因此,在将其传递给堆栈中的下一个应用程序之前,您有机会在中间件中改变环境哈希。

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

结论

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

你可以构建一个“请求管道”,由几个中间件组成,处理如下事情:

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

(以上是本帖另一个答案的要点)

您将经常在专业的Sinatra应用程序中看到这一点。Sinatra使用Rack!Sinatra < em > < / em >!

最后要注意的是,我们的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

有用的链接

机架-接口b/w Web &应用服务器

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

通俗地说,它基本上只是一组指导方针,说明服务器和Rails应用程序(或任何其他Ruby web应用程序)应该如何相互通信

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

  • 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

图像显示机架之间的独角兽和轨道

Rack提供了一个简单的接口来抽象HTTP请求/响应。Rack位于web框架(Rails, Sinatra等)和web服务器(unicorn, puma)之间,作为适配器。从上图来看,这使得unicorn服务器完全独立于了解rails的服务器,而rails不知道unicorn的服务器。这是松散耦合分离关注点的一个很好的例子。

上图来自机架https://youtu.be/3PnUV9QzB0g上的rails会议讲话,我建议观看它以深入理解。