O que é o middleware Rack?

267

O que é o middleware Rack no Ruby? Não consegui encontrar nenhuma boa explicação para o que eles querem dizer com "middleware".

chrisgoyal
fonte
4
Há também um guia sobre RailsGuide agora cobrindo rack de forma abrangente, incluindo middleware: guides.rubyonrails.org/rails_on_rack.html
XJI
Muito obrigado à equipe do PhusionPassenger, eles têm um artigo bem explicado em seu blog. Rubyraptor.org/…
Lamian
Rack e rack de middleware são explicados em ESTE artigo. Também explicado sobre a criação de um aplicativo baseado em rack.
shashwat srivastava

Respostas:

353

Rack como Design

O middleware do rack é mais do que "uma maneira de filtrar uma solicitação e resposta" - é uma implementação do padrão de design de pipeline para servidores da Web que usam o Rack .

Ele separa de maneira muito clara as diferentes etapas do processamento de uma solicitação - a separação de preocupações é um objetivo principal de todos os produtos de software bem projetados.

Por exemplo, com o Rack, posso ter estágios separados do pipeline executando:

  • Autenticação : quando a solicitação chega, os detalhes de logon do usuário estão corretos? Como validar esse OAuth, autenticação básica HTTP, nome / senha?

  • Autorização : "o usuário está autorizado a executar esta tarefa específica?", Ou seja, segurança baseada em função.

  • Armazenamento em cache : já processei essa solicitação, posso retornar um resultado em cache?

  • Decoração : como posso melhorar a solicitação para melhorar o processamento a jusante?

  • Monitoramento de desempenho e uso : quais estatísticas posso obter da solicitação e resposta?

  • Execução : efetue o tratamento da solicitação e forneça uma resposta.

Ser capaz de separar os diferentes estágios (e opcionalmente incluí-los) é uma grande ajuda no desenvolvimento de aplicativos bem estruturados.

Comunidade

Há também um ótimo ecossistema em desenvolvimento no Rack Middleware - você deve encontrar componentes de rack pré-criados para executar todas as etapas acima e muito mais. Consulte o wiki do Rack GitHub para obter uma lista do middleware .

O que é Middleware?

Middleware é um termo terrível que se refere a qualquer componente / biblioteca de software que ajude, mas não esteja diretamente envolvido na execução de alguma tarefa. Exemplos muito comuns são log, autenticação e outros componentes de processamento horizontal comuns . Essas tendem a ser as coisas que todos precisam em vários aplicativos, mas muitas pessoas não estão interessadas (ou deveriam estar) em se desenvolver.

Mais Informações

Chris McCauley
fonte
Uma coisa sobre a qual não estou claro: todo o middleware compartilha os mesmos dados? É possível separá-los (por exemplo, sandbox one) por segurança?
Brian Armstrong
2
O rack faz parte do seu aplicativo, para que todo o middleware componha a mesma cópia da solicitação e cada um possa modificá-la da maneira que desejar. AFAIK, não há como sandboxá-los da mesma maneira que não há como sandbox um objeto de outro dentro do mesmo processo (apesar das tentativas de sandbox Ruby).
precisa
1
e Entenda que Rack é diferente de Rake.
Manish Shrivastava
1
Eu gosto de pensar no middleware como algo que fica no meio do meu aplicativo entre o que eu codifiquei e o que vai e volta do meu servidor ... que está hospedado no rackspace. A razão pela qual o termo 'rack middleware' é confuso, como todos sabemos, é porque Confúcio escreveu todo o middleware original do rack, há mais de 2000 anos. Na França.
LpLrich
74

Primeiro de tudo, o Rack é exatamente duas coisas:

  • Uma convenção de interface de servidor da web
  • Uma jóia

Rack - a interface do servidor da Web

O básico do rack é uma convenção simples. Todo servidor da web compatível com rack sempre chama um método de chamada em um objeto que você fornece a ele e serve o resultado desse método. O rack especifica exatamente como esse método de chamada deve ser e o que ele deve retornar. Isso é tortura.

