Quel est le middleware Rack?

Quel est le middleware Rack dans Ruby? Je n’ai pas trouvé de bonne explication à ce qu’ils entendent par “middleware”.

Rack comme Design

Le middleware Rack est plus qu’un moyen de filtrer une requête et une réponse. Il s’agit d’une implémentation du modèle de conception de pipeline pour les serveurs Web utilisant Rack .

Il sépare très clairement les différentes étapes du traitement d’une demande – la séparation des préoccupations étant un objective clé de tous les produits logiciels bien conçus.

Par exemple, avec Rack, je peux avoir différentes étapes du pipeline:

  • Authentification : lorsque la demande arrive, les informations de connexion des utilisateurs sont-elles correctes? Comment puis-je valider cette authentification OAuth, HTTP Basic, nom / mot de passe?

  • Autorisation : “l’utilisateur est-il autorisé à exécuter cette tâche particulière?”, C.-à-d. La sécurité basée sur les rôles.

  • Mise en cache : ai-je déjà traité cette demande, puis-je retourner un résultat en cache?

  • Décoration : comment améliorer la demande pour améliorer le traitement en aval?

  • Surveillance des performances et de l’utilisation : quelles statistiques puis-je obtenir de la demande et de la réponse?

  • Execution : gère réellement la demande et fournit une réponse.

Pouvoir séparer les différentes étapes (et éventuellement les inclure) est une aide précieuse pour développer des applications bien structurées.

Communauté

Il existe également un excellent éco-système qui se développe autour de Rack Middleware – vous devriez être en mesure de trouver des composants de rack pré-construits pour effectuer toutes les étapes ci-dessus et plus encore. Voir le wiki Rack GitHub pour une liste de middleware .

Quel est le middleware?

Le middleware est un terme terrible qui fait référence à tout composant / bibliothèque de logiciel qui aide mais n’est pas directement impliqué dans l’exécution d’une tâche. Les exemples les plus courants sont la journalisation, l’authentification et les autres composants de traitement horizontaux courants . Celles-ci ont tendance à être les choses dont tout le monde a besoin dans plusieurs applications, mais pas trop de personnes sont intéressées (ou devraient l’être) à se construire.

Plus d’information

  • Le commentaire à ce sujet étant un moyen de filtrer les requêtes provient probablement de l’ épisode 151 de RailsCast: la projection d’écran de Rack Middleware .

  • Le middleware Rack a été développé à partir de Rack et il y a une excellente introduction au middleware Introduction to Rack .

  • Il y a une introduction au middleware sur Wikipedia ici .

Tout d’abord, Rack est exactement deux choses:

  • Une convention d’interface de serveur web
  • Une gemme

Rack – L’interface du serveur Web

Les bases du rack sont une convention simple. Chaque serveur Web compatible avec un rack appelle toujours une méthode d’appel sur un object que vous lui donnez et sert le résultat de cette méthode. Rack spécifie exactement comment cette méthode d’appel doit ressembler et ce qu’elle doit renvoyer. C’est rackable.

Essayons simplement. Je vais utiliser WEBrick comme serveur Web compatible avec les racks, mais chacun d’entre eux le fera. Créons une application web simple qui retourne une chaîne JSON. Pour cela, nous allons créer un fichier appelé config.ru. Le fichier config.ru sera automatiquement appelé par le rack de commandes du rack gem, qui exécutera simplement le contenu du fichier config.ru sur un serveur Web compatible avec le rack. Ajoutons donc ceci au fichier config.ru:

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

Comme le stipule la convention, notre serveur a une méthode appelée call qui accepte un hash d’environnement et renvoie un tableau avec la forme [status, headers, body] pour le serveur Web. Essayons cela en appelant simplement le rackup. Un serveur par défaut compatible avec le rack, peut-être WEBrick ou Mongrel démarrera et attendra immédiatement les demandes de service.

 $ 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 

http://localhost:9292/hello.json notre nouveau serveur JSON en curling ou en visitant l’url http://localhost:9292/hello.json et voila:

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

