¿Qué es el middleware Rack?

267

¿Qué es el middleware Rack en Ruby? No pude encontrar ninguna buena explicación de lo que quieren decir con "middleware".

chrisgoyal
fuente
44
También hay una guía sobre RailsGuide que ahora cubre Rack de manera integral, incluyendo middleware: guides.rubyonrails.org/rails_on_rack.html
xji
Muchas gracias al equipo de PhusionPassenger, tienen un artículo bien explicado en su blog. rubyraptor.org/…
Lamian
Rack y middleware de rack se explican en ESTE artículo. También se explica sobre la creación de una aplicación basada en rack
Shashwat Srivastava

Respuestas:

353

Estante como diseño

El middleware de Rack es más que "una forma de filtrar una solicitud y respuesta": es una implementación del patrón de diseño de canalización para servidores web que usan Rack .

Separa de forma muy clara las diferentes etapas de procesamiento de una solicitud: la separación de las preocupaciones es un objetivo clave de todos los productos de software bien diseñados.

Por ejemplo, con Rack puedo tener etapas separadas de la tubería haciendo:

  • Autenticación : cuando llega la solicitud, ¿son correctos los detalles de inicio de sesión de los usuarios? ¿Cómo valido este OAuth, autenticación básica HTTP, nombre / contraseña?

  • Autorización : "¿está autorizado el usuario para realizar esta tarea en particular?", Es decir, seguridad basada en roles.

  • Almacenamiento en caché : ya he procesado esta solicitud, ¿puedo devolver un resultado en caché?

  • Decoración : ¿cómo puedo mejorar la solicitud para mejorar el procesamiento posterior?

  • Monitoreo de rendimiento y uso : ¿qué estadísticas puedo obtener de la solicitud y la respuesta?

  • Ejecución : realmente maneja la solicitud y proporciona una respuesta.

Ser capaz de separar las diferentes etapas (y opcionalmente incluirlas) es de gran ayuda para desarrollar aplicaciones bien estructuradas.

Comunidad

También se está desarrollando un gran ecosistema en torno a Rack Middleware: debería poder encontrar componentes de rack preconstruidos para realizar todos los pasos anteriores y más. Consulte el wiki de Rack GitHub para obtener una lista de middleware .

¿Qué es el middleware?

Middleware es un término terrible que se refiere a cualquier componente / biblioteca de software que ayuda pero no está directamente involucrado en la ejecución de alguna tarea. Ejemplos muy comunes son el registro, la autenticación y otros componentes comunes de procesamiento horizontal . Estos tienden a ser lo que todo el mundo necesita en múltiples aplicaciones, pero no muchas personas están interesadas (o deberían estar) en construirse.

Más información

Chris McCauley
fuente
Algo que no tengo claro: ¿todo el middleware comparte los mismos datos? ¿Es posible separarlos (es decir, sandbox one) por seguridad?
Brian Armstrong
2
Rack es parte de su aplicación, por lo que todos los middleware componen la misma copia de la solicitud y cada uno puede modificarla de la forma que desee. AFAIK, no hay forma de ponerles sandbox de la misma manera que no hay manera de poner un sandbox en un objeto de otro dentro del mismo proceso (a pesar de los intentos de hacer un sandbox en Ruby).
Chris McCauley
1
y entiendo que Rack es diferente de Rake.
Manish Shrivastava
1
Me gusta pensar que el middleware es algo que se encuentra en el medio de mi aplicación entre lo que he codificado y lo que va y viene de mi servidor ... que está alojado en el espacio en rack. La razón por la cual el término 'middleware en rack' es confuso, como todos sabemos, es porque fue Confucio quien escribió todo el middleware en rack original, hace más de 2000 años. En Francia.
LpLrich
74

En primer lugar, Rack es exactamente dos cosas:

  • Una convención de interfaz de servidor web
  • Una gema

Rack: la interfaz del servidor web

Los conceptos básicos de rack son una convención simple. Cada servidor web compatible con bastidores siempre llamará a un método de llamada en un objeto que le proporcione y servirá el resultado de ese método. Rack especifica exactamente cómo debe verse este método de llamada y qué tiene que devolver. Eso es estante.

