Che cos'è il middleware Rack?


267

Che cos'è il middleware Rack in Ruby? Non sono riuscito a trovare una buona spiegazione del significato di "middleware".


4
C'è anche una guida su RailsGuide che ora copre in modo completo Rack, incluso il middleware : guide.rubyonrails.org/rails_on_rack.html
xji

Grazie mille al team PhusionPassenger, hanno un articolo ben spiegato sul loro blog. rubyraptor.org/…
Lamian,

Il middleware rack e rack è spiegato in QUESTO articolo. Spiega anche come creare un'applicazione basata su rack.
shashwat srivastava,

Risposte:


353

Rack come design

Il middleware Rack è più che "un modo per filtrare una richiesta e una risposta" - è un'implementazione del modello di progettazione della pipeline per i server Web che utilizzano Rack .

Separa in modo molto chiaro le diverse fasi dell'elaborazione di una richiesta: la separazione delle preoccupazioni è un obiettivo chiave di tutti i prodotti software ben progettati.

Ad esempio con Rack I posso avere fasi separate della pipeline facendo:

  • Autenticazione : quando arriva la richiesta, i dettagli di accesso degli utenti sono corretti? Come convalidare questo OAuth, autenticazione di base HTTP, nome / password?

  • Autorizzazione : "l'utente è autorizzato a svolgere questo specifico compito?", Ovvero la sicurezza basata sui ruoli.

  • Memorizzazione nella cache : ho già elaborato questa richiesta, posso restituire un risultato memorizzato nella cache?

  • Decorazione : come posso migliorare la richiesta per migliorare l'elaborazione a valle?

  • Monitoraggio delle prestazioni e dell'utilizzo : quali statistiche posso ottenere dalla richiesta e dalla risposta?

  • Esecuzione : gestisce effettivamente la richiesta e fornisce una risposta.

Essere in grado di separare le diverse fasi (e facoltativamente includerle) è di grande aiuto nello sviluppo di applicazioni ben strutturate.

Comunità

C'è anche un ottimo ecosistema che si sviluppa attorno a Rack Middleware: dovresti essere in grado di trovare componenti rack predefiniti per eseguire tutti i passaggi sopra e altro. Consulta il wiki di Rack GitHub per un elenco di middleware .

Che cos'è il middleware?

Middleware è un termine terribile che si riferisce a qualsiasi componente / libreria software che assiste ma non è direttamente coinvolto nell'esecuzione di alcune attività. Esempi molto comuni sono la registrazione, l'autenticazione e gli altri componenti comuni di elaborazione orizzontale . Queste tendono ad essere le cose di cui tutti hanno bisogno in più applicazioni ma non troppe persone sono interessate (o dovrebbero essere) a costruirsi.

Maggiori informazioni


Una cosa su cui non sono chiaro: tutti i middleware condividono gli stessi dati? È possibile separarli (ad esempio sandbox one) per motivi di sicurezza?
Brian Armstrong,

2
Rack fa parte della tua applicazione, quindi tutti i componenti del middleware hanno la stessa copia della richiesta e ognuno può modificarlo come desidera. AFAIK, non c'è modo di sandbox nello stesso modo in cui non c'è modo di sandbox un oggetto da un altro all'interno dello stesso processo (nonostante i tentativi di sandboxing di Ruby).
Chris McCauley,

1
e capire che Rack è diverso da Rake.
Manish Shrivastava,

1
Mi piace pensare al middleware come a qualsiasi cosa si trovi nel mezzo della mia app tra ciò che ho codificato e ciò che va e viene dal mio server ... che è ospitato su Rackspace. Il motivo per cui il termine "middleware rack" è confuso, come tutti sappiamo, è perché è stato Confucio a scrivere tutto il middleware rack originale, più di 2000 anni fa. In Francia.
LpLrich

74

Prima di tutto, Rack è esattamente due cose:

  • Una convenzione di interfaccia server web
  • Una gemma

Rack: l'interfaccia Webserver

Le basi del rack sono una convenzione semplice. Ogni server Web conforme al rack chiamerà sempre un metodo di chiamata su un oggetto che gli viene fornito e servirà il risultato di tale metodo. Rack specifica esattamente come deve apparire questo metodo di chiamata e cosa deve restituire. Questo è un rack.

