Вы заметите сходство между веб-приложениями Ruby, созданными с использованием Rails, Sinatra, Roda или одного из множества других вариантов. Во всех них вы найдете сходство, поскольку они интегрируются с веб-серверами и сторонними библиотеками. Rack делает это возможным.

Rack - это HTTP-интерфейс для Ruby. Rack определяет стандартный интерфейс для взаимодействия с HTTP и подключения веб-серверов. Rack упрощает написание HTTP-приложений на Ruby. Стоечные приложения потрясающе просты. Есть код, который принимает запрос, а код обслуживает ответ. Стойка определяет интерфейс между ними.

Это пошаговое руководство охватывает Rack от начала до приложений, промежуточного программного обеспечения, стеков промежуточного программного обеспечения, тестирования, интеграции и, наконец, веб-серверов. К концу этого пошагового руководства вы гораздо лучше поймете, как работает веб-стек Ruby.

Приложения Dead Simple Rack

Стоечные приложения - это объекты, которые реагируют на call. Они должны вернуть тройку. Триплет содержит код состояния, заголовки и тело. Вот пример класса, который показывает "привет, мир".

class HelloWorld
  def response
   [ 200, { }, [ ‘Hello World’ ] ]
  end
end

Этот класс не является приложением Rack. Он демонстрирует, как выглядит тройня. Первый элемент - это код ответа HTTP. Второй - это хеш заголовков. Третий - это перечислимый объект, представляющий тело. Мы можем использовать наш класс HelloWorld для создания простого стоечного приложения. Мы знаем, что нам нужно создать объект, который реагирует на вызов. call принимает один аргумент: среда стойки. Мы вернемся к envпозже.

class HelloWorldApp
  def self.call(env)
    HellowWorld.new.response
  end
end

Теперь для работающего серверного скрипта:

require ‘rack’
require ‘rack/server’
class HelloWorld
  def response
    [ 200, { }, [ ‘Hello World’ ] ]
  end
end
class HelloWorldApp
  def self.call(env)
    HelloWorld.new.response
  end
end
Rack::Server.start :app => HelloWorldApp

Вот что происходит, когда вы запускаете этот скрипт:

$ ruby hello_world.rb
>> Thin web server (v1.4.1 codename Chromeo)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:8080, CTRL+C to stop

ПРИМЕЧАНИЕ. Отображаемый результат может отличаться. Rack::Server выбирает сервер в зависимости от того, что установлено, в порядке предпочтения. Он использует Webrick, если у вас ничего не установлено, потому что это часть стандартной библиотеки Ruby. Подробнее о серверах позже.

Просто откройте http://localhost:8080, и вы увидите в браузере «Hello World». Это не модно, но вы только что написали свое первое стоечное приложение! Мы не писали собственный сервер, и это нормально. Собственно говоря, это фантастика. Скорее всего, вам никогда не понадобится писать собственный сервер. Есть множество серверов на выбор: Тонкий, Единорог, Радуга, Голиаф, Пума и Пассажир. Вы же не хотите их писать. Вы хотите писать приложения.

Env

class HelloWorldApp
  def self.call(env)
    [ 200, { }, [ "Hello World. You said: #{env['QUERY_STRING']}" ]]
  end
end
Rack::Server.start :app => HelloWorldApp

Теперь посетите http://localhost:8080?message=foo, и на странице вы увидите сообщение = foo. Если вас интересует env, вы можете сделать следующее:

class EnvInspector
  def self.call(env)
    [ 200, { }, [ env.inspect ] ]
  end
end
Rack::Server.start :app => EnvInspector

Теперь curl URL и все, что входит в env. Это стандартный Hash экземпляр.