Vamos fazer uma tentativa simples. Usarei o WEBrick como servidor da web compatível com rack, mas qualquer um deles o fará. Vamos criar um aplicativo Web simples que retorne uma sequência JSON. Para isso, criaremos um arquivo chamado config.ru. O config.ru será chamado automaticamente pelo conjunto de comandos da gem do rack, que simplesmente executará o conteúdo do config.ru em um servidor da web compatível com rack. Então, vamos adicionar o seguinte ao arquivo 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 a convenção especifica, nosso servidor possui um método chamado call que aceita um hash do ambiente e retorna uma matriz com o formato [status, headers, body] para o servidor da web servir. Vamos testá-lo simplesmente chamando de rackup. Um servidor compatível com rack padrão, talvez o WEBrick ou Mongrel seja iniciado e aguarde imediatamente a entrega das solicitações.

$ 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

Vamos testar nosso novo servidor JSON, curvando ou visitando o URL http://localhost:9292/hello.jsone voila:

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

Funciona. Ótimo! Essa é a base para todo framework web, seja Rails ou Sinatra. Em algum momento, eles implementam um método de chamada, trabalham com todo o código da estrutura e, finalmente, retornam uma resposta na forma típica [status, headers, body].

No Ruby on Rails, por exemplo, as solicitações de rack atingem a ActionDispatch::Routing.Mapperclasse que se parece com isso:

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

Então, basicamente, o Rails verifica, dependendo do hash env, se alguma rota corresponder. Nesse caso, ele passa o hash env para o aplicativo para calcular a resposta, caso contrário, ele responde imediatamente com um 404. Portanto, qualquer servidor da Web que seja compatível com a convenção de interface do rack poderá atender a um aplicativo Rails totalmente desenvolvido.

Middleware

O rack também suporta a criação de camadas de middleware. Eles basicamente interceptam uma solicitação, fazem algo com ela e a transmitem. Isso é muito útil para tarefas versáteis.

Digamos que queremos adicionar log ao nosso servidor JSON que também mede quanto tempo uma solicitação leva. Podemos simplesmente criar um registrador de middleware que faça exatamente isso:

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 é criado, ele salva uma cópia do aplicativo real do rack. No nosso caso, é uma instância do nosso JSONServer. O rack chama automaticamente o método de chamada no middleware e espera retornar uma [status, headers, body]matriz, assim como nosso JSONServer retorna.

Portanto, nesse middleware, o ponto de partida é adotado, a chamada real para o JSONServer é feita @app.call(env)e o logger gera a entrada de log e, finalmente, retorna a resposta como [@status, @headers, @body].

Para fazer nosso pequeno rackup.ru usar esse middleware, adicione um RackLogger de uso 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 o servidor e pronto, ele gera um log em cada solicitação. O rack permite adicionar vários middlewares chamados na ordem em que são adicionados. É apenas uma ótima maneira de adicionar funcionalidade sem alterar o núcleo do aplicativo em rack.

Rack - A jóia

Embora o rack - antes de tudo - seja uma convenção, ele também é uma jóia que fornece ótima funcionalidade. Um deles já usamos para o nosso servidor JSON, o comando rackup. Mas tem mais! A jóia do rack fornece pequenos aplicativos para muitos casos de uso, como servir arquivos estáticos ou mesmo diretórios inteiros. Vamos ver como servimos um arquivo simples, por exemplo, um arquivo HTML muito básico localizado em htmls / index.html:

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

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

Talvez desejemos servir esse arquivo a partir da raiz do site, então vamos adicionar o seguinte ao nosso config.ru:

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

Se o visitarmos http://localhost:9292, veremos nosso arquivo html perfeitamente renderizado. Isso foi fácil, certo?

Vamos adicionar um diretório inteiro de arquivos javascript, criando alguns arquivos javascript em / javascripts e adicionando o seguinte ao config.ru:

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

Reinicie o servidor e visite http://localhost:9292/javascripte você verá uma lista de todos os arquivos javascript que você pode incluir agora diretamente de qualquer lugar.