Ça marche. Génial! C’est la base de chaque framework Web, que ce soit Rails ou Sinatra. À un moment donné, ils implémentent une méthode d’appel, parcourent tout le code de la structure et renvoient finalement une réponse dans la forme [état, en-têtes, corps] typique.

Dans Ruby on Rails, par exemple, les demandes de rack ActionDispatch::Routing.Mapper classe ActionDispatch::Routing.Mapper qui ressemble à ceci:

 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 

Donc, fondamentalement, Rails vérifie, dépendant du hachage env si n’importe quel itinéraire correspond. Si tel est le cas, il transmet le hachage env à l’application pour calculer la réponse, sinon il répond immédiatement par un 404. Ainsi, tout serveur Web compatible avec la convention d’interface de rack peut servir une application Rails totalement saturée.

Middleware

Rack prend également en charge la création de couches middleware. Ils interceptent essentiellement une demande, en font quelque chose et la transmettent. Ceci est très utile pour les tâches polyvalentes.

Disons que nous voulons append la journalisation à notre serveur JSON qui mesure également la durée d’une requête. Nous pouvons simplement créer un logger de logiciel intermédiaire qui fait exactement ceci:

 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 

Une fois créé, il enregistre une copie de l’application de rack réelle. Dans notre cas, c’est une instance de notre JSONServer. Rack appelle automatiquement la méthode d’appel sur le middleware et attend un tableau [status, headers, body] , tout comme nos retours JSONServer.

Donc, dans ce middleware, le sharepoint départ est pris, puis l’appel réel à JSONServer est effectué avec @app.call(env) , puis le @app.call(env) l’entrée de journalisation et retourne la réponse sous la forme [@status, @headers, @body] .

Pour que notre petit rackup.ru utilise ce middleware, ajoutez-y une utilisation de RackLogger comme ceci:

 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 

Redémarrez le serveur et le tour est joué, il affiche un journal à chaque demande. Rack vous permet d’append plusieurs middlewares appelés dans l’ordre dans lequel ils ont été ajoutés. C’est juste un excellent moyen d’append des fonctionnalités sans changer le cœur de l’application rack.

Rack – Le joyau

Bien que le rack – avant tout – soit une convention, il s’agit également d’un joyau qui offre de grandes fonctionnalités. L’un d’entre eux est déjà utilisé pour notre serveur JSON, la commande rackup. Mais il y a plus! Le rack fournit peu d’applications pour de nombreux cas d’utilisation, tels que le service de fichiers statiques ou même de répertoires entiers. Voyons comment nous servons un fichier simple, par exemple un fichier HTML très basique situé à l’adresse htmls / index.html:

    The Index   

Index Page

Nous voulons peut-être servir ce fichier à partir de la racine du site Web, ajoutons donc ce qui suit à notre config.ru:

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

Si nous visitons http://localhost:9292 nous voyons notre fichier HTML parfaitement rendu. C’était facile, non?

Ajoutons tout un répertoire de fichiers javascript en créant des fichiers javascript sous / javascripts et en ajoutant ce qui suit à config.ru:

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

Redémarrez le serveur et visitez http://localhost:9292/javascript et vous verrez une liste de tous les fichiers javascript que vous pouvez inclure maintenant de n’importe où.

J’ai eu du mal à comprendre Rack moi-même pendant un bon moment. Je ne l’ai pleinement compris qu’après avoir travaillé moi-même sur ce serveur web miniature Ruby . J’ai partagé mes connaissances sur Rack (sous la forme d’une histoire) sur mon blog: http://gauravchande.com/what-is-rack-in-ruby-rails

Les commentaires sont les bienvenus.

Le middleware en rack est un moyen de filtrer une requête et une réponse entrant dans votre application. Un composant middleware se situe entre le client et le serveur, traitant les demandes entrantes et les réponses sortantes, mais il ne se limite pas à l’interface permettant de communiquer avec le serveur Web. Il est utilisé pour regrouper et ordonner des modules, qui sont généralement des classes Ruby, et pour spécifier leur dépendance. Le module middleware en rack doit uniquement: – avoir un constructeur qui prend la prochaine application en stack en tant que paramètre – répondre à la méthode «call», qui prend comme paramètre le hachage de l’environnement. La valeur renvoyée par cet appel est un tableau de: code d’état, environnement hash et corps de réponse.

