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

    Данный обзор является небольшим путеводителем для тех, решился (или решается) доверить этому чудесному языку будущее своего стартапа. Несмотря на то, что основной акцент будет ставиться на 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-страницу.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 59

      +2
      мсье знает толк в извращениях
        +3
        Это не извращения. Просто человеку нравится всё контролировать и работать на низком уровне без фреймворков. Я тоже этим страдаю. Это ужасно. :)

        С другой стороны, товарищ TheShock ЕМНИП собирался на асме писать фреймворк.
          +1
          Тока вот все забывают, что жизнь у нас короткая, очень, особенно для такого подхода =)
        +1
        нормальный такой сервер, действительно один из самых продвинутых на сегодняшний день и развивающихся.
        но вот писать html на сommon lisp не очень удобно, было бы лучше если бы был какой-то хороший template toolkit, который видимо никогда не появится, потому как «s-expressions это же так удобно для написания html. они же тоже имеют древовидную структуру!»
          0
          Автор RESTAS с вами согласен и поэтому написал cl-closure-template. github.com/archimag/cl-closure-template

          Но всё-таки, что такого ужасного в написании xhtml на s-expression'ах?
            0
            то, что каждый сверстаный шаблон полученный от дизайнера нужно переписывать еще на одном языке.
              0
              Статичные страницы можно и просто в html отдавать, а шаблоны всё-равно в каждом фреймворке свои, просто hunchentoot+who меняет синтаксис чуть сильнее, чем большинство.
                +1
                Особо хочется подчеркнуть что меняется только синтаксис. xhtml как язык никто не трогает, просто в who он записывается немного по-другому.
                  0
                  причём удобнее, поскольку не надо лепить закрывающие теги
                  0
                  Не надо, использовать cl-who в серьёзных проектах никто в здравом уме не будет. cl-who годится только для небольших демонстраций.
                    0
                    Аргументируете?
                      0
                      Во-первых, про дизайнеров тут уже всё сказали. Но даже если в роли дизайнера выступает программист, то всё равно использование системы шаблонов намного более эффективно. При использовании cl-who происходит жуткое раздувание основного кода, потом по нему надо тыкаться и исправлять логику. Это очень не эффективно.

                      Во-вторых, cl-who (если не лезть в кишки) не позволяет эффективно составлять страницы из разных частей.

                      Шаблонизаторы это сейчас единственный приемлемый способ генерации контента, как бы доказано всей практикой веб-разработки. Та же E-factory из lxml (Python) будет, например, удобней, чем cl-who, но никто не использует её для генерации веб-страниц.
                        0
                        Знаете, я использовал CL-WHO долго и всерьёз, и скажу вам следующее. Структура html и любых других теговых языков этого типа объективно сложнее структуры без закрывающих тегов — закрывающие теги скорее для машины, для её удобства парсинга. А то что сегодня попсовое направление за php или python совсем не означает что обучаемому человеку это будет трудно освоить.

                        Теперь о модульности и разбухании кода. Построение web-страницы при помощи CL-WHO прекрасно разбивается на функции, которые повторно используются, не говоря уже о Lisp-макросах, которые генерируют обширные фрагменты кода с нетривиальной логикой на основании короткой спецификации по-сути.
                          0
                          Насколько долго?

                          lisper.ru я сначала так и писал. Потом передела на шаблонах и насколько же всё стало проще. Закрывающиеся тэги здесь вообще не при чём, редактируйте шаблоны в нормальном редакторе (Emacs) и не будете иметь с ними никаких проблем.

                          > Построение web-страницы при помощи CL-WHO прекрасно разбивается на функции

                          Нет, не разбивается, ибо cl-who специальным образом перерабатывает дерево с кодом и генерирует на его основании новый код, вы не сможете разбить шаблон на несколько функций (если не использовать недокументированные возможности).

                          > не говоря уже о Lisp-макросах

                          А, так дизайнер должен ещё и макросы освоить? Для такой тривиальной задачи как генерация html не имеет никакого смысла привлекать тяжёлую артиллерию. Иначе идёт усложение простейших вещей, которые решаются элементарно за счёт шаблоново.

                          Кроме того, при использовании cl-who происходит смешивание логики приложения с логикой представления и править это потом просто кошмар, особенно учитывая какие простыни кода при этом получаются при генерации реальный веб-страниц.
                            0
                            Нет, не разбивается, ибо cl-who специальным образом перерабатывает дерево с кодом и генерирует на его основании новый код
                            > меня мало волнует то, что там генерируется, меня волнует лишь то, что фрагменты CL-WHO DSL прекрасно разбиваются на функции и прекрасно генерируются макросами, а внутренняя кухня с записью строк в поток вывода меня не волнует, если нет проблем с производительностью (а их почти никогда не бывает в этом месте).

                            А, так дизайнер должен ещё и макросы освоить?
                            > Не должен, мы все должны стремиться к отделению кода от стилей (хотя это не всегда возможно). Дизайнеру дизайнерово — править стили и статические ресурсы.

                            Кроме того, при использовании cl-who происходит смешивание логики приложения с логикой представления
                            > Не происходит, если использовать CL-WHO по назначению, а именно строго разделять бизнес логику (чистый CL) от построения html/xml (вот тут CL-WHO). CL-WHO не претендует на роль языка бизнес-логики — это всего лишь удобный способ записи SGML-выражений, XML-подмножество которого, к сожалению, сейчас правит миром.
                              0
                              Ладно, в мире CL есть такая болезнь, над которой смеются все веб-разработчики: старание генерировать html/xml через s-выражения и cl-who часть этой болезни. Веб-разработка на CL становится значительно проще если от этой болезни избавиться.
                                +1
                                no comments
                                  0
                                  У меня нет какого либо желания переубеждать конкретно вас, в конце концов, это ваш личный выбор. Но в подобных статьях, цель которых, насколько я понимаю, — популяризация, нельзя обходиться только cl-who, ибо это производит очень негативное впечатление на опытных веб-разработчиков.
              0
              Вот бы туда haml.
                0
                "… определяющую простой и удобный DSL для генерации HTML/XML. Как водится, лучше один раз увидеть" ©.

                Господи… Это же надо такое придумать… Нужно точно один раз увидеть, чтоб понять какой он «простой» и «удобный»…

                Уважаемый, Вы будете один писать или у вас уже команда собралась?
                  0
                  Вы наверное не знаете Common Lisp. Всё равно, что я читал бы исходники на Perl. Для знающего человека этот код очень прост, можно сказать прозрачен.
                    –1
                    Я на Lispe делал презентацию в университете. Уж Вы меня извините, но template язык, типа, velocity куда проще, нежели приведённый вами пример. Показываю:

                    Ваш пример:

                      –1
                      показываю:

                      (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»))))))

                      результат:
                        –1
                        "
                        /><tag3 />/>"

                        что именно тут нужно достичь? не проще ли сделать так:

                        где вся структура .html уже заранее прописана и легко читаема каждому?
                          0
                          Госпади, хабр всё захабрил (причём не правильно парсиг ещё и сработал)… Вот пример:

                          <tag1 attr1='$map.key' attr2='$map.value'>
                            0
                            Тому кто ставит минусы, хочется сказать, что нужно приводить конкретные коментарии, а не кидать гранатами из-за окоп. Итак, коментарии…
                              0
                              (минусы ставить Хабраобщество не даёт)

                              Просто хочу уточнить.

                              <tag1 attr1='$map.key' attr2='$map.value'>

                              Выведет то же самое, что и

                              (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»))))))

                              ?

                              У вас половина тегов в каждом примере сожралась. Приведите их в порядок, может и комментарии последуют.
                                0
                                Приведите, пожалуйста, более коректный пример, а не пример притянутый за уши из потолка.

                                пс: почему хабр не может все теги просто заменить на > <?
                                  +1
                                  Я просто ваш пример повторил. Т.е. вы признаёте, что ваш пример притянут за уши с потолка?

                                  Если я не уловил вашу мысль, то вот и ответ на вопрос почему нет комментов. Её никто не уловил.
                                    –1
                                    Пример автора в котором я лично совсем не понимаю смысла:

                                    #set( $attr3 = 1 + 2 ) // улетает в контроллер
                                    <tag1 attr1='1' attr2='2'>
                                        <tag2 attr3='$attr3'></tag2>
                                        #foreach(i in [1..3])
                                            <tag3 #if( $oddp == $i )attr4='odd'#{end} /><tag3 />
                                        #end
                                    </tag1>

                                    1. Что это такое? Это какой-то объект? Может тут темплате вообще не нужен?

                                    2. Вы весь xhtml собираетесь переписать на языке, типа, (:tag2 :attr3 (+ 1 2)))?

                                    3. Укажите, в каких же местах template ваше «чудо» выигрывает, например, в простоте кода.
                                      0
                                      1. Да не объект это, а просто элементарный пример без конкретики, CL-WHO неплевать на имена тегов, атрибутов, т.е. он не знает о HTML.

                                      2. XHTML никуда не делся, вместо tag1 пишите html, вместо tag2 — head…

                                      3. Хотя бы в том, что не надо писать закрывающих тегов.
                                        –1
                                        1, 2, 3. Это не аргументы, такой код не просматриваемый (1) уже на 50 строчках и его тяжело поддерживать.

                                        (2) Им не сможет заниматься дизайнер (если речь идёт о веб-е) или тяжело (не реально) найти человека/ОВ.
                                          +1
                                          Не могу согласиться ни с (1) ни с (2).
                                            –1
                                            То есть, Вы утверждаете, что примером приведённым Вами в главе «генерация HTML» может заниматься любой дизайнер и делает это с лёгкостью? Фуф… ню тогда успехов Вам, что ещё можно тут сказать.
                                              +2
                                              Всегда радовали люди свято верящие в необучаемость коллег.

                                              Изучение cl-who ничем не сложнее, чем изучение очередного модного шаблонного движка, вроде того, на котором приведены ваши примеры.
                                                –1
                                                Дизайнер не сможет править cl-who, ибо там код на CL. Что, дизайнер должен изучить CL? И при этом его надо пускать в код с логикой? Лучше дизайнеров держать подальше от таких вещей. А сами шаблоны не должны быть частью кода. Всем будет от этого лучше, и прежде всего самим разработчикам.
                                                  –1
                                                  Если человек умеет хорошо верстать/ рисовать, но не умеет программировать/ никогда не программировал/ программировал, но не на этом языке/ недостаточно опыта для Вашего проекта/ тут допишите сами, то Вы не научите его этому, хоть лоб разбейте — для этого нужно много времени. В условиях бизнеса времени никогда не достаточно. Вы богатый человек/ у Вас богатая фирма которая занимается благотворительностью?
                      0
                      У меня (я hutchentoot не ставил, пушо не знаю, как правильно) (asdf:oos 'asdf:load-op :hunchentoot) закономерно ругается «component "hutchentoot" not found».
                      Так вот, реквестирую быстрый ликбез. Есть ли аналог питоновского easy_install под CL (SBCL если точнее)? То есть мне нужно, чтобы оно не только умело ставить пакеты (это ASDF умеет, да), но и скачивать. Если их несколько — то какой лучше с точки зрения «чайника»? Спасибо.
                        0
                        А ещё можно попробовать новую систему: www.quicklisp.org/beta/

                        Только предупреждаю, «beta» там не для галочки написано. Иногда падает.
                          0
                          Но лучший вариант — ставьте менеджером пакетов вашего любимого дистрибутива, если hunchentoot там есть.
                            0
                            искал — нету
                          0
                          ASDF-INSTALL
                            0
                            Там всего 4 или 5 библиотек, от которых он зависит (ссылки на сайте). Можете просто их по очереди скачать и поставить.
                          0
                          Понравилась ваша постановка вопроса: «Подскажите, пожалуйста, как выяснить, кто здесь дебил.»))

                          Надо не hutchentoot, а hunchentoot
                            +1
                            вот тут-то меня и подвела привычка набирать незнакомые команды руками
                            facepalm.jpg
                            0
                            Продолжения не будет?
                              0
                              Планировал написать ещё одну часть (про работу с PostgreSQL), но вижу что не особо востребованно…
                                0
                                Впечатлило. Просто тех кто впечатлило в комментариях нет — они пробуют. Пиши про постгресс, ждем
                                  0
                                  Я бы почитал. Интересно же.
                                  0
                                  Ваша статья ориентирована на начинающих, с этим есть одна проблема. Вы начинаете с этого:

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

                                  и продолжаете этим:

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

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

                                  Чтобы оно вместе работало, надо использовать не acceptor, а easy-acceptor:

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

                                  иначе простейший обработчик из документации не работает.
                                    0
                                    Весь код, который я приводил, был мною тестирован. Возможно, он не очень актуален для текущей версии Hunchentoot.
                                      0
                                      Скажите, вы чем пользуетесь для демонизации и использовании 80 порта для hunchentoot?
                                  0
                                  Весь код, который я приводил, был мною тестирован. Возможно, он не очень актуален для текущей версии Hunchentoot.

                                  Only users with full accounts can post comments. Log in, please.