Данный обзор является небольшим путеводителем для тех, решился (или решается) доверить этому чудесному языку будущее своего стартапа. Несмотря на то, что основной акцент будет ставиться на web-разработке, я постараюсь осветить также и более общие темы, так или иначе связанные с Common Lisp. Материал почерпнут из собственного опыта разработки web-сервиса AlterMoby.
Третья часть этого обзора будет посвящена web-серверу Hunchentoot. Рассмотрим его архитектуру и базовые возможности. Кроме того, затронем некоторые смежные вопросы, в частности, генерацию HTML/XML.
После развёртывания минимальной 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:
Теперь создадим и запустим экземпляр класса acceptor – приёмщика http-запросов:
Чтобы Hunchentoot мог принимать https-запросы, нужно заменить ‘acceptor на ‘ssl-acceptor (его потомка), не забыв указать файлы сертификата и ключа.
Рассмотрим простейший обработчик, взятый из справки по Hunchentoot:
Данный обработчик называется say-yo, привязывается к URL /yo и принимает единственный параметр name (через GET или POST). Обработчик генерирует текстовый документ, содержащий “Hey имя!”, где имя – значение параметра name, или же просто “Hey!”, если параметр name не задан (т.е. равен nil). Если вас смутила замысловатость форматирования строки, познакомьтесь с директивами функции format. Проверьте работу данного обработчика, перейдя на /yo и /yo?name=Vasia.
Вместо текста обработчик может отдавать нужный файл:
В принципе, этого функционала должно хватить для написания небольшой любительской поделки. Более серьёзная работа потребует отказаться от макроса 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-странице.
В заключение этой части нашего обзора рассмотрим библиотеку CL-WHO, определяющую простой и удобный DSL для генерации HTML/XML. Как водится, лучше один раз увидеть:
Результат выполнения этой конструкции не противоречит интуиции:
Как видите, макрос 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-страницу.
Третья часть этого обзора будет посвящена web-серверу Hunchentoot. Рассмотрим его архитектуру и базовые возможности. Кроме того, затронем некоторые смежные вопросы, в частности, генерацию HTML/XML.
Выбор 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-страницу.