config.ru exemple config.ru minimal

 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) 

Exécutez le rackup et visitez localhost:9292 . La sortie est la suivante:

 main Middleware 

Il est donc clair que le Middleware enveloppe et appelle l'application principale. Par conséquent, il est possible de pré-traiter la demande et de post-traiter la réponse de quelque manière que ce soit.

Comme expliqué à: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , Rails utilise des middlewares Rack pour une grande partie de ses fonctionnalités, et vous pouvez en append vous-même avec config.middleware.use méthodes familiales.

L'avantage de l'implémentation de fonctionnalités dans un middleware est que vous pouvez le réutiliser sur n'importe quelle structure Rack, donc toutes les principales versions de Ruby, et pas seulement les Rails.

J’ai utilisé le middleware Rack pour résoudre quelques problèmes:

  1. Récupération des erreurs d’parsing JSON avec un middleware Rack personnalisé et retour de messages d’erreur bien formatés lorsque le client soumet un JSON bloqué
  2. Compression de contenu via Rack :: Deflater

Il offrait des corrections assez élégantes dans les deux cas.

Qu’est-ce que le rack?

Rack fournit une interface minimale entre les serveurs Web prenant en charge les frameworks Ruby et Ruby.

En utilisant Rack, vous pouvez écrire une application Rack.

Rack transmettra le hash Environment (un Hash, contenu dans une requête HTTP provenant d’un client, composé d’en-têtes de type CGI) à votre application Rack, qui peut utiliser les éléments contenus dans ce hachage pour faire ce qu’il veut.

Qu’est-ce qu’une application rack?

Pour utiliser Rack, vous devez fournir une application – un object qui répond à la méthode #call avec le hachage d’environnement en tant que paramètre (généralement défini comme env ). #call doit retourner un tableau de trois valeurs exactement:

  • le code de statut (par exemple «200»),
  • un Hash of Headers ,
  • le corps de réponse (qui doit répondre à la méthode Ruby, each ).

Vous pouvez écrire une application Rack qui renvoie un tel tableau – celle-ci sera renvoyée à votre client par Rack, à l’intérieur d’une réponse (il s’agira en fait d’une instance de Class Rack::Response [cliquez pour accéder à docs]).

Une application de rack très simple:

  • gem install rack
  • Créez un fichier config.ru – Rack sait le rechercher.

Nous allons créer une application Rack minuscule qui renvoie une réponse (une instance de Rack::Response ) dont le corps de réponse est un tableau contenant une chaîne: "Hello, World!" .

Nous allons lancer un serveur local en utilisant la commande rackup .

Lors de la visite du port correspondant dans notre navigateur, nous verrons “Hello, World!” rendu dans la fenêtre d’affichage.

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

rackup un serveur local avec rackup et visitez localhost: 9292 et vous devriez voir ‘Hello, World!’ rendu.

Il ne s’agit pas là d’une explication complète, mais essentiellement, le client (le navigateur) envoie un message HTTP Request to Rack via votre serveur local et Rack instancie MessageApp et lance un call en transmettant le hachage de l’environnement comme paramètre dans le méthode (l’argument env ).

Rack prend la valeur de retour (le tableau) et l’utilise pour créer une instance de Rack::Response et l’envoie au client. Le navigateur utilise la magie pour imprimer “Hello, World!” à l’écran.

Incidemment, si vous voulez voir à quoi ressemble le hash de l’environnement, il suffit de mettre put en dessous de def call(env) .

Aussi minime soit-il, ce que vous avez écrit ici est une application Rack!

Faire une application en rack interagit avec le hachage d’environnement entrant

Dans notre petite application Rack, nous pouvons interagir avec le hash env (voir ici pour en savoir plus sur le hash Environment).

Nous mettrons en œuvre la possibilité pour l’utilisateur d’entrer sa propre chaîne de requête dans l’URL. Par conséquent, cette chaîne sera présente dans la requête HTTP, encapsulée comme une valeur dans l’une des paires clé / valeur du hachage Environment.

