Оригинал этой статьи опубликован в журнале «Системный администратор» №5 (150) за май 2015. Прошу обратить внимание на год — какие-то моменты могут расходиться с современными версиями языка и библиотек…
Библиотека Rack — простой объектный интерфейс для написания веб-приложений.
Слово «rack» в английском языке имеет множество значений, включая такие, как «пытка» и «разрушение»… Однако, надо полагать, название рассматриваемой библиотеки произошло от другой группы смыслов: «стойка», «штатив», «каркас» и т.д. Rack обеспечивает простой и в то же время удобный интерфейс, обеспечивающий взаимодействие между веб-сервером и приложением, позволяя программисту сосредоточиться исключительно на логике последнего.
Этот интерфейс достаточно низкоуровневый и не ограничивает разработчика каким-либо заранее заданным способом организации приложения и высокоуровневыми абстракциями. Соответственно, он и не предоставляет таких абстракций — это уже дело фреймворков, которые работают поверх него: Rails, Sinatra и других.
Зачем знать Rack?
Практически вся веб-разработка на Ruby использует Rack, как правило — посредством того или иного более высокоуровневого фреймворка. Но это не обязательно, задачи бывают разные: для каких-то те же Rails слишком тяжеловесны, для каких-то — слишком «заточены» под определенное использование и структуру программы.
Можно выделить три цели изучения именно Rack как такового:
- Понимание того, что находится у популярных фреймворков «под капотом», чтобы ориентироваться в более-менее сложных случаях, не предусмотренных их создателями.
- Написание небольших веб-приложений для простых задач, когда использование чего-то более тяжелого будет напрасной тратой ресурсов.
- Разработка сложных и необычных веб-приложений, которые не вписываются в идеологию и структуру существующих фреймворков. MVC1 — хорошая и проверенная временем концепция, но все же не панацея и не «серебряная пуля».
Как это работает
Для начала установим соответствующий гем:
Установить можно было бы и без sudo
, т.е. только для локального пользователя. Но в этом случае мы не сможем
использовать предоставляемые гемом исполняемые файлы, без которых обойтись можно, но не хочется.
Простейшее веб-приложение может выглядеть, например, так2:
Запускаем:
И теперь мы можем, зайдя браузером по адресу http://localhost:8080/, видеть список HTTP-заголовков и переменных
сервера в виде объекта класса Hash
. 8080 здесь — порт по умолчанию, он используется если при вызове run
не указан
какой-либо другой.
Как можно видеть из кода выше, основная рабочая часть у нас — это Proc
-объект, который получает на вход хэш
с переменными, и возвращает специальным образом определенный массив. Поскольку в Ruby принято использовать
«утиную» типизацию, класс объекта на самом деле не важен, важен метод call
, принимающий хэш и возвращающий
соответствующий массив.
Массив должен содержать три элемента: код ответа (в нашем случае — «200 OK»3), HTTP-заголовки ответа в виде хэша и тело ответа в виде массива (точнее опять же произвольного объекта, позволяющего последовательный перебор элементов), содержащего строки.
Итак, наше приложение, т.е. объект с методом call
, запускается неким обработчиком, в данном случае — WEBrick
,
который определяет используемый сервер. Серверы могут быть разными, каждый имеет свои преимущества и недостатки,
причем для серьезной работы под реальной нагрузкой WEBrick
, вероятно, хуже всех, однако только он входит
в стандартную библиотеку Ruby, соответственно его не придется отдельно устанавливать в большинстве случаев.
Останавливается сервер нажатием Ctrl+C
в консоли, где он запущен (в некоторых случаях, в зависимости от версий ПО
и системного окружения, может потребоваться Ctrl+Alt+C
). Это корректный способ прерывания работы, высвобождающий
системные ресурсы (такие, например, как занятый порт), хотя, конечно, в реальных проектах следует предусмотреть
альтернативный способ останова/перезапуска, чтобы выполнить какие-то дополнительные действия — сохранить данные, например.
Разобравшись с простым примером, рассмотрим его же, но немного иначе запущенный. Нам понадобится файл с расширением
.ru
следующего содержания:
Воспользуемся утилитой rackup
:
Изменился порт по умолчанию, не требуется явное указание «require 'rack'
», и, что гораздо интереснее, наш исходный
код абстрагировался от выбора сервера. Его при использовании rackup
тоже можно указать в явном виде, но уже
в параметрах командной строки (как и номер порта).
В целом, rackup
продолжает доброе дело абстрагирования разработчика от каких-то вещей, которые должны быть скорее
в ведении администратора.
Rack::Request и разбор параметров
Данные запроса приходят в параметре env
в виде текстовых строк, как они собственно и определены протоколом HTTP.
Однако некоторые из них на самом деле имеют свою внутреннюю структуру и для нормальной работы их еще предстоит
распарсить. Можно, конечно, это каждый раз делать вручную, но Rack предоставляет более удобный вариант — класс Rack::Request
.
Так, мы можем получить параметры в виде хэша — заменим нашу процедуру (в новом .ru
-файле) на такую:
Запустим приложение:
И обратимся, например, по адресу http://localhost:9292/?alpha=beta&gamma=delta. В браузере мы увидим:
Вместо строковой переменной из demo01.ru
:
Аналогично в удобном структурированном виде представлены и куки.
Кроме того, состав переменных в хэше env
зависит от выбранного сервера, отличия небольшие, но учитывать
их придется, и лучше пусть это сделает за нас Rack::Request
, методы которого всегда выдают одну и ту же
информацию4.
Для формирования ответа тоже есть специальный класс — Rack::Response
5.
С его помощью пример можно переписать так:
Важно не забыть про метод finish
, который собственно и формирует необходимый ответ.
Декомпозиция
Очевидно, более-менее сложное приложение в одной процедуре или методе не поместится. Поэтому, как правило,
приложение все-таки представляет собой отдельный объект (модуль/класс), в котором метод call
обращается
к более специфическим методам. Но это — процедурная декомпозиция, подходящая для разных действий одного объекта.
Если наше приложение содержит логически различные части, имеет смысл вынести эту логику в разные объекты.
И здесь Rack предоставляет два механизма декомпозиции — в разных, если можно так выразиться, измерениях.
Во-первых, мы можем назначить разные объекты на разные пути внутри нашего сайта. Например, адреса, начинающиеся
с /img/
будут читать из какого-то каталога на диске файлы изображений и просто отдавать их. А все прочие адреса
уже обрабатываться более сложным образом.
Во-вторых, некоторые действия, такие как, например, проверка пользовательской сессии, нужно производить при каждом вызове, до основной логики.
Для этих целей используются механизмы роутинга и «прослоек» (middleware в терминологии Rack), предоставляемые
классом Rack::Builder
. Как это выглядит? Примерно так (создадим очередной .ru
-файл):
Здесь класс Log
— та самая прослойка, которая будет вызываться при любом запросе страницы. А приложений — два:
одно генерирует JSON-представление, а другое, как и раньше простой текст. Как видим, прослойка подключается методом
use
, а приложение для определенного пути — в блоке метода map
. Кстати, внутри этого блока можно добавлять
и отдельные прослойки, если они нужны только там, а не глобально.
В результате вызова Rack::Builder.app
мы получаем комбинированное rack-приложение, которое и запускаем последней строчкой.
На самом деле, в .ru
-файле такое описание избыточно, поскольку его содержимое запускается утилитой rackup
уже внутри блока
Rack::Bilder
, т.е. мы могли бы написать и просто:
Штатные дополнения
Некоторые задачи при создании веб-приложений встречаются регулярно — практически всегда требуется вести какое-то логирование,
отдавать статические файлы и так далее. Гем 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/. ↩