Вы заметите сходство между веб-приложениями 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=foo
4) 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
- Включите методы для использования в тестовом примере
- Определите объект, совместимый со стойкой, чтобы также делать «запросы»
- Сделайте запрос на получение
/
, ответ сохраняется в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.
Вывод
Это конец прохождения. Мы рассмотрели:
- Интерфейс
call
и тройки стоек - Разница между промежуточным ПО и серверами приложений
- Тестирование приложений Rack с помощью
rack-test
gem - Как работает
rackup
командаconfig.ru
- Общие веб-серверы
Теперь вы лучше подготовлены для использования этих абстракций в своих приложениях и библиотеках.
Этот пост был первоначально опубликован на https://www.slashdeploy.com 12 января 2019 года.