Facciamo un semplice tentativo. Userò WEBrick come server Web compatibile con rack, ma uno qualsiasi di essi lo farà. Creiamo una semplice applicazione Web che restituisce una stringa JSON. Per questo creeremo un file chiamato config.ru. Config.ru verrà chiamato automaticamente dal comando di rackup della gemma che eseguirà semplicemente il contenuto di config.ru in un server Web compatibile con il rack. Quindi aggiungiamo quanto segue al file config.ru:

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

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

Come specificato dalla convenzione, il nostro server ha un metodo chiamato call che accetta un hash di ambiente e restituisce un array con il formato [stato, intestazioni, corpo] per il server web da servire. Proviamo semplicemente chiamando il rackup. Un server conforme a rack predefinito, forse WEBrick o Mongrel, si avvierà e attenderà immediatamente che le richieste vengano pubblicate.

$ 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

Testiamo il nostro nuovo server JSON curling o visitando l'URL http://localhost:9292/hello.jsone voilà:

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

Funziona. Grande! Questa è la base per ogni framework web, sia esso Rails o Sinatra. Ad un certo punto implementano un metodo call, lavorano attraverso tutto il codice del framework e infine restituiscono una risposta nella tipica forma [stato, intestazioni, corpo].

In Ruby on Rails, ad esempio, le richieste del rack raggiungono la ActionDispatch::Routing.Mapperclasse che assomiglia a questa:

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

Quindi, in pratica, i controlli di Rails dipendono dall'hash di env se una route corrisponde. In tal caso, passa l'hash env all'applicazione per calcolare la risposta, altrimenti risponde immediatamente con un 404. Quindi qualsiasi server Web conforme alla convenzione sull'interfaccia del rack è in grado di servire un'applicazione Rails completamente funzionante.

middleware

Rack supporta anche la creazione di livelli middleware. Fondamentalmente intercettano una richiesta, fanno qualcosa con essa e la trasmettono. Questo è molto utile per compiti versatili.

Supponiamo di voler aggiungere la registrazione al nostro server JSON che misura anche il tempo impiegato da una richiesta. Possiamo semplicemente creare un logger middleware che fa esattamente questo:

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

Quando viene creato, si salva una copia dell'applicazione rack effettiva. Nel nostro caso questa è un'istanza del nostro JSONServer. Rack chiama automaticamente il metodo di chiamata sul middleware e si aspetta un [status, headers, body]array, proprio come il nostro server JSON restituisce.

Quindi, in questo middleware, viene preso il punto iniziale, quindi viene effettuata la chiamata effettiva a JSONServer @app.call(env), quindi il logger emette la voce di registrazione e infine restituisce la risposta come [@status, @headers, @body].

Per fare in modo che il nostro piccolo rackup.ru utilizzi questo middleware, aggiungi un RackLogger ad esso in questo modo:

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   

Riavvia il server e voilà, genera un registro su ogni richiesta. Rack consente di aggiungere più middleware chiamati nell'ordine in cui sono stati aggiunti. È solo un ottimo modo per aggiungere funzionalità senza cambiare il core dell'applicazione rack.

Rack - The Gem

Sebbene il rack - prima di tutto - sia una convenzione, è anche un gioiello che offre grandi funzionalità. Uno di quelli che abbiamo già utilizzato per il nostro server JSON, il comando di rackup. Ma c'è di più! La gemma del rack offre piccole applicazioni per molti casi d'uso, come servire file statici o persino intere directory. Vediamo come serviamo un semplice file, ad esempio un file HTML di base che si trova in htmls / index.html:

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

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

Forse vogliamo servire questo file dalla radice del sito web, quindi aggiungiamo quanto segue al nostro config.ru:

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

Se visitiamo http://localhost:9292vediamo il nostro file html reso perfettamente. È stato facile, vero?

Aggiungiamo un'intera directory di file javascript creando alcuni file javascript in / javascripts e aggiungendo quanto segue a config.ru:

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

Riavvia il server e visita http://localhost:9292/javascripte vedrai un elenco di tutti i file javascript che puoi includere ora direttamente da qualsiasi luogo.