Notre application Rack accédera à cette chaîne de requête à partir du hachage Environment et l’enverra au client (notre navigateur, dans ce cas) via le corps dans la réponse.

À partir des documents Rack sur l’environnement Hash: “QUERY_STRING: la partie de l’URL de la requête qui suit le cas échéant. Peut être vide, mais est toujours obligatoire!”

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

Maintenant, rackup et visitez localhost:9292?hello ( ?hello étant la chaîne de requête) et vous devriez voir “bonjour” rendu dans la fenêtre d’affichage.

Rack Middleware

Nous allons:

  • insérer un morceau de Rack Middleware dans notre base de code – une classe: MessageSetter ,
  • le hachage de l’environnement va d’abord bash cette classe et sera passé en paramètre: env ,
  • MessageSetter va insérer une clé 'MESSAGE' dans le hash env, sa valeur étant 'Hello, World!' si env['QUERY_STRING'] est vide; env['QUERY_STRING'] sinon,
  • Enfin, il retournera @app.call(env)@app étant la prochaine application de la stack: MessageApp .

Tout d’abord, la version «à long terme»:

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

À partir des documents Rack :: Builder, nous voyons que Rack::Builder implémente un petit DSL pour construire de manière itérative des applications Rack. Cela signifie essentiellement que vous pouvez créer une «stack» comprenant un ou plusieurs middlewares et une application de «niveau inférieur» à envoyer. Toutes les demandes envoyées à votre application de niveau inférieur seront d’abord traitées par votre ou vos middleware (s).

#use spécifie le middleware à utiliser dans une stack. Il prend le middleware comme argument.

Rack Middleware doit:

  • avoir un constructeur qui prend l’application suivante dans la stack en tant que paramètre.
  • répondre à la méthode d’ call qui prend le hachage Environment comme paramètre.

Dans notre cas, le ‘Middleware’ est MessageSetter , le ‘constructeur’ est la méthode d’ initialize de MessageSetter, la prochaine application de la stack est MessageApp .

Donc, à cause de ce que fait Rack::Builder , l’argument de la méthode d’ initialize de MessageSetter est MessageApp .

(faites le tour de la tête avant de continuer)

Par conséquent, chaque élément de middleware transmet essentiellement le hachage d’environnement existant à l’application suivante de la chaîne. Vous avez donc la possibilité de modifier cet environnement dans le middleware avant de le transmettre à l’application suivante de la stack.

#run prend un argument qui est un object qui répond à #call et retourne une réponse de Rack (une instance de Rack::Response ).

Conclusions

En utilisant Rack::Builder vous pouvez construire des chaînes de Middlewares et toute requête à votre application sera traitée par chaque Middleware à son tour avant d’être finalement traitée par la pièce finale de la stack (dans notre cas, MessageApp ). Ceci est extrêmement utile car il sépare les différentes étapes du traitement des requêtes. En termes de «séparation des préoccupations», cela ne pouvait pas être beaucoup plus propre!

Vous pouvez construire un «pipeline de demandes» composé de plusieurs Middlewares traitant de sujets tels que:

  • Authentification
  • Autorisation
  • Mise en cache
  • Décoration
  • Surveillance des performances et de l’utilisation
  • Execution (gère la demande et fournit une réponse)

(ci-dessus les puces d’une autre réponse sur ce sujet)

Vous verrez souvent cela dans les applications professionnelles de Sinatra. Sinatra utilise Rack! Voir ici pour la définition de ce qu’est Sinatra IS !

Pour config.ru , notre config.ru peut être écrit dans un style abrégé, produisant exactement les mêmes fonctionnalités (et c’est ce que vous verrez généralement):

 require_relative './message_app' require_relative './middleware/message_setter' use MessageSetter run MessageApp.new 

Et pour montrer plus explicitement ce que fait MessageApp , voici sa version «à main levée» qui montre explicitement que #call crée une nouvelle instance de Rack::Response , avec les trois arguments requirejs.

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

Liens utiles

  • Code complet pour cet article (Github repo commit)
  • Bon article de blog, “Introduction à Rack Middleware”
  • De la bonne documentation Rack