Thomas Fankhauser
fonte
3
Mas não o middleware de rack?
Rup
1
Se você não souber o que é rack, saberá exatamente o que é e como usá-lo depois de ler esta postagem do blog. Muito agradável. Ironicamente, porém, o link para a documentação oficial do rack no final da postagem não está mais disponível!
Colin
Você está certo, obrigado. Incluí o conteúdo na postagem e removi o link morto.
Thomas Fankhauser
Eu diria que não é uma convenção. é uma interface, um contrato bem definido para um modelo de solicitação-resposta
Ron Klein
20

Eu tive um problema para entender o Rack por um bom tempo. Eu só o entendi completamente depois de trabalhar na criação desse servidor Web em miniatura Ruby . Compartilhei meus aprendizados sobre o Rack (na forma de uma história) aqui no meu blog: http://gauravchande.com/what-is-rack-in-ruby-rails

O feedback é mais que bem-vindo.

Gaurav Chande
fonte
13
As respostas somente de link são desencorajadas no Stack Overflow , porque se o recurso para o qual o link se tornar indisponível no futuro, a resposta se tornará inútil. Resuma pelo menos os pontos relevantes da sua postagem no blog e adicione-os a esta resposta.
Obrigado por você postar. Sou um programador iniciante no Rails e entendi o conceito de rack com sua postagem clara.
Eduardo Ramos
Ótima postagem no blog. As outras respostas parecem IMO um pouco mais complicadas.
Moluscos
Que explicação incrível. Obrigado, Gaurav.
Rovitulli
7

config.ru exemplo executável mínimo

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)

Corra rackupe visite localhost:9292. A saída é:

main
Middleware

Portanto, é claro que o Middlewareembrulho e chama o aplicativo principal. Portanto, ele pode pré-processar a solicitação e pós-processar a resposta de qualquer maneira.

Conforme explicado em: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , o Rails usa middlewares de rack para muitas funcionalidades, e você também pode adicionar config.middleware.useos métodos da família.

A vantagem de implementar a funcionalidade em um middleware é que você pode reutilizá-la em qualquer estrutura Rack, portanto, todas as principais Ruby, e não apenas Rails.

Ciro Santilli adicionou uma nova foto
fonte
6

O middleware em rack é uma maneira de filtrar uma solicitação e resposta que entra no seu aplicativo. Um componente de middleware fica entre o cliente e o servidor, processando solicitações de entrada e respostas de saída, mas é mais do que uma interface que pode ser usada para conversar com o servidor da web. É usado para agrupar e solicitar módulos, que geralmente são classes Ruby, e especificar a dependência entre elas. O módulo de middleware do rack deve apenas: - ter o construtor que leva o próximo aplicativo na pilha como parâmetro - responder ao método "call", que usa o hash do ambiente como parâmetro. O retorno do valor desta chamada é uma matriz de: código de status, hash do ambiente e corpo da resposta.

L.Cole
fonte
4

Eu usei o middleware Rack para resolver alguns problemas:

  1. Captura de erros de análise JSON com o middleware Rack personalizado e retorno de mensagens de erro bem formatadas quando o cliente envia JSON bloqueado
  2. Compactação de conteúdo via Rack :: Deflater

Forneceu correções bastante elegantes em ambos os casos.

djcp
fonte
2
Essa resposta, embora um pouco útil, na verdade não aborda a questão do que é o Rack Middleware .
Também esta é uma resposta bastante apenas para link ...: P
Smar
4

O que é Rack?

O rack fornece uma interface mínima entre os servidores da web que suportam as estruturas Ruby e Ruby.

Usando o Rack, você pode escrever um Aplicativo de Rack.

O rack transmitirá o hash do ambiente (um Hash, contido em uma solicitação HTTP de um cliente, que consiste em cabeçalhos semelhantes a CGI) para o aplicativo de rack, que pode usar as coisas contidas nesse hash para fazer o que quiser.

O que é um aplicativo de rack?

Para usar o Rack, você deve fornecer um 'aplicativo' - um objeto que responda ao #callmétodo com o Hash do ambiente como parâmetro (normalmente definido como env). #calldeve retornar uma matriz de exatamente três valores:

  • o código de status (por exemplo, '200'),
  • uma mistura de cabeçalhos ,
  • o corpo da resposta (que deve responder ao método Ruby each).