3
Ma non il middleware Rack?
Rup,

1
Se non sai cos'è un rack, saprai esattamente di cosa si tratta e come utilizzarlo dopo aver letto questo post sul blog. Molto bella. Ironia della sorte, però, il link alla documentazione ufficiale del rack alla fine del post non è più disponibile!
Colin,

Hai ragione, grazie. Ho incluso i contenuti nel post e rimosso il link non funzionante.
Thomas Fankhauser,

Direi che non è una convenzione. è un'interfaccia, un contratto ben definito per un modello di richiesta-risposta
Ron Klein,

20

Ho avuto un problema a capire Rack da solo per un bel po 'di tempo. L'ho capito appieno solo dopo aver lavorato sul creare questo web server Ruby in miniatura da solo. Ho condiviso le mie conoscenze su Rack (sotto forma di una storia) qui sul mio blog: http://gauravchande.com/what-is-rack-in-ruby-rails

Il feedback è più che benvenuto.


13
Le risposte di solo collegamento sono sconsigliate su Stack Overflow , perché se la risorsa a cui il collegamento va diventa non disponibile in futuro, la risposta diventa inutile. Per favore, riassumi almeno i punti rilevanti del tuo post sul blog e aggiungili a questa risposta.

Grazie per il tuo post. Sono un programmatore di Rails per principianti e ho capito il concetto di rack con il tuo post chiaro.
Eduardo Ramos,

Ottimo post sul blog. Le altre risposte sembrano un po 'più contorte dell'IMO.
Clam

Che spiegazione fantastica. Grazie Gaurav.
Rovitulli,

7

config.ru esempio eseguibile minimo

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)

Corri rackupe visita localhost:9292. L'output è:

main
Middleware

Quindi è chiaro che Middlewareavvolge e chiama l'app principale. Pertanto è in grado di pre-elaborare la richiesta e post-elaborazione della risposta in qualsiasi modo.

Come spiegato su: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , Rails utilizza i middleware Rack per molte delle sue funzionalità e puoi aggiungerne anche tu con config.middleware.usei metodi familiari.

Il vantaggio dell'implementazione della funzionalità in un middleware è che è possibile riutilizzarlo su qualsiasi framework Rack, quindi su tutti i principali Ruby e non solo su Rails.


6

Il middleware rack è un modo per filtrare una richiesta e una risposta che arrivano nell'applicazione. Un componente middleware si trova tra il client e il server, elaborando le richieste in entrata e le risposte in uscita, ma è più che un'interfaccia che può essere utilizzata per parlare con il web server. È usato per raggruppare e ordinare moduli, che di solito sono classi Ruby, e specificare la dipendenza tra di loro. Il modulo middleware per rack deve solo: - avere un costruttore che prende la prossima applicazione nello stack come parametro - rispondere al metodo "call", che utilizza l'hash dell'ambiente come parametro. Il valore restituito da questa chiamata è un array di: codice di stato, hash dell'ambiente e corpo della risposta.


4

Ho usato il middleware Rack per risolvere un paio di problemi:

  1. Rilevamento di errori di analisi JSON con middleware Rack personalizzato e restituzione di messaggi di errore ben formattati quando il client invia JSON eliminato
  2. Compressione del contenuto tramite rack :: Deflater

Ha offerto correzioni piuttosto eleganti in entrambi i casi.


2
Questa risposta, sebbene alquanto utile, in realtà non affronta la questione di cosa sia il Middleware Rack .

Anche questa è una risposta abbastanza solo link ...: P
Smar

4

Che cos'è Rack?

Rack fornisce un'interfaccia minima tra i server Web che supportano i framework Ruby e Ruby.

Utilizzando Rack è possibile scrivere un'applicazione Rack.

Rack passerà l'hash Environment (un hash, contenuto all'interno di una richiesta HTTP da un client, costituito da intestazioni simili a CGI) all'applicazione Rack che può usare le cose contenute in questo hash per fare quello che vuole.

Che cos'è un'applicazione rack?

