¿Qué es el middleware en rack?

Resuelto chrisgoyal asked hace 14 años • 9 respuestas

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

chrisgoyal avatar Feb 13 '10 12:02 chrisgoyal
Aceptado

Estante como diseño

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

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

Por ejemplo, con Rack puedo tener etapas separadas del proceso haciendo:

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

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

  • Almacenamiento en caché : ¿ya procesé esta solicitud? ¿Puedo devolver un resultado almacenado en caché?

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

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

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

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

Comunidad

También se está desarrollando un excelente ecosistema en torno a Rack Middleware: debería poder encontrar componentes de rack prediseñados para realizar todos los pasos anteriores y más. Consulte la 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 con la ejecución de alguna tarea, pero que no participa directamente en ella. Ejemplos muy comunes son el registro, la autenticación y otros componentes comunes de procesamiento horizontal . Estas tienden a ser las cosas que todos necesitan en múltiples aplicaciones, pero no mucha gente está interesada (o debería estar) en construirlas ellos mismos.

Más información

  • El comentario acerca de que es una forma de filtrar solicitudes probablemente proviene del episodio 151 de RailsCast: transmisión de pantalla de Rack Middleware.

  • El middleware de Rack evolucionó a partir de Rack y hay una excelente introducción en Introducción al middleware de Rack .

  • Hay una introducción al middleware en Wikipedia aquí .

Chris McCauley avatar Feb 13 '2010 09:02 Chris McCauley

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 del rack son una convención simple. Todo servidor web compatible con rack siempre llamará a un método de llamada en un objeto que usted le proporcione y entregará el resultado de ese método. Rack especifica exactamente cómo debe verse este método de llamada y qué debe devolver. Eso es un estante.