Vamos a intentarlo de manera simple. Usaré WEBrick como servidor web compatible con rack, pero cualquiera de ellos funcionará. Creemos una aplicación web simple que devuelva una cadena JSON. Para esto crearemos un archivo llamado config.ru. Config.ru será llamado automáticamente por el comando rackup de la gema del rack, que simplemente ejecutará el contenido de config.ru en un servidor web compatible con el rack. Así que agreguemos lo siguiente al archivo config.ru:

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

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

Como la convención especifica, nuestro servidor tiene un método llamado call que acepta un hash de entorno y devuelve una matriz con la forma [estado, encabezados, cuerpo] para que el servidor web sirva. Probémoslo simplemente llamando al acumulador. Un servidor compatible con rack predeterminado, tal vez WEBrick o Mongrel se iniciarán y esperarán inmediatamente a que se envíen las solicitudes.

$ 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

Probemos nuestro nuevo servidor JSON curvando o visitando la url http://localhost:9292/hello.jsony listo:

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

Funciona. ¡Excelente! Esa es la base de cada marco web, ya sea Rails o Sinatra. En algún momento implementan un método de llamada, trabajan a través de todo el código marco y finalmente devuelven una respuesta en la forma típica [estado, encabezados, cuerpo].

En Ruby on Rails, por ejemplo, las solicitudes de rack llegan a la ActionDispatch::Routing.Mapperclase que se ve así:

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

Básicamente, las comprobaciones de Rails dependen del hash env si alguna ruta coincide. Si es así, pasa el hash env a la aplicación para calcular la respuesta, de lo contrario, responde inmediatamente con un 404. Por lo tanto, cualquier servidor web que cumpla con la convención de interfaz de rack puede servir una aplicación Rails totalmente desarrollada.

Middleware

Rack también admite la creación de capas de middleware. Básicamente interceptan una solicitud, hacen algo con ella y la transmiten. Esto es muy útil para tareas versátiles.

Supongamos que queremos agregar el registro a nuestro servidor JSON que también mide cuánto tiempo tarda una solicitud. Simplemente podemos crear un registrador de middleware que haga exactamente esto:

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

Cuando se crea, se guarda una copia de la aplicación de rack real. En nuestro caso, esa es una instancia de nuestro JSONServer. Rack llama automáticamente al método de llamada en el middleware y espera una [status, headers, body]matriz, al igual que nuestro JSONServer regresa.

Entonces, en este middleware, se toma el punto de inicio, luego se realiza la llamada real al JSONServer @app.call(env), luego el registrador genera la entrada de registro y finalmente devuelve la respuesta como [@status, @headers, @body].

Para hacer que nuestro pequeño rackup.ru use este middleware, agregue un uso RackLogger así:

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   

Reinicie el servidor y listo, genera un registro en cada solicitud. Rack le permite agregar múltiples middlewares que se llaman en el orden en que se agregan. Es simplemente una excelente manera de agregar funcionalidad sin cambiar el núcleo de la aplicación de rack.

Estante - La Gema

Aunque el rack, en primer lugar, es una convención, también es una joya que proporciona una gran funcionalidad. Uno de ellos ya lo usamos para nuestro servidor JSON, el comando rackup. ¡Pero hay más! La gema de rack proporciona pequeñas aplicaciones para muchos casos de uso, como servir archivos estáticos o incluso directorios completos. Veamos cómo servimos un archivo simple, por ejemplo, un archivo HTML muy básico ubicado en htmls / index.html:

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

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

Quizás queramos servir este archivo desde la raíz del sitio web, así que agreguemos lo siguiente a nuestro config.ru:

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

Si visitamos http://localhost:9292, vemos nuestro archivo html perfectamente renderizado. Eso fue fácil, ¿verdad?

Agreguemos un directorio completo de archivos javascript creando algunos archivos javascript en / javascripts y agregando lo siguiente a config.ru:

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

Reinicie el servidor y visite http://localhost:9292/javascripty verá una lista de todos los archivos javascript que puede incluir ahora directamente desde cualquier lugar.