Per utilizzare Rack, è necessario fornire un'app, un oggetto che risponde al #callmetodo con l'hash dell'ambiente come parametro (generalmente definito come env). #calldeve restituire una matrice con esattamente tre valori:

  • il codice di stato (ad esempio "200"),
  • un hash di intestazioni ,
  • il corpo di risposta (che deve rispondere al metodo Ruby each).

Puoi scrivere un'applicazione Rack che restituisce un tale array - questo verrà rispedito al tuo client, da Rack, all'interno di una risposta (questa sarà in realtà un'istanza della classe Rack::Response[fai clic per andare ai documenti]).

Un'applicazione rack molto semplice:

  • gem install rack
  • Crea un config.rufile: Rack sa cercarlo.

Creeremo una piccola applicazione rack che restituisce una risposta (un'istanza Rack::Response) che è corpo di risposta è un array che contiene una stringa: "Hello, World!".

Accenderemo un server locale usando il comando rackup.

Quando visiteremo la porta pertinente nel nostro browser vedremo "Ciao, mondo!" reso nel viewport.

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

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

run MessageApp.new

Avvia un server locale con rackupe visita localhost: 9292 e dovresti vedere 'Hello, World!' reso.

Questa non è una spiegazione esaustiva, ma essenzialmente ciò che accade qui è che il Client (il browser) invia una richiesta HTTP a Rack, tramite il server locale, e Rack istanzia MessageApped esegue call, passando l'Hash Ambiente come parametro nel metodo ( l' envargomento).

Il rack accetta il valore restituito (l'array) e lo utilizza per creare un'istanza di Rack::Responsee inviarlo al client. Il browser utilizza la magia per stampare "Hello, World!" allo schermo.

Per inciso, se vuoi vedere com'è l'hash dell'ambiente, mettici puts envsotto def call(env).

Per quanto minimale, quello che hai scritto qui è un'applicazione Rack!

Far interagire un'applicazione Rack con l'hash dell'ambiente Incoming

Nella nostra piccola app Rack, possiamo interagire con l' envhash (vedi qui per ulteriori informazioni sull'hash dell'ambiente).

Implementeremo la possibilità per l'utente di inserire la propria stringa di query nell'URL, pertanto tale stringa sarà presente nella richiesta HTTP, incapsulata come valore in una delle coppie chiave / valore dell'hash Environment.

La nostra app Rack accederà a quella stringa di query dall'hash Environment e la invierà al client (il nostro browser, in questo caso) tramite il Body in the Response.

Dai documenti Rack sull'hash dell'ambiente: "QUERY_STRING: la parte dell'URL della richiesta che segue il?, Se presente. Può essere vuota, ma è sempre richiesta!"

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

Ora, rackupvisita localhost:9292?hello( ?helloessendo la stringa della query) e dovresti vedere 'ciao' reso nel viewport.

Middleware Rack

Noi:

  • inserire un pezzo di cremagliera Middleware nella nostra base di codice - una classe: MessageSetter,
  • l'hash Ambiente colpirà questa classe prima e verrà passato come parametro: env,
  • MessageSetterinserirà una 'MESSAGE'chiave nell'hash di env, il cui valore è 'Hello, World!'se env['QUERY_STRING']è vuoto; env['QUERY_STRING']altrimenti,
  • infine, tornerà @app.call(env)- @appdi essere la prossima app nel 'stack': MessageApp.

Innanzitutto, la versione "long-hand":

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

Dai documenti Rack :: Builder vediamo che Rack::Builderimplementa un piccolo DSL per costruire iterativamente applicazioni Rack. Ciò significa sostanzialmente che è possibile creare uno "Stack" composto da uno o più Middleware e un'applicazione di "livello inferiore" a cui inviare. Tutte le richieste che passano all'applicazione di livello inferiore verranno prima elaborate dal tuo Middleware.

#usespecifica il middleware da utilizzare in una pila. Prende il middleware come argomento.

Il Middleware Rack deve:

  • avere un costruttore che accetta l'applicazione successiva nello stack come parametro.
  • rispondere al callmetodo che utilizza l'hash Environment come parametro.

Nel nostro caso, il "Middleware" è MessageSetter, il "costruttore" è il initializemetodo di MessageSetter , la "prossima applicazione" nello stack è MessageApp.

Così qui, a causa di ciò che Rack::Builderha sotto il cofano, l' appargomento del MessageSetter's initializemetodo è MessageApp.

(aggira la testa prima di andare avanti)

Pertanto, ogni pezzo di Middleware essenzialmente "passa" l'hash Ambiente esistente alla successiva applicazione nella catena - quindi hai l'opportunità di mutare l'hash ambiente all'interno del Middleware prima di passarlo all'applicazione successiva nello stack.

#runaccetta un argomento che è un oggetto che risponde #calle restituisce una risposta in rack (un'istanza di Rack::Response).

conclusioni

Usando Rack::Builderpuoi costruire catene di Middleware e qualsiasi richiesta alla tua applicazione verrà elaborata da ciascun Middleware a sua volta prima di essere infine elaborata dal pezzo finale nello stack (nel nostro caso MessageApp). Ciò è estremamente utile perché separa le diverse fasi di elaborazione delle richieste. In termini di "separazione delle preoccupazioni", non potrebbe essere molto più pulito!

Puoi costruire una 'pipeline di richieste' composta da diversi Middleware che trattano cose come:

  • Autenticazione
  • Autorizzazione
  • caching
  • Decorazione
  • Monitoraggio delle prestazioni e dell'utilizzo
  • Esecuzione (effettivamente gestire la richiesta e fornire una risposta)

(sopra i punti elenco di un'altra risposta su questa discussione)

Lo vedrai spesso nelle applicazioni Sinatra professionali. Sinatra usa Rack! Vedi qui per la definizione di ciò che Sinatra È !

Come nota finale, il nostro config.rupuò essere scritto in uno stile di breve durata, producendo esattamente la stessa funzionalità (e questo è ciò che vedrai in genere):

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

use MessageSetter
run MessageApp.new

E per mostrare più esplicitamente ciò che MessageAppsta facendo, ecco la sua versione "long-hand" che mostra esplicitamente che #callsta creando una nuova istanza di Rack::Response, con i tre argomenti richiesti.

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

Link utili


1

Rack: l'interfaccia b / n server Web e app

Rack è un pacchetto Ruby che fornisce un'interfaccia per un server Web per comunicare con l'applicazione. È facile aggiungere componenti middleware tra il server Web e l'app per modificare il comportamento della richiesta / risposta. Il componente middleware si trova tra il client e il server, elaborando le richieste in entrata e le risposte in uscita.

In parole povere, è fondamentalmente solo una serie di linee guida su come un server e un'app Rails (o qualsiasi altra app Web Ruby) dovrebbero dialogare .

Per utilizzare Rack, fornire una "app": un oggetto che risponde al metodo di chiamata, prendendo l'hash dell'ambiente come parametro e restituendo una matrice con tre elementi:

  • Il codice di risposta HTTP
  • Un hash di intestazioni
  • L' organismo di risposta , che deve rispondere a ciascuna richiesta .

Per ulteriori spiegazioni, è possibile seguire i collegamenti seguenti.

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

In rails, abbiamo config.ru come file rack, puoi eseguire qualsiasi file rack con rackupcomando. E la porta predefinita per questo è 9292. Per verificarlo, puoi semplicemente eseguirlo rackupnella directory delle rotaie e vedere il risultato. È inoltre possibile assegnare la porta su cui si desidera eseguirlo. Il comando per eseguire il file rack su qualsiasi porta specifica è

rackup -p PORT_NUMBER

1

immagine che mostra un rack tra unicorno e rotaie

Rack è un gioiello che fornisce una semplice interfaccia per astrarre la richiesta / risposta HTTP. Il rack si trova tra i framework Web (Rails, Sinatra ecc.) E i server Web (unicorno, puma) come un adattatore. Dall'immagine sopra ciò mantiene il server unicorno completamente indipendente dalla conoscenza delle rotaie e le rotaie non conoscono l'unicorno. Questo è un buon esempio di accoppiamento lento , separazione delle preoccupazioni .

L'immagine qui sopra proviene da questa conferenza conferenza su rotaie su rack https://youtu.be/3PnUV9QzB0g Consiglio di guardarla per una comprensione più profonda.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.