Оригинал этой статьи опубликован в журнале «Системный администратор» №5 (150) за май 2015. Прошу обратить внимание на год — какие-то моменты могут расходиться с современными версиями языка и библиотек…

Библиотека Rack — простой объектный интерфейс для написания веб-приложений.
Слово «rack» в английском языке имеет множество значений, включая такие, как «пытка» и «разрушение»… Однако, надо полагать, название рассматриваемой библиотеки произошло от другой группы смыслов: «стойка», «штатив», «каркас» и т.д. Rack обеспечивает простой и в то же время удобный интерфейс, обеспечивающий взаимодействие между веб-сервером и приложением, позволяя программисту сосредоточиться исключительно на логике последнего.
Этот интерфейс достаточно низкоуровневый и не ограничивает разработчика каким-либо заранее заданным способом организации приложения и высокоуровневыми абстракциями. Соответственно, он и не предоставляет таких абстракций — это уже дело фреймворков, которые работают поверх него: Rails, Sinatra и других.
Зачем знать Rack?
Практически вся веб-разработка на Ruby использует Rack, как правило — посредством того или иного более высокоуровневого фреймворка. Но это не обязательно, задачи бывают разные: для каких-то те же Rails слишком тяжеловесны, для каких-то — слишком «заточены» под определенное использование и структуру программы.
Можно выделить три цели изучения именно Rack как такового:
- Понимание того, что находится у популярных фреймворков «под капотом», чтобы ориентироваться в более-менее сложных случаях, не предусмотренных их создателями.
- Написание небольших веб-приложений для простых задач, когда использование чего-то более тяжелого будет напрасной тратой ресурсов.
- Разработка сложных и необычных веб-приложений, которые не вписываются в идеологию и структуру существующих фреймворков. MVC1 — хорошая и проверенная временем концепция, но все же не панацея и не «серебряная пуля».
Как это работает
Для начала установим соответствующий гем:
$ sudo gem install rack
Fetching: rack-1.6.0.gem (100%)
Successfully installed rack-1.6.0
1 gem installedУстановить можно было бы и без sudo, т.е. только для локального пользователя. Но в этом случае мы не сможем
использовать предоставляемые гемом исполняемые файлы, без которых обойтись можно, но не хочется.
Простейшее веб-приложение может выглядеть, например, так2:
require 'pp'
require 'rack'
app = proc do |env|
  [
    200,
    { 'Content-Type' => 'text/plain' },
    [ env.pretty_inspect ]
  ]
end
Rack::Handler::WEBrick.run appЗапускаем:
$ ruby demo01.rb
[2015-03-24 14:07:11] INFO  WEBrick 1.3.1
[2015-03-24 14:07:11] INFO  ruby 2.1.5 (2014-11-13) [x86_64-linux]
[2015-03-24 14:07:11] INFO  WEBrick::HTTPServer♯start: pid=6883 port=8080И теперь мы можем, зайдя браузером по адресу http://localhost:8080/, видеть список HTTP-заголовков и переменных
сервера в виде объекта класса Hash. 8080 здесь — порт по умолчанию, он используется если при вызове run не указан
какой-либо другой.
Как можно видеть из кода выше, основная рабочая часть у нас — это Proc-объект, который получает на вход хэш
с переменными, и возвращает специальным образом определенный массив. Поскольку в Ruby принято использовать
«утиную» типизацию, класс объекта на самом деле не важен, важен метод call, принимающий хэш и возвращающий
соответствующий массив.
Массив должен содержать три элемента: код ответа (в нашем случае — «200 OK»3), HTTP-заголовки ответа в виде хэша и тело ответа в виде массива (точнее опять же произвольного объекта, позволяющего последовательный перебор элементов), содержащего строки.
Итак, наше приложение, т.е. объект с методом call, запускается неким обработчиком, в данном случае — WEBrick,
который определяет используемый сервер. Серверы могут быть разными, каждый имеет свои преимущества и недостатки,
причем для серьезной работы под реальной нагрузкой WEBrick, вероятно, хуже всех, однако только он входит
в стандартную библиотеку Ruby, соответственно его не придется отдельно устанавливать в большинстве случаев.
Останавливается сервер нажатием Ctrl+C в консоли, где он запущен (в некоторых случаях, в зависимости от версий ПО
и системного окружения, может потребоваться Ctrl+Alt+C). Это корректный способ прерывания работы, высвобождающий
системные ресурсы (такие, например, как занятый порт), хотя, конечно, в реальных проектах следует предусмотреть
альтернативный способ останова/перезапуска, чтобы выполнить какие-то дополнительные действия — сохранить данные, например.
Разобравшись с простым примером, рассмотрим его же, но немного иначе запущенный. Нам понадобится файл с расширением
.ru следующего содержания:
require 'pp'
app = proc do |env|
  [
    200,
    { 'Content-Type' => 'text/plain' },
    [ env.pretty_inspect ]
  ]