Você pode escrever um aplicativo de rack que retorne essa matriz - isso será enviado de volta ao seu cliente, pelo rack, dentro de uma resposta (na verdade, essa é uma instância da classe Rack::Response[clique para acessar a documentação]).

Uma aplicação de rack muito simples:

  • gem install rack
  • Crie um config.ruarquivo - o Rack sabe procurar por isso.

Criaremos um pequeno aplicativo de rack que retornará uma resposta (uma instância de Rack::Response) cujo corpo de resposta seja uma matriz que contenha uma String:"Hello, World!" .

Iniciaremos um servidor local usando o comando rackup .

Ao visitar a porta relevante em nosso navegador, veremos "Olá, mundo!" renderizado na janela de exibição.

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

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

run MessageApp.new

Inicie um servidor local rackupe visite localhost: 9292 e você verá 'Olá, mundo!' prestados.

Essa não é uma explicação abrangente, mas essencialmente o que acontece aqui é que o Cliente (o navegador) envia uma Solicitação HTTP ao Rack, através do servidor local, e o Rack instancia MessageAppe executa call, passando o Environment Hash como parâmetro para o método ( o envargumento).

O rack pega o valor de retorno (a matriz) e o usa para criar uma instância Rack::Responsee envia de volta ao cliente. O navegador usa magia para imprimir 'Hello, World!' para a tela.

Aliás, se você quiser ver como é o hash do ambiente, basta colocar puts envembaixodef call(env) .

Por menor que seja, o que você escreveu aqui é um aplicativo Rack!

Fazendo um aplicativo de rack interagir com o hash do ambiente de entrada

Em nosso pequeno aplicativo Rack, podemos interagir com o envhash (veja aqui mais sobre o hash do ambiente).

Implementaremos a capacidade do usuário inserir sua própria string de consulta na URL; portanto, essa string estará presente na solicitação HTTP, encapsulada como um valor em um dos pares de chave / valor do hash do ambiente.

Nosso aplicativo Rack acessará essa sequência de consultas no hash Environment e a enviará de volta ao cliente (nosso navegador, neste caso) por meio do Corpo na resposta.

Dos documentos do rack sobre a Hash do ambiente: "QUERY_STRING: a parte do URL da solicitação que segue o?, Se houver. Pode estar vazia, mas é sempre necessária!"

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

Agora, rackupvisite localhost:9292?hello( ?hellosendo a string de consulta) e você verá 'olá' processado na janela de exibição.

Rack Middleware

Nós vamos:

  • insira um pedaço de Rack Middleware em nossa base de código - uma classe: MessageSetter ,
  • o hash do ambiente atingirá essa classe primeiro e será passado como um parâmetro: env ,
  • MessageSetterirá inserir uma 'MESSAGE'chave no hash env, seu valor sendo 'Hello, World!'se env['QUERY_STRING']estiver vazio;env['QUERY_STRING']se não,
  • finalmente, ele irá retornar @app.call(env)- @appser o próximo aplicativo na 'pilha': MessageApp.

Primeiro, a versão 'mão longa':

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

Nos documentos do Rack :: Builder , vemos queRack::Builder implementa um pequeno DSL para construir iterativamente aplicativos de rack. Isso basicamente significa que você pode criar uma 'Pilha' consistindo em um ou mais Middlewares e um aplicativo 'nível inferior' para o qual enviar. Todas as solicitações enviadas ao seu aplicativo de nível inferior serão processadas primeiro pelo (s) seu (s) Middleware (s).

#useespecifica o middleware a ser usado em uma pilha. Leva o middleware como argumento.

O Middleware do rack deve:

  • tem um construtor que usa o próximo aplicativo na pilha como parâmetro.
  • responda ao callmétodo que usa o hash do ambiente como parâmetro.

No nosso caso, o 'Middleware' é MessageSetter, o 'construtor' é o initializemétodo do MessageSetter , o 'próximo aplicativo' na pilha éMessageApp .