{
  "SERVER_SOFTWARE"=>"thin 1.4.1 codename Chromeo",
  "SERVER_NAME"=>"localhost",
  "rack.input"=>#<StringIO:0x007fa1bce039f8>,
  "rack.version"=>[1, 0],
  "rack.errors"=>#<IO:<STDERR>>,
  "rack.multithread"=>false,
  "rack.multiprocess"=>false,
  "rack.run_once"=>false,
  "REQUEST_METHOD"=>"GET",
  "REQUEST_PATH"=>"/favicon.ico",
  "PATH_INFO"=>"/favicon.ico",
  "REQUEST_URI"=>"/favicon.ico",
  "HTTP_VERSION"=>"HTTP/1.1",
  "HTTP_HOST"=>"localhost:8080",
  "HTTP_CONNECTION"=>"keep-alive",
  "HTTP_ACCEPT"=>"*/*",
  "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10http://localhost:8080?message=foo4) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.47 Safari/536.11",
  "HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch",
  "HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8",
  "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.3",
  "HTTP_COOKIE"=> "_gauges_unique_year=1;  _gauges_unique_month=1",
  "GATEWAY_INTERFACE"=>"CGI/1.2",
  "SERVER_PORT"=>"8080",
  "QUERY_STRING"=>"",
  "SERVER_PROTOCOL"=>"HTTP/1.1",
  "rack.url_scheme"=>"http",
  "SCRIPT_NAME"=>"",
  "REMOTE_ADDR"=>"127.0.0.1",
  "async.callback"=>#<Method: Thin::Connection#post_process>,
  "async.close"=>#<EventMachine::DefaultDeferrable:0x007fa1bce35b88
}

Возможно, вы заметили, что env не выполняет никакого сложного синтаксического анализа. Строка запроса не была хешем. Это была веревка. Это необработанные данные. Стойка проста в понимании и использовании. Вы могли работать только с хешами и тройками. Однако это утомительно и не масштабируется. Сложным приложениям нужны абстракции. Введите Rack::Request и Rack::Response.

Дополнительная информация

Абстракции