end
run appВоспользуемся утилитой rackup:
$ rackup demo01.ru
[2015-03-24 15:04:08] INFO  WEBrick 1.3.1
[2015-03-24 15:04:08] INFO  ruby 2.1.5 (2014-11-13) [x86_64-linux]
[2015-03-24 15:04:08] INFO  WEBrick::HTTPServer♯start: pid=12753 port=9292Изменился порт по умолчанию, не требуется явное указание «require 'rack'», и, что гораздо интереснее, наш исходный
код абстрагировался от выбора сервера. Его при использовании rackup тоже можно указать в явном виде, но уже
в параметрах командной строки (как и номер порта).
В целом, rackup продолжает доброе дело абстрагирования разработчика от каких-то вещей, которые должны быть скорее
в ведении администратора.
Rack::Request и разбор параметров
Данные запроса приходят в параметре env в виде текстовых строк, как они собственно и определены протоколом HTTP.
Однако некоторые из них на самом деле имеют свою внутреннюю структуру и для нормальной работы их еще предстоит
распарсить. Можно, конечно, это каждый раз делать вручную, но Rack предоставляет более удобный вариант — класс Rack::Request.
Так, мы можем получить параметры в виде хэша — заменим нашу процедуру (в новом .ru-файле) на такую:
app = proc do |env|
  req = Rack::Request.new env
  [
    200,
    { 'Content-Type' => 'text/plain' },
    [ req.params.pretty_inspect ]
  ]
endЗапустим приложение:
$ rackup demo02.ruИ обратимся, например, по адресу http://localhost:9292/?alpha=beta&gamma=delta. В браузере мы увидим:
{"alpha"=>"beta", "gamma"=>"delta"}Вместо строковой переменной из demo01.ru:
{. . .,
 "QUERY_STRING"=>"alpha=beta&gamma=delta",
 . . .}Аналогично в удобном структурированном виде представлены и куки.
Кроме того, состав переменных в хэше env зависит от выбранного сервера, отличия небольшие, но учитывать
их придется, и лучше пусть это сделает за нас Rack::Request, методы которого всегда выдают одну и ту же
информацию4.
Для формирования ответа тоже есть специальный класс — Rack::Response5.
С его помощью пример можно переписать так:
app = proc do |env|
  req = Rack::Request.new env
  res = Rack::Response.new
  res['Content-Type'] = 'text/plain'
  res.write req.params.pretty_inspect
  res.finish
endВажно не забыть про метод finish, который собственно и формирует необходимый ответ.
Декомпозиция
Очевидно, более-менее сложное приложение в одной процедуре или методе не поместится. Поэтому, как правило,
приложение все-таки представляет собой отдельный объект (модуль/класс), в котором метод call обращается
к более специфическим методам. Но это — процедурная декомпозиция, подходящая для разных действий одного объекта.
Если наше приложение содержит логически различные части, имеет смысл вынести эту логику в разные объекты.
И здесь Rack предоставляет два механизма декомпозиции — в разных, если можно так выразиться, измерениях.
Во-первых, мы можем назначить разные объекты на разные пути внутри нашего сайта. Например, адреса, начинающиеся
с /img/ будут читать из какого-то каталога на диске файлы изображений и просто отдавать их. А все прочие адреса
уже обрабатываться более сложным образом.
Во-вторых, некоторые действия, такие как, например, проверка пользовательской сессии, нужно производить при каждом вызове, до основной логики.
Для этих целей используются механизмы роутинга и «прослоек» (middleware в терминологии Rack), предоставляемые
классом Rack::Builder. Как это выглядит? Примерно так (создадим очередной .ru-файл):
class Log
  def initialize app, output = $stderr
    @app = app
    @output = output
  end
  def call env
    @output.puts env.pretty_inspect
    @app.call env
  end
