¿Qué es el middleware en rack?
¿Qué es el middleware Rack en Ruby? No pude encontrar ninguna buena explicación de lo que quieren decir con "middleware".
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í .
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.json
y 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.Mapper
clase 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: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 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.
¿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 #call
método con el Hash de entorno como parámetro (generalmente 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 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.ru
archivo: 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 rackup
y 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 MessageApp
y ejecuta call
, pasando el Hash de entorno como parámetro al método ( el env
argumento).
Rack toma el valor de retorno (la matriz) y lo usa para crear una instancia Rack::Response
y 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 env
debajo 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 env
hash (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, rackup
visite localhost:9292?hello
( ?hello
siendo 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
: MessageSetter
insertará una'MESSAGE'
clave en el hash env, siendo su valor'Hello, World!'
ifenv['QUERY_STRING']
está vacío;env['QUERY_STRING']
si no,- finalmente, regresará
@app.call(env)
,@app
siendo 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::Builder
implementa 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).
#use
especifica 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
call
mé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 initialize
y la 'siguiente aplicación' en la pila es MessageApp
.
Entonces aquí, debido a lo que Rack::Builder
hace bajo el capó, el app
argumento del MessageSetter
método initialize
es 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.
#run
toma un argumento que es un objeto que responde #call
y 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.ru
puede 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 MessageApp
está haciendo, aquí está su versión 'larga' que muestra explícitamente 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
- 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