Pull to refresh

Разработка web-приложений на языке Common Lisp (часть третья)

Reading time6 min
Views4.1K
Данный обзор является небольшим путеводителем для тех, решился (или решается) доверить этому чудесному языку будущее своего стартапа. Несмотря на то, что основной акцент будет ставиться на web-разработке, я постараюсь осветить также и более общие темы, так или иначе связанные с Common Lisp. Материал почерпнут из собственного опыта разработки web-сервиса AlterMoby.

Третья часть этого обзора будет посвящена web-серверу Hunchentoot. Рассмотрим его архитектуру и базовые возможности. Кроме того, затронем некоторые смежные вопросы, в частности, генерацию HTML/XML.

image

Выбор web-сервера


После развёртывания минимальной Lisp-системы в прошлой части данного обзора пришло время выбрать web-сервер. Этот вопрос не очевиден — тот же CLiki даёт ссылки на 7 разных библиотек. Когда я выбирал web-сервер для работы, исходил из нескольких факторов. Во-первых, он должен быть известным в CL-сообществе, поддерживаться широким кругом энтузиастов и находиться в актуальном состоянии вплоть до нынешнего времени. Во-вторых, на нём должен базироваться хотя бы один перспективный web-фреймворк. Кроме того, желательно удостовериться в наличии коммерчески успешных сайтов на его основе. Исходя из этих критериев, в моё поле зрения попало всего два продукта: Portable AllegroServ и Hunchentoot.

Portable AllegroServ является версией AllegroServ – оригинального web-сервера, написанного специально для Allegro CL. Исходный код последнего не совместим с Common Lisp, поскольку использует нестандартные расширения родного компилятора. Дело в том, что автор AllegroServ стремился достичь максимальной эффективности, потому и был вынужден отходить от строгого стандарта. В итоге получился быстрый и элегантный web-сервер, который, наверное, и по сей день является лучшим выбором для Allegro CL. Поскольку исходный код AllegroServ выпущен под лицензией LLGPL, нашлись энтузиасты, адаптировавшие его к стандартному Common Lisp. Получившейся продукт назвали Portable AllegroServ. По мнению некоторых лисперов последний уступает оригиналу, как в производительности, так и в надёжности.

Главный герой этой части нашего обзора, web-сервер со странным названием Hunchentoot изначально ориентировался на стандартный Common Lisp. Потому он “прямо из коробки” может работать с большинством популярных компиляторов. Для меня существенным аргументом в пользу Hunchentoot являлся также тот факт, что на его основе базируется перспективный web-фреймворк Weblocks. Несмотря на то, что я отказался в итоге от использования этого фреймворка в своём проекте, Hunchentoot меня нисколько не разочаровал. Далее рассмотрим структуру и функционал этого прекрасного продукта.

Знакомство с Hunchentoot


Прежде всего, удостоверимся в том, что мы поставили Hunchentoot:

(asdf:oos 'asdf:load-op :hunchentoot)

Теперь создадим и запустим экземпляр класса acceptor – приёмщика http-запросов:

(hunchentoot:start (make-instance 'hunchentoot:acceptor :port 8080))

Чтобы Hunchentoot мог принимать https-запросы, нужно заменить ‘acceptor на ‘ssl-acceptor (его потомка), не забыв указать файлы сертификата и ключа.

Рассмотрим простейший обработчик, взятый из справки по Hunchentoot:

(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
  (setf (hunchentoot:content-type*"text/plain")
  (format nil "Hey~@[ ~a~]!" name))


Данный обработчик называется say-yo, привязывается к URL /yo и принимает единственный параметр name (через GET или POST). Обработчик генерирует текстовый документ, содержащий “Hey имя!”, где имя – значение параметра name, или же просто “Hey!”, если параметр name не задан (т.е. равен nil). Если вас смутила замысловатость форматирования строки, познакомьтесь с директивами функции format. Проверьте работу данного обработчика, перейдя на /yo и /yo?name=Vasia.

Вместо текста обработчик может отдавать нужный файл:

(hunchentoot:define-easy-handler (get-file :uri "/file") (name)
   (handle-static-file (format nil "/home/me/folder/~a" name))


В принципе, этого функционала должно хватить для написания небольшой любительской поделки. Более серьёзная работа потребует отказаться от макроса define-easy-handler, разобравшись в механизме диспетчеризации запросов. Обрисуем основные черты последнего.

Каждому экземпляру класса acceptor (а значит и класса ssl-acceptor) соответствует диспетчер запросов. Диспетчер запросов представляет собой функцию, принимающую экземпляр класса request (http-запрос) и возвращающую выходной http-поток (HTML/XML или бинарные данные). Поскольку выходной поток зависит от заданного URL, на практике диспетчер запросов его не генерирует, а занимается поиском соответствующего обработчика, которому и делегирует эту роль. По умолчанию диспетчер запросов просматривает список *dispatch-table*, содержащий функции диспетчеризации. Каждая функция диспетчеризации принимает объект запроса, анализирует его согласно своим критериям и в случае соответствия возвращает обработчик – функцию, генерирующую выходной поток (при несоответствии возвращается nil). Таким образом, стандартный диспетчер запросов по очереди запускает функции диспетчеризации до тех пор, пока одна из них не вернёт обработчик. В конце *dispatch-table* обычно располагается default-dispatcher – функция диспетчеризации, всегда возвращающая обработчик (по умолчанию это сообщение о ненайденной странице).

Вышеописанная схема диспетчеризации может показаться несколько переусложнённой, однако, на практике такая структура позволяет добиваться большой гибкости. В частности, подход с возвратом обработчика позволяет писать элегантные функции диспетчеризации. Диспетчеризация обработчиков, созданных макросом define-easy-handlers, использует эту стандартную схему: *dispatch-table* содержит функцию диспетчеризации dispatch-easy-handlers. Последняя реализует свой упрощённый диспетчер, ищущий в собственном внутреннем списке. Этот список содержит описания всех обработчиков, определённых с помощью define-easy-handlers. Таким образом, диспетчеризацию можно разбивать на множество независимых веток с собственной логикой выбора обработчика.

Hunchentoot определяет несколько генераторов стандартных функций диспетчеризации, а также некоторые стандартные обработчики. Например, create-prefix-dispatcher по заданному префиксу URL и обработчику генерирует функцию диспетчеризации, проверяющую URL запросы на соответствие заданному префиксу. Функция create-regex-dispatcher аналогична предыдущей, но генерирует функцию диспетчеризации, сопоставляющую URLs заданному шаблону регулярного выражения. Функция create-folder-dispatcher-and-handler принимает префикс URL и путь к директории, возвращая функцию диспетчеризации, раздающую файлы из заданной директории. К стандартным обработчикам относится handle-static-file, использованный нами в определении get-file.

Гибкость Common Lisp позволяет налету переопределять существующие функции и методы, что не может не способствовать гибкости Hunchentoot. Так можно не только создать новый потомок класса acceptor, но и переопределить диспетчер по умолчанию, полностью отказавшись от вышеописанной схемы поиска обработчиков, или полностью изменить логику обработки запросов. Например, в процессе работы над своим проектом я заставил Hunchentoot проверять формат запросов multipart/form-data ещё в процессе закачки данных, а не после того, как они будут полностью приняты. Это позволяет избежать ситуации, когда при DoS-атаке множество машин постят на сервер многомегабайтный мусор.

Являясь мощным и современным web-сервером, Hunchentoot поддерживает множество стандартных функций. К ним относятся поддержка cookies, удобные отладка и логирование и многое другое. Описание этого функционала вы сможете найти на его web-странице.

Генерация HTML/XML


В заключение этой части нашего обзора рассмотрим библиотеку CL-WHO, определяющую простой и удобный DSL для генерации HTML/XML. Как водится, лучше один раз увидеть:

(push :tag3 *html-empty-tags*)
(with-html-output-to-string (http-stream)
  (:tag1 :attr1 1 :attr2 "2"
    (:tag2 :attr3 (+ 1 2))
    (loop for i from 1 to 3 do
      (with-html-output (http-stream)
        (:tag3 :attr4 (when (oddp i"odd"))))))


Результат выполнения этой конструкции не противоречит интуиции:

"<tag1 attr1='1' attr2='2'><tag2 attr3='3'></tag2>
<tag3 attr4='odd' /><tag3 /><tag3 attr4='odd' /></tag1>"


Как видите, макрос with-html-output-to-string строит XML-конструкцию из открывающих и закрывающих тегов, соответствующих данному s-выражению. Первая команда добавляет тег tag3 в список одиночных тегов. Для такого тега при отсутствии вложений не будет генерироваться закрывающий тег. Далее вызывается макрос with-html-output-to-string, являющийся обёрткой для with-html-output – основного макроса CL-WHO. Из примера видно, что допускаются произвольные управляющие конструкции для автоматизации генерации кода.

Результирующая строка была создана в режиме XML. В этом режиме стоит генерировать XML/XHTML документы. Для работы с HTML есть режим SGML, в котором пустые теги не содержат закрывающий слеш, а атрибуты могут быть без значений. Как и в случае с Hunchentoot для более углублённого ознакомления с CL-WHO отправляю вас на её web-страницу.
Tags:
Hubs:
Total votes 34: ↑32 and ↓2+30
Comments59

Articles