class HelloWorldApp
  def self.call(env)
    request = Rack::Request.new env
    request.params # contains the union of GET and POST params
    request.xhr?   # requested with AJAX
    require.body   # the incoming request IO stream
    if request.params['message']
      [ 200, { }, [ request.params['message' ] ]
    else
      [ 400, { }, [ 'Say something to me!' ] ]
    end
  end
end

Rack::Request - это просто прокси для хеша env. Базовый хеш env изменен, так что имейте это в виду.

Rack::Response - это абстракция триплетов ответов. Это упрощает доступ к заголовкам, файлам cookie и телу. Вот пример:

class HelloWorldApp
  def self.call(env)
    response = Rack::Response.new
    response.write 'Hello World'      # write some content to the body
    response.body = [ 'Hello World' ] # or set it directly
    response['X-Custom-Header'] = 'foo'
    response.set_cookie 'bar', 'baz'
    response.status = 202
response.finish # return the generated triplet
  end
end

Это базовые абстракции. Они не требуют особых объяснений. Вы можете узнать о них больше, прочитав документацию.

Теперь мы создаем более сложные приложения на этих абстракциях. Трудно создать приложение, когда вся логика находится в одном классе. Приложения состоят из разных классов с разными обязанностями. Эти дискретные фрагменты называются «промежуточным программным обеспечением».

Дополнительная информация

ПО промежуточного слоя

Стоечные приложения - это объекты, которые реагируют на call. Внутри call мы можем делать все, что захотим, например, можем делегировать другому классу. Вот пример:

class ParamsParser
  def initialize(app)
    @app = app
  end
  def call(env)
    request = Rack::Request.new env
    env['params'] = request.params
    app.call env
  end
end
class HelloWorldApp
  def self.call(env)
    parser = ParamsParser.new self
    env = parser.call env
    # env['params'] is now set to a hash for all the input paramters
    [ 200, { }, [ env['params'].inspect ] ]
  end
end

Вот промежуточное ПО "null op":

class Middleware
  def initialize(app)
    @app = app
  end
  # This is a "null" middlware because it simply calls the next one.
  # We can manipulate the input before calling the next middleware
  # or manipulate the response before returning up the chain.
  def call(env)
    @app.call env
  end
end

Еще одно промежуточное ПО, изменяющее ответ восходящего потока:

class Middleware
  def initialize(app)
    @app = app
  end
  # the My-Custom-Header on every request before sending to the
  # application before processing.
  def call(env)
    @app.call(env.merge({
      'HTTP_MY_CUSTOM_HEADER' => 'foo'
    })
  end
end

Наконец, промежуточное ПО, которое изменяет запрос перед последующей обработкой:

class Middleware
  def initialize(app)
    @app = app
  end
  # Set My-Custom-Header on every response before sending to the
  # web server
  def call(env)
    status, headers, body = @app.call env
    [ status, headers.merge({ 'My-Custom-Header' => 'foo' }), body ]
  end
end

Стек промежуточного программного обеспечения иллюстрирует шаблон построителя. Составление приложений Rack настолько распространено (и необходимо), что Rack включает в себя класс, упрощающий эту задачу.

Стеки промежуточного программного обеспечения

Rack::Builder создает стек промежуточного программного обеспечения. Каждый объект вызывает следующий и возвращает его возвращаемое значение. Стойка содержит множество удобных промежуточных программ. У них есть один для кеширования и кодирования. Давайте увеличим производительность наших тестовых приложений.

# this returns an app that responds to call cascading down the list of
# middlewares. Technically there is no difference between "use" and
# "run". "run" is just there to illustrate that it's the end of the
# chain.
app = Rack::Builder.new do
  use Rack::Etag            # Add an ETag
  use Rack::ConditionalGet  # Support Caching
  use Rack::Deflator        # GZip
  run HelloWorldApp         # Say Hello
end
Rack::Server.start :app => app

app имеет call метод, который генерирует это дерево вызовов:

Rack::Etag
 Rack::ConditionalGet
  Rack::Deflator
   HelloWorldApp

Мы пропустим функциональность каждого промежуточного программного обеспечения, потому что это не важно. Это пример того, как вы можете наращивать функциональность в приложениях. Промежуточное ПО - мощное средство. Вы можете добавить управление входящими данными перед переходом к следующему или изменить ответ существующего. Давайте создадим их для практики.

class EnsureJsonResponse
  def initialize(app)
    @app = app
  end
  # Set the 'Accept' header to 'application/json' no matter what.
  # Hopefully the next middleware respects the accept header :)
  def call(env)
    env['HTTP_ACCEPT'] = 'application/json'
    @app.call env
  end
end

И другой:

class Timer
  def initialize(app)
    @app = app
  end
  def call(env)
    before = Time.now
    status, headers, body = @app.call env
    headers['X-Timing'] = (Time.now - before).to_i.to_s
    [ status, headers, body ]
  end
end

Теперь мы можем использовать это промежуточное ПО в нашем приложении.

app = Rack::Builder.new do
  use Timer # put the timer at the top so it captures everything below it
  use EnsureJsonResponse
  run HelloWorldApp
end
Rack::Server.start :app => app

Мы только что написали собственное промежуточное программное обеспечение и узнали, как сгенерировать работающее приложение со стеком промежуточного программного обеспечения. Так на практике пишутся стоечные приложения. Теперь перейдем к последней части головоломки: config.ru

Дополнительные ресурсы

  • Rack-contrib - Набор удобного промежуточного программного обеспечения, такого как Rack::PostBodyContentTypeParser для обработки application/json тел запросов.
  • Многообразие - Мертвая простая поддержка CORS для API Rack::URLMap . Сопоставьте разные подпути с разными приложениями для стоек. Пример: /foo -> FooApp или /bar -> BarApp.

Стеллаж

# HelloWorldApp defintion
# EnsureJsonResponse defintion
# Timer definition
use Timer
use EnsureJsonResponse
run HelloWorldApp

Теперь перейдите в правильный каталог и запустите: rackup, и вы увидите:

$ rackup
>> Thin web server (v1.4.1 codename Chromeo)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:8080, CTRL+C to stop

ПРИМЕЧАНИЕ. Точный вывод может отличаться от системы к системе в зависимости от установленных гемов и их версий.

rackup предпочитает более качественные серверы, такие как Thin, а не WeBrick. Код внутри config.ru оценивается и строится с использованием Rack::Builder, который генерирует объект, совместимый с API стойки. Объект передается на стоечный сервер (Тонкий). Стоечный сервер переводит приложение в оперативный режим.

СОВЕТ: ваш веб-сервер (например, puma, thin) обычно предоставляет собственный интерфейс командной строки с параметрами, специфичными для сервера. rackup предоставляет способ пересылки параметров на базовый сервер приложений, но это может работать не во всех случаях. Базовый сервер приложений также может иметь отдельный файл конфигурации для своей конкретной конфигурации (например, порт / сокет управления). Вам рекомендуется понимать, что вы можете запустить свой веб-сервер несколькими способами. Изучите и узнайте, какой вариант подходит вам.

Рельсы и стойки

Rails 3+ полностью совместим с Rack. Приложение Rails 3 - более сложное приложение Rack. Он использует сложный стек промежуточного программного обеспечения. Диспетчер - это последняя промежуточная программа. Диспетчер читает таблицу маршрутизации и вызывает правильный контроллер и метод. Вот стандартный стек промежуточного программного обеспечения, используемый в производстве:

use Rack::Cache
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fce77f21690>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
run YourApp::Application.routes

И файл стеллажа:

# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment',  __FILE__)
run Example::Application

Вы знаете, что Example::Application должен иметь call метод. Вот реализация этого метода из стабильной версии 3.2:

# Rails::Application, Rails::Application < Rails::Engine
def call(env)
  env["ORIGINAL_FULLPATH"] = build_original_fullpath(env)
  super(env)
end
# Rails::Engine
# the super class method
def call(env)
  app.call(env.merge!(env_config))
end
# app method in the super class
def app
  @app ||= begin
    config.middleware = config.middleware.merge_into(default_middleware_stack)
    config.middleware.build(endpoint)
  end
end

Тестирование

require 'minitest/autorun'
require 'rack/test'
class HelloWorldApp
  def self.call(env)
    [ 200, { 'Content-Type' => 'text/plain' }, [ 'Hello World' ] ]
  end
end
class HelloWorldAppTest < MiniTest::Test
  include Rack::Test::Methods (1)
  def app
    HelloWorldApp (2)
  end
  def test_returns_hello_world
    get '/' (3)
    assert last_response.ok?
    assert_equal 'Hello World', last_response.body
    assert_equal 'text/plain', last_response.content_type
  end
end
  1. Включите методы для использования в тестовом примере
  2. Определите объект, совместимый со стойкой, чтобы также делать «запросы»
  3. Сделайте запрос на получение /, ответ сохраняется в last_response

Серверы

Мы рассмотрели, как rackup и Rack::Server использовать доступные серверные библиотеки для запуска реального HTTP-сервера. Сообщество Ruby предлагает множество вариантов для различных случаев использования. Давайте рассмотрим их, чтобы вы могли сделать лучший выбор.

WeBrick

Этот сервер входит в стандартную библиотеку. Он предназначен только для разработки. Webrick - игрушечная реализация. Его никогда не следует использовать для чего-либо, отдаленно близкого к производственному.

Тонкий

Этот сервер использует шаблон реактора, предоставленный Eventmachine. Раньше он был популярен, но его заменили лучшие альтернативы. Если вы не можете использовать ничего лучшего, то выбирайте худой. Лучше, чем Уэбрик.

Пума

Puma является многопоточным и многопроцессорным. Это самый гибкий сервер для чистого Ruby-приложения. Вы можете использовать многопроцессорность (также известный как кластерный режим), если ваша платформа Ruby имеет GIL (читай: MRI), а ваше приложение связано с процессором. Приложения с привязкой к вводу-выводу (большинство веб-приложений) могут работать в многопоточном режиме (режим по умолчанию), поскольку GIL блокирует истинный параллелизм. Puma также включает интерфейс управления HTTP для программных манипуляций или получения статистики. Проверьте puma, прежде чем покупать что-либо еще, особенно если вы используете JRuby или Rubinius.

Единорог

Unicorn использует модель предварительного разветвления. Это означает, что он запускает несколько процессов и использует системные вызовы для «балансировки нагрузки» между дочерними процессами. Пользователи должны помнить, что соединения с внешними ресурсами (такими как база данных) должны происходить в дочернем процессе. Unicorn предлагает для этого «вилку после». Пользователи, заблокированные на МРТ, должны изучить этот вариант.

Пассажир

Пассажир немного отличается от остальных в списке. Passenger - это модуль nginx или apache. Он также имеет автономный режим. Он поддерживает многопоточные и многопроцессорные модели. Вам следует подумать о пассажирах, если вам нужно развернуть приложение на существующем сервере apache или nginx.

Вывод

Это конец прохождения. Мы рассмотрели:

  1. Интерфейс call и тройки стоек
  2. Разница между промежуточным ПО и серверами приложений
  3. Тестирование приложений Rack с помощью rack-test gem
  4. Как работает rackup команда config.ru
  5. Общие веб-серверы

Теперь вы лучше подготовлены для использования этих абстракций в своих приложениях и библиотеках.

Этот пост был первоначально опубликован на https://www.slashdeploy.com 12 января 2019 года.