Intentémoslo de forma sencilla. Usaré WEBrick como servidor web compatible con rack, pero cualquiera de ellos servirá. Creemos una aplicación web simple que devuelva una cadena JSON. Para ello crearemos un archivo llamado config.ru. El archivo config.ru será llamado automáticamente por el comando rackup de rack gem, que simplemente ejecutará el contenido de config.ru en un servidor web compatible con rack. Entonces 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 lo especifica la convención, nuestro servidor tiene un método llamado llamada que acepta un hash de entorno y devuelve una matriz con el formato [estado, encabezados, cuerpo] para que lo sirva el servidor web. Probémoslo simplemente llamando a rackup. Un servidor predeterminado compatible con rack, tal vez WEBrick o Mongrel, se iniciará y esperará inmediatamente a que se atiendan 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 haciendo curl 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 todo framework web, ya sea Rails o Sinatra. En algún momento, implementan un método de llamada, analizan todo el código del marco y finalmente devuelven una respuesta en el formato típico [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, Rails verifica, dependiendo 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 completa.

software intermedio

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.

Digamos que queremos agregar un registro a nuestro servidor JSON que también mida 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, guarda una copia de la aplicación del rack real. En nuestro caso, 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, tal como lo hace nuestro JSONServer.

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

Para que nuestro pequeño rackup.ru use este middleware, agréguele un uso RackLogger como este:

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 del rack.

Estante - La joya

Aunque 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 rack proporciona pequeñas aplicaciones para muchos casos de uso, como servir archivos estáticos o incluso directorios completos. Veamos cómo entregamos 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 lo visitamos http://localhost:9292vemos 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 avatar Feb 21 '2012 12:02 Thomas Fankhauser

Tuve problemas para entender a Rack durante un buen tiempo. Sólo lo entendí completamente después de trabajar yo mismo en la creación de este servidor web Ruby en miniatura . He compartido mis aprendizajes sobre Rack (en forma de historia) aquí en mi blog: http://blog.gauravchande.com/what-is-rack-in-ruby-rails

Los comentarios son más que bienvenidos.

Gaurav Chande avatar Oct 16 '2013 23:10 Gaurav Chande

¿Qué es el estante?

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

Usando Rack puedes escribir una aplicación Rack.

Rack pasará el hash del entorno (un hash, contenido dentro de una solicitud HTTP de un cliente, que consta de encabezados tipo CGI) a su aplicación Rack, que puede usar los elementos contenidos en este hash para hacer lo que quiera.

¿Qué es una aplicación en rack?

Para usar Rack, debe proporcionar una 'aplicación', un objeto que responde al #callmétodo con el Hash de entorno como parámetro (generalmente 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 regreso a su cliente, dentro de una Respuesta (en realidad será una instancia de la Clase Rack::Response[haga clic para ir a los documentos]).

Una aplicación de bastidor muy sencilla:

  • gem install rack
  • Cree un config.ruarchivo: Rack sabe que debe buscarlo.

Crearemos una pequeña aplicación en rack que devuelve una respuesta (una instancia de Rack::Response) cuyo cuerpo de respuesta es una matriz que contiene una cadena "Hello, World!":.

Activaremos un servidor local usando el comando rackup.

Al visitar el puerto correspondiente en nuestro navegador veremos "¡Hola, mundo!" renderizado 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 rackupy visite localhost:9292 y debería ver '¡Hola, mundo!' prestado.

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 una instancia MessageAppy ejecuta call, pasando el Hash de entorno como parámetro al método ( el envargumento).

Rack toma el valor de retorno (la matriz) y lo usa para crear una instancia Rack::Responsey la envía de regreso 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, ponlo puts envdebajo def call(env).

Por mínimo que sea, ¡lo que has escrito aquí es una aplicación Rack!

Hacer que una aplicación en rack interactúe con el hash del entorno entrante

En nuestra pequeña aplicación Rack, podemos interactuar con el envhash (consulte 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 del entorno y la enviará de vuelta al cliente (nuestro navegador, en este caso) a través del cuerpo de la respuesta.

De los documentos de Rack en el Hash de entorno: "QUERY_STRING: la parte de la URL de solicitud que sigue a ?, si corresponde. Puede estar vacía, ¡pero siempre es obligatoria!"

#./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" representado en la ventana gráfica.

Middleware de bastidor

Lo haremos:

  • inserte una pieza de Rack Middleware en nuestro código base: una clase MessageSetter:,
  • El hash del entorno llegará primero a esta clase y se pasará como parámetro env:
  • MessageSetterinsertará una 'MESSAGE'clave en el hash env, siendo su valor 'Hello, World!'if env['QUERY_STRING']está vacío; env['QUERY_STRING']si no,
  • finalmente, regresará @app.call(env), @appsiendo la siguiente aplicación en la 'Pila': MessageApp.

Primero, la versión '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

En los documentos de Rack::Builder vemos que Rack::Builderimplementa un pequeño DSL para construir aplicaciones Rack de forma iterativa. Básicamente, esto significa que puede crear una 'pila' que consta de uno o más middlewares y una aplicación de 'nivel inferior' para enviar. Todas las solicitudes que lleguen a su aplicación de nivel inferior serán procesadas primero por su(s) Middleware(s).

#useespecifica el middleware que se utilizará 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 del entorno como parámetro.

En nuestro caso, el 'Middleware' es MessageSetter, el 'constructor' es el método de MessageSetter initializey la 'siguiente aplicación' en la pila es MessageApp.

Entonces aquí, debido a lo que Rack::Builderhace bajo el capó, el appargumento del MessageSettermétodo initializees MessageApp.

(Entienda lo anterior antes de continuar)

Por lo tanto, cada pieza de Middleware esencialmente "transmite" el hash del entorno existente a la siguiente aplicación de 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 de la pila.

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

Conclusiones

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

Puede construir una 'canalización de solicitudes' 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 manejar la solicitud y proporcionar una respuesta)

(arriba de viñetas de otra respuesta en este hilo)

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

Como nota final, nuestro config.rupuede escribirse en un estilo abreviado, 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 '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

  • Código completo para esta publicación (compromiso del repositorio de Github)
  • Buena publicación de blog, "Introducción al middleware en rack"
  • Alguna buena documentación sobre Rack
Yorkshireman avatar Feb 12 '2017 01:02 Yorkshireman