Então, aqui, por causa do que Rack::Builderfaz sob o capô, o appargumento de MessageSetter's initializemétodo é MessageApp.

(contorne o item acima antes de prosseguir)

Portanto, cada parte do Middleware basicamente 'transmite' o hash de ambiente existente para o próximo aplicativo na cadeia - para que você tenha a oportunidade de alterar esse hash de ambiente no Middleware antes de passá-lo para o próximo aplicativo na pilha.

#runaceita um argumento que é um objeto que responde #calle retorna uma resposta em rack (uma instância de Rack::Response).

Conclusões

Usando Rack::Buildervocê pode construir cadeias de Middlewares e qualquer solicitação para seu aplicativo será processada por cada Middleware, por sua vez, antes de finalmente ser processada pela parte final da pilha (no nosso caso MessageApp). Isso é extremamente útil porque separa diferentes estágios dos pedidos de processamento. Em termos de 'separação de preocupações', não poderia ser muito mais limpo!

Você pode construir um 'pipeline de solicitação' que consiste em vários Middlewares que lidam com coisas como:

  • Autenticação
  • Autorização
  • Armazenamento em cache
  • Decoração
  • Monitoramento de desempenho e uso
  • Execução (realmente lide com a solicitação e forneça uma resposta)

(pontos de marcador acima de outra resposta neste tópico)

Você verá isso frequentemente em aplicativos profissionais do Sinatra. Sinatra usa Rack! Veja aqui a definição do que Sinatra é!

Como observação final, config.rupodemos escrever em estilo abreviado, produzindo exatamente a mesma funcionalidade (e é isso que você normalmente verá):

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

use MessageSetter
run MessageApp.new

E para mostrar mais explicitamente o que MessageAppestá fazendo, aqui está sua versão de 'mão longa' que mostra explicitamente que #callestá criando uma nova instância de Rack::Response, com os três argumentos necessários.

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

Links Úteis

Yorkshireman
fonte
1

Rack - a interface entre o servidor de aplicativos e a Web

Rack é um pacote Ruby que fornece uma interface para um servidor web se comunicar com o aplicativo. É fácil adicionar componentes de middleware entre o servidor da Web e o aplicativo para modificar a maneira como sua solicitação / resposta se comporta. O componente de middleware fica entre o cliente e o servidor, processando solicitações de entrada e respostas de saída.

Em palavras leigas, é basicamente apenas um conjunto de diretrizes sobre como um servidor e um aplicativo Rails (ou qualquer outro aplicativo da web Ruby) devem conversar entre si .

Para usar o Rack, forneça um "aplicativo": um objeto que responda ao método de chamada, usando o hash do ambiente como parâmetro e retornando uma matriz com três elementos:

  • O código de resposta HTTP
  • Uma mistura de cabeçalhos
  • O corpo da resposta , que deve responder a cada solicitação .

Para mais explicações, você pode seguir os links abaixo.

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

No rails, temos o config.ru como um arquivo em rack, você pode executar qualquer arquivo em rack com o rackupcomando E a porta padrão para isso é 9292. Para testar isso, você pode simplesmente rodar rackupno diretório rails e ver o resultado. Você também pode atribuir a porta na qual deseja executá-la. O comando para executar o arquivo em rack em qualquer porta específica é

rackup -p PORT_NUMBER
VK Singh
fonte
1

imagem mostrando rack entre unicórnio e trilhos

Rack é uma jóia que fornece uma interface simples para abstrair a solicitação / resposta HTTP. O rack fica entre estruturas da web (Rails, Sinatra etc.) e servidores da web (unicórnio, puma) como um adaptador. Da imagem acima, isso mantém o servidor unicórnio completamente independente de saber sobre trilhos e os trilhos não sabem sobre unicórnio. Este é um bom exemplo de acoplamento solto , separação de preocupações .

A imagem acima é desta palestra da conferência sobre trilhos no rack https://youtu.be/3PnUV9QzB0g Eu recomendo assistir para um entendimento mais profundo.

Vbp
fonte