Thomas Fankhauser
fuente
3
¿Pero no el middleware Rack?
Rup
1
Si no sabe qué es el rack, sabrá exactamente qué es y cómo usarlo después de leer esta publicación de blog. Muy agradable. Irónicamente, sin embargo, el enlace a la documentación oficial del rack al final de la publicación ya no está disponible.
Colin
Tienes razón, gracias. Incluí el contenido en la publicación y eliminé el enlace muerto.
Thomas Fankhauser
Yo diría que no es una convención. es una interfaz, un contrato bien definido para un modelo de solicitud-respuesta
Ron Klein
20

Tuve un problema para entender a Rack por una buena cantidad de tiempo. Solo lo entendí completamente después de trabajar en hacer este servidor web Ruby en miniatura . He compartido mis conocimientos sobre Rack (en forma de historia) aquí en mi blog: http://gauravchande.com/what-is-rack-in-ruby-rails

La retroalimentación es más que bienvenida.

Gaurav Chande
fuente
13
Las respuestas de solo enlace se desaconsejan en Stack Overflow , porque si el recurso al que va el enlace deja de estar disponible en el futuro, la respuesta se vuelve inútil. Al menos resuma los puntos relevantes de su publicación de blog y agréguelos a esta respuesta.
Gracias por tu publicación. Soy un programador de Rails muy principiante y entendí el concepto de rack con tu publicación clara.
Eduardo Ramos
Gran publicación de blog. Las otras respuestas parecen un poco más complicadas de la OMI.
Almeja
Qué explicación tan asombrosa. Gracias Gaurav.
rovitulli
7

config.ru ejemplo ejecutable mínimo

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)

Corre rackupy visita localhost:9292. El resultado es:

main
Middleware

Por lo tanto, está claro que se Middlewareenvuelve y llama a la aplicación principal. Por lo tanto, puede preprocesar la solicitud y procesar la respuesta de cualquier manera.

Como se explica en: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , Rails utiliza los middlewares de Rack para muchas de sus funciones, y también puede agregar el suyo propio con config.middleware.usemétodos familiares.

La ventaja de implementar la funcionalidad en un middleware es que puede reutilizarlo en cualquier marco de Rack, por lo tanto, en todos los principales Ruby, y no solo en Rails.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
6

El middleware en rack es una forma de filtrar una solicitud y una respuesta que ingresan a su aplicación. Un componente de middleware se encuentra entre el cliente y el servidor, procesando solicitudes entrantes y respuestas salientes, pero es más que una interfaz que se puede usar para hablar con el servidor web. Se utiliza para agrupar y ordenar módulos, que generalmente son clases de Ruby, y especificar la dependencia entre ellos. El módulo de middleware de bastidor solo debe: - tener un constructor que tome la siguiente aplicación en la pila como parámetro - responder al método de "llamada", que tome el hash del entorno como parámetro. El valor de retorno de esta llamada es una matriz de: código de estado, hash de entorno y cuerpo de respuesta.

L.Cole
fuente
4

He usado el middleware Rack para resolver un par de problemas:

  1. Detectando errores de análisis JSON con middleware Rack personalizado y devolviendo mensajes de error bien formateados cuando el cliente envía JSON reventado
  2. Compresión de contenido a través de Rack :: Deflater

Proporcionó soluciones bastante elegantes en ambos casos.

djcp
fuente
2
Esta respuesta, aunque algo útil, en realidad no aborda la pregunta de qué es Rack Middleware .
También esta es una respuesta bastante solo de enlace ...: P
Smar
4

¿Qué es el estante?

Rack proporciona una interfaz mínima entre servidores web que admiten los marcos Ruby y Ruby.

Usando Rack puedes escribir una Aplicación Rack.

Rack pasará el hash de entorno (un hash, contenido dentro de una solicitud HTTP de un cliente, que consiste en encabezados similares a CGI) a su aplicación Rack, que puede usar las cosas contenidas en este hash para hacer lo que quiera.

¿Qué es una aplicación de bastidor?

Para usar Rack, debe proporcionar una 'aplicación', un objeto que responde al #callmétodo con el Hash de entorno como parámetro (normalmente definido como env). #calldebe devolver una matriz de exactamente tres valores:

  • el código de estado (por ejemplo, '200'),
  • un hash de encabezados ,
  • el cuerpo de respuesta (que debe responder al método Ruby each).

