¿Qué es el middleware Rack?

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

Rack como diseño

El middleware 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 utilizan Rack .

Limpia muy bien las diferentes etapas de procesamiento de una solicitud, la separación de las preocupaciones es un objective clave de todos los productos de software bien diseñados.

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

  • Autenticación : cuando llega la solicitud, ¿los datos de inicio de sesión de los usuarios son correctos? ¿Cómo valido este OAuth, Autenticación básica de 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 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 manejar la solicitud y proporcionar una respuesta.

Ser capaz de separar las diferentes etapas (y opcionalmente incluirlas) es una gran ayuda en el desarrollo de aplicaciones bien estructuradas.

Comunidad

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

¿Qué es Middleware?

Middleware es un término espantoso que se refiere a cualquier componente / biblioteca de software que colabora 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 . Estas tienden a ser las cosas que todo el mundo necesita a través de múltiples aplicaciones, pero no demasiadas personas están interesadas (o deberían) en construir ellos mismos.

Más información

  • El comentario acerca de que es una forma de filtrar las solicitudes probablemente provenga del episodio 151 de RailsCast: Repartición de la pantalla Rack Middleware .

  • El middleware Rack evolucionó a partir de Rack y hay una gran introducción en el middleware de Introduction to Rack .

  • Aquí hay una introducción al middleware en Wikipedia.

En primer lugar, Rack es exactamente dos cosas:

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

Rack: la interfaz del servidor web

Lo básico del rack es una convención simple. Cada servidor web compatible con bastidor siempre llamará a un método de llamada en un objeto que le proporcione y le mostrará 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 rack.

Démosle una simple oportunidad. Utilizaré WEBrick como servidor web compatible con el bastidor, pero cualquiera de ellos servirá. Vamos a crear una aplicación web simple que devuelva una cadena JSON. Para esto, crearemos un archivo llamado config.ru. El config.ru será llamado automáticamente por el rackup de comandos de rack gem que simplemente ejecutará los contenidos de config.ru en un servidor web compatible con 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 especifica la convención, nuestro servidor tiene un método llamado llamada que acepta un hash de entorno y devuelve una matriz con la forma [estado, encabezados, cuerpo] para que se 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á de inmediato 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 ya sea curling o visitando la url http://localhost:9292/hello.json y listo:

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

Funciona. ¡Estupendo! 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 de la estructura y finalmente devuelven una respuesta en la forma típica [estado, encabezados, cuerpo].

En Ruby on Rails, por ejemplo, las solicitudes de rack ActionDispatch::Routing.Mapper clase ActionDispatch::Routing.Mapper , 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 

Así que, básicamente, Rails comprueba, dependiendo del hash de env si hay alguna ruta coincidente. Si es así pasa el hash de env a la aplicación para calcular la respuesta, de lo contrario responde inmediatamente con un 404. Por lo tanto, cualquier servidor web que sea compatible con la convención de interfaz de rack, puede servir una aplicación de Rails completamente volada.

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.

Digamos que queremos agregar el registro a nuestro servidor JSON que también mide cuánto tarda una solicitud. Simplemente podemos crear un registrador de middleware que hace 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 matriz de [status, headers, body] , al igual que nuestro JSONServer.

Entonces, en este middleware, se toma el punto de partida, luego la llamada real al JSONServer se realiza con @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 RackLogger de uso de la siguiente manera:

 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 varios middlewares que se invocan en el orden en que se agregan. Es una excelente forma de agregar funcionalidad sin cambiar el núcleo de la aplicación de rack.

Rack – La joya

Aunque rack – ante todo – 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 gem de rack proporciona pocas 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:

    The Index   

Index Page

Es posible que deseemos servir este archivo desde la raíz del sitio web, así que agreguemos lo siguiente a nuestra 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/javascript y verá una lista de todos los archivos javascript que puede incluir ahora directamente desde cualquier lugar.

Tuve un problema para entender Rack por un buen tiempo. Lo entendí completamente después de trabajar en la creación de este servidor web miniatura de Ruby . 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.

El middleware de Rack es una forma de filtrar una solicitud y respuesta que llega a su aplicación. Un componente de middleware se ubica entre el cliente y el servidor, procesa las solicitudes de entrada y las respuestas de salida, pero es más que una interfaz que puede usarse para comunicarse con el servidor web. Se usa para agrupar y ordenar módulos, que generalmente son clases de Ruby, y especificar la dependencia entre ellos. El módulo de middleware de rack solo debe: – tener un constructor que tome la próxima aplicación en la stack como parámetro – responder al método de “llamada”, que toma el hash del entorno como parámetro. La devolución del valor de esta llamada es una matriz de: código de estado, hash de entorno y cuerpo de respuesta.

ejemplo ejecutable minimal de 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) 

Ejecute rackup y visite localhost:9292 . El resultado es:

 main Middleware 

Por lo tanto, está claro que Middleware envuelve y llama a la aplicación principal. Por lo tanto, es capaz de preprocesar la solicitud y procesarla posteriormente de cualquier forma.

