Pull to refresh

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

Reading time 6 min
Views 3.7K
Website development *
Данный обзор является небольшим путеводителем для тех, решился (или решается) доверить этому чудесному языку будущее своего стартапа. Несмотря на то, что основной акцент будет ставиться на 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
Comments 59
Comments Comments 59

Articles