end
json = proc do |env|
  [
    200,
    { 'Content-Type' => 'application/json' },
    [ JSON.generate(env) ]
  ]
end
txt = proc do |env|
  [
    200,
    { 'Content-Type' => 'text/plain' },
    [ env.pretty_inspect ]
  ]
end
app = Rack::Builder.app do
  use Log
  map '/js/' do
    run json
  end
  run txt
end
run appЗдесь класс Log — та самая прослойка, которая будет вызываться при любом запросе страницы. А приложений — два:
одно генерирует JSON-представление, а другое, как и раньше простой текст. Как видим, прослойка подключается методом
use, а приложение для определенного пути — в блоке метода map. Кстати, внутри этого блока можно добавлять
и отдельные прослойки, если они нужны только там, а не глобально.
В результате вызова Rack::Builder.app мы получаем комбинированное rack-приложение, которое и запускаем последней строчкой.
На самом деле, в .ru-файле такое описание избыточно, поскольку его содержимое запускается утилитой rackup уже внутри блока
Rack::Bilder, т.е. мы могли бы написать и просто:
use Log
map '/js/' do
  run json
end
run txtШтатные дополнения
Некоторые задачи при создании веб-приложений встречаются регулярно — практически всегда требуется вести какое-то логирование,
отдавать статические файлы и так далее. Гем rack содержит ряд классов для решения таких типовых задач, я не буду на них
подробно останавливаться6, лишь перечислю основные.
Приложения:
- 
Rack::Fileотдает статические файлы по заранее заданному пути.
- 
Rack::Directoryвыводит содержимое каталогов и отдает файлы посредствомRack::File.
Прослойки:
- 
Rack::Auth::BasicиRack::Auth::Digestпредоставляют простую стандартную HTTP-аутентификацию.
- 
Rack::LoggerиRack::CommonLoggerобеспечивают логирование.
- 
Rack::ConditionalGetиRack::ETagпозволяют уменьшить отдаваемый трафик, управляя заголовками кэширования. К сожалению, они работают с уже сформированным ответом, поэтому экономии вычислительных ресурсов сервера не получится.
- 
Rack::ContentLengthустанавливает автоматически соответствующий заголовок.
- 
Rack::Deflate— сжимает передаваемые данные, автоматически определяя, когда это можно и нужно делать.
- Модуль Rack::Sessionи определенные внутри него классыCookie,MemcacheиPoolобеспечивают управление сессиями.
- 
Rack::Staticотдает статические файлы по заранее определенным правилам.
Эти классы и модули решают типовые задачи типовыми и довольно таки прямолинейными методами, каких-то чудес от них ждать не стоит. Зато работают, что называется, «из коробки».
Дополнительно стоит упомянуть о модулях Rack::Mime и Rack::Utils, которые предоставляют различные вспомогательные средства
для веб-разработки.
Итого
Rack представляет собой очень простой объектный интерфейс для взаимодействия с веб-сервером, с одной стороны — абстрагируя разработчика от мелких деталей, с другой — не навязывая какой-то определенной архитектуры приложения. Стоит ли его использовать вместо фрейморков — зависит от задачи и личной склонности программиста. Не надо только забывать, что это «голый каркас», и большинство даже типовых задач придется решать «с нуля», тогда как в Ruby on Rails, например, они уже решены.
- 
      Model-View-Controller — широко распространенный шаблон проектирования приложений. ↩ 
- 
      Полные тексты примеров размещены на GitHub — https://gist.github.com/shikhalev/8409eef4a4e66a003670. ↩ 
- 
      См. коды ответов, например, в Википедии — https://ru.wikipedia.org/wiki/Список_кодов_состояния_HTTP. ↩ 
- 
      Список методов см. в документации — https://www.rubydoc.info/gems/rack/Rack/Request. ↩ 
- 
      За подробностями стоит обратиться к документации — http://www.rubydoc.info/gems/rack/. ↩