Como se explica en: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , Rails usa Rack middlewares para muchas de sus funciones, y usted puede agregarle también con config.middleware.use métodos familiares

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

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

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

Permitió correcciones muy elegantes en ambos casos.

¿Qué es Rack?

Rack proporciona una interfaz mínima entre los servidores web compatibles con los marcos Ruby y Ruby.

Usando Rack puede escribir una Aplicación de 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 de rack, que puede usar las cosas contenidas en este hash para hacer lo que quiera.

¿Qué es una aplicación de rack?

Para usar Rack, debe proporcionar una ‘aplicación’, un objeto que responda al método #call con la Hash de Entorno como un parámetro (típicamente definido como env ). #call debe 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 de Ruby, each ).

Puede escribir una Aplicación de Rack que devuelva dicha matriz; esto se enviará a su cliente, por Rack, dentro de una Respuesta (esto en realidad será una instancia de Class Rack::Response [haga clic para ir a los documentos]).

Una aplicación de rack muy simple:

  • gem install rack
  • Cree un archivo config.ru – Rack sabe que debe buscar esto.

Crearemos una pequeña Aplicación de Rack que devuelva una Respuesta (una instancia de Rack::Response ) cuyo Cuerpo de Respuesta es una matriz que contiene una Cadena: "Hello, World!" .

rackup un servidor local usando el comando rackup .

Al visitar el puerto correspondiente en nuestro navegador veremos “Hello, World!” representado 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 

rackup un servidor local con rackup y visita localhost: 9292 y deberías ver ‘Hello, World!’ prestado

Esta no es una explicación exhaustiva, pero básicamente 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 MessageApp instancia de MessageApp y ejecuta una call , pasando el Hash de Entorno como parámetro en el método (el argumento env ).

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

Por cierto, si quieres ver qué aspecto tiene el entorno hash, simplemente pon puts env debajo de def call(env) .

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

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

En nuestra pequeña aplicación Rack, podemos interactuar con el hash de env (consulte aquí para obtener más información sobre el hash de Environment).

Implementaremos la capacidad del usuario para ingresar 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 de entorno.

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

Desde los documentos de Rack en el Hash de entorno: “QUERY_STRING: la parte de la URL de solicitud que sigue a?, Si hay alguna, 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, rackup y visite localhost:9292?hello ( ?hello es la cadena de consulta) y debería ver ‘hello’ renderizado en la ventana gráfica.

Middleware de Rack

Lo haremos:

  • inserte una pieza de Rack Middleware en nuestra base de código – una clase: MessageSetter ,
  • el hash de entorno golpeará esta clase primero y se pasará como un parámetro: env ,
  • MessageSetter insertará una clave 'MESSAGE' en el hash env, su valor es 'Hello, World!' si env['QUERY_STRING'] está vacío; env['QUERY_STRING'] si no es así,
  • finalmente, devolverá @app.call(env)@app será la siguiente aplicación en ‘Stack’: MessageApp .

Primero, la versión ‘larga mano’:

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

De los documentos de Rack :: Builder vemos que Rack::Builder implementa una pequeña DSL para construir iterativamente aplicaciones Rack. Esto básicamente significa que puedes construir una ‘Pila’ que consiste en una o más aplicaciones de Middlewares y una de ‘nivel inferior’ para enviar a. Todas las solicitudes que pasen a su aplicación de nivel inferior serán procesadas primero por su Middleware (s).

#use especifica middleware para usar en una stack. Toma el middleware como argumento.

Rack Middleware debe:

  • tener un constructor que toma la siguiente aplicación en la stack como un parámetro.
  • responda al método de call que toma el hash de entorno como parámetro.

En nuestro caso, el ‘Middleware’ es MessageSetter , el ‘constructor’ es el método de initialize de MessageSetter, la ‘próxima aplicación’ en la stack es MessageApp .

Entonces, aquí, debido a lo que Rack::Builder hace bajo el capó, el argumento de la app del método de initialize de MessageSetter es MessageApp .

(Entérate de lo anterior antes de continuar)

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

#run toma un argumento que es un objeto que responde a #call y devuelve una Respuesta de Rack (una instancia de Rack::Response ).

Conclusiones

Usando Rack::Builder puede construir cadenas de Middlewares y cada petición de Middleware será procesada por cada middleware antes de ser finalmente procesada por la pieza final en la stack (en nuestro caso, MessageApp ). Esto es extremadamente útil porque separa las diferentes etapas de solicitudes de procesamiento. En términos de “separación de preocupaciones”, ¡no podría ser mucho más limpio!

Puede construir un ‘pipeline de solicitud’ que consiste en varios Middlewares que se ocupan de cosas tales como:

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

(puntos de viñeta anteriores de otra respuesta en este hilo)

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

Como nota final, nuestra config.ru se puede escribir 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 está haciendo MessageApp , aquí está su versión ‘long-hand’ que explícitamente muestra que #call está 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

  • Completa el código para esta publicación (Github repo commit)
  • Good Blog Post, “Introducción al Middleware de Rack”
  • Una buena documentación de Rack