Puede escribir una aplicación Rack que devuelva dicha matriz; Rack la enviará de vuelta a su cliente dentro de un Respuesta (en realidad, será una instancia de la Clase Rack::Response[haga clic para ir a los documentos]).

Una aplicación de rack muy simple:

  • gem install rack
  • Cree un config.ruarchivo: Rack sabe buscar esto.

Vamos a crear una pequeña aplicación Rack que devuelve una respuesta (una instancia de Rack::Response) que es la respuesta del cuerpo es una matriz que contiene una cadena de caracteres: "Hello, World!".

Lanzaremos un servidor local usando el comando rackup.

Al visitar el puerto correspondiente en nuestro navegador, veremos "¡Hola, mundo!" prestados en la ventana gráfica.

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

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

run MessageApp.new

Inicie un servidor local con rackupy visite localhost: 9292 y debería ver '¡Hola, mundo!' prestados.

Esta no es una explicación completa, pero esencialmente lo que sucede aquí es que el Cliente (el navegador) envía una Solicitud HTTP a Rack, a través de su servidor local, y Rack crea instancias MessageAppy ejecutacall , pasando el Hash del entorno como un parámetro en el método ( el envargumento)

Rack toma el valor de retorno (la matriz) y lo usa para crear una instancia Rack::Responsey lo envía de vuelta al Cliente. El navegador usa magia para imprimir '¡Hola, mundo!' a la pantalla

Por cierto, si quieres ver cómo se ve el hash del entorno, simplemente pon puts env debajodef call(env) .

Minimal como es, ¡lo que has escrito aquí es una aplicación Rack!

Hacer que una aplicación de bastidor interactúe con el hash Entorno entrante

En nuestra pequeña aplicación Rack, podemos interactuar con el envhash (ver aquí para obtener más información sobre el hash del entorno).

Implementaremos la capacidad para que el usuario ingrese su propia cadena de consulta en la URL, por lo tanto, esa cadena estará presente en la solicitud HTTP, encapsulada como un valor en uno de los pares clave / valor del hash del Entorno.

Nuestra aplicación Rack accederá a esa cadena de consulta desde el hash Environment y la enviará de vuelta al cliente (nuestro navegador, en este caso) a través del Cuerpo en la Respuesta.

De los documentos de Rack en Environment Hash: "QUERY_STRING: la parte de la URL de solicitud que sigue a?, Si la hay. Puede estar vacía, ¡pero siempre es necesaria!"

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

Ahora, rackupvisite localhost:9292?hello( ?hellosiendo la cadena de consulta) y debería ver 'hola' en la ventana gráfica.

Middleware en rack

Lo haremos:

  • insertar una pieza de cremallera Middleware en nuestra base de código - una clase: MessageSetter,
  • el hash Medio Ambiente llegará a esta primera clase y se pasa como un parámetro: env,
  • MessageSetterinsertará una 'MESSAGE'clave en el hash env, siendo su valor 'Hello, World!'si env['QUERY_STRING']está vacío; env['QUERY_STRING']si no,
  • Por último, se volverá @app.call(env)- @appsiendo la próxima aplicación de la 'pila': MessageApp.

Primero, la versión de 'mano larga':

#./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

Desde los documentos de Rack :: Builder vemos queRack::Builder implementa un pequeño DSL para construir iterativamente aplicaciones de Rack. Básicamente, esto significa que puede construir una 'Pila' que consta de uno o más Middlewares y una aplicación de 'nivel inferior' para enviar. Todas las solicitudes que pasen a su aplicación de nivel inferior serán procesadas primero por su (s) Middleware (s).

#useespecifica middleware para usar en una pila. Toma el middleware como argumento.

El Middleware de Rack debe:

  • tener un constructor que tome la siguiente aplicación en la pila como parámetro.
  • responder al callmétodo que toma el hash de entorno como parámetro.

En nuestro caso, el 'Middleware' es MessageSetter, el 'constructor' es el initializemétodo de MessageSetter , la 'próxima aplicación' en la pila esMessageApp .

Así que aquí, debido a lo que Rack::Builderlo hace bajo el capó, el appargumento de MessageSetter's initializemétodo es MessageApp.

(pon tu cabeza alrededor de lo anterior antes de continuar)

Por lo tanto, cada pieza de Middleware esencialmente 'pasa' el hash del entorno existente a la siguiente aplicación en la cadena, por lo que tiene la oportunidad de mutar ese hash del entorno dentro del Middleware antes de pasarlo a la siguiente aplicación en la pila.

#runtoma un argumento que es un objeto que responde #cally devuelve una Respuesta de rack (una instancia de Rack::Response).

Conclusiones

Utilizando Rack::Builderpuede construir cadenas de Middlewares y cualquier solicitud a su aplicación será procesada por cada Middleware a su vez antes de que finalmente sea procesada por la pieza final en la pila (en nuestro caso,MessageApp ). Esto es extremadamente útil porque separa las diferentes etapas de procesamiento de solicitudes. En términos de 'separación de preocupaciones', ¡no podría ser mucho más limpio!

Puede construir una 'tubería de solicitud' que consta de varios Middlewares que se ocupan de cosas como:

  • Autenticación
  • Autorización
  • Almacenamiento en caché
  • Decoración
  • Monitoreo de rendimiento y uso
  • Ejecución (realmente maneja la solicitud y proporciona una respuesta)

(puntos anteriores de otra respuesta en este hilo)

A menudo verá esto en aplicaciones profesionales de Sinatra. ¡Sinatra usa Rack! Ver aquí para la definición de lo que Sinatra ES !

Como nota final, nuestro config.ruse puede escribir en un estilo de mano corta, produciendo exactamente la misma funcionalidad (y esto es lo que normalmente verá):

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

use MessageSetter
run MessageApp.new

Y para mostrar más explícitamente lo que MessageAppestá haciendo, aquí está su versión de 'mano larga' que muestra explícitamente que #callestá creando una nueva instancia de Rack::Response, con los tres argumentos requeridos.

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

Enlaces útiles

Yorkshireman
fuente
1

Rack: la interfaz en blanco y negro del servidor web y de aplicaciones

Rack es un paquete de Ruby que proporciona una interfaz para que un servidor web se comunique con la aplicación. Es fácil agregar componentes de middleware entre el servidor web y la aplicación para modificar el comportamiento de su solicitud / respuesta. El componente de middleware se encuentra entre el cliente y el servidor, procesando solicitudes entrantes y respuestas salientes.

En palabras simples, es básicamente un conjunto de pautas sobre cómo un servidor y una aplicación Rails (o cualquier otra aplicación web Ruby) deben comunicarse entre sí. .

Para usar Rack, proporcione una "aplicación": un objeto que responda al método de llamada, tomando el hash del entorno como parámetro y devolviendo una matriz con tres elementos:

  • El código de respuesta HTTP
  • Un hash de encabezados
  • El cuerpo de respuesta , que debe responder a cada solicitud .

Para más explicaciones, puede seguir los siguientes enlaces.

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

En rails, tenemos config.ru como un archivo de rack, puede ejecutar cualquier archivo de rack con el rackupcomando. Y el puerto predeterminado para esto es 9292. Para probar esto, simplemente puede ejecutar rackupen su directorio de rieles y ver el resultado. También puede asignar el puerto en el que desea ejecutarlo. El comando para ejecutar el archivo en rack en cualquier puerto específico es

rackup -p PORT_NUMBER
VK Singh
fuente
1

imagen que muestra el estante entre unicornio y rieles

Rack es una gema que proporciona una interfaz simple para abstraer la solicitud / respuesta HTTP. Rack se encuentra entre los frameworks web (Rails, Sinatra, etc.) y los servidores web (unicornio, puma) como un adaptador. De la imagen de arriba, esto mantiene al servidor de unicornio completamente independiente de saber sobre rieles y rieles no sabe sobre unicornio. Este es un buen ejemplo de acoplamiento flojo , separación de preocupaciones .

La imagen es de esta charla conferencia de carriles sobre una rejilla https://youtu.be/3PnUV9QzB0g Recomiendo ver que para una comprensión más profunda.

Vbp
fuente