Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

26. Практика. Web-программирование с помощью AllegroServe

В этой главе вы ознакомитесь с одним из способов разработки Web-приложений на Common Lisp: используя AllegroServe – Web-сервер с открытым исходным кодом. Это не означает, что вы найдете здесь исчерпывающее введение в AllegroServe. И я определенно не собираюсь описывать ничего более чем небольшую часть огромной темы Web-программирования. Моей целью является описание достаточного количества базовых вещей по части использования AllegroServe, которые позволят нам в главе 29 разработать приложение для просмотра библиотеки MP3-файлов и проигрывания их на MP3-клиенте. Кроме того, данная глава может служить кратким введением в Web-программирование для новичков.

30-секундное введение в Web-программирование на стороне сервера

Хотя в настоящее время Web-программирование обычно означает использование одного из доступных программных каркасов (frameworks) и различных протоколов, основы Web-программирования не особо изменились с момента их появления в начале 1990-х. Для простых приложений, таких как мы напишем в главе 29, вам необходимо понять только несколько основных концепций, так что в этом разделе я сделаю их быстрый обзор. Опытные Web-программисты могут лишь просмотреть, а то и вовсе пропустить этот раздел.1)

Для начала вам необходимо понимание ролей Web-браузера и Web-сервера в Web-программировании. Хотя современные браузеры поставляются с кучей свистелок и дуделок (FIXME bells and whistles), основной функциональностью Web-браузера является запрос Web-страниц с Web-сервера и их отображение. Обычно эти страницы пишутся на Hypertext Markup Language (HTML, язык разметки гипертекста), который указывает браузеру как отображать страницу, включая информацию о том, где вставить изображения и ссылки на другие страницы. HTML состоит из текста, размеченного с помощью тегов, которые структурируют текст, а эту структуру браузер использует при отображении страницы. Например, простой HTML-документ выглядит вот так:

<html>
 <head>
 <title>Hello</title>
 </head>
 <body>
 <p>Hello, world!</p>
 <p>This is a picture: <img src="some-image.gif"></p>
 <p>This is a <a href="another-page.html">link</a> to another page.</p>
 </body>
</html>

Рисунок 26-1 показывает как браузер отображает эту страницу.

Рисунок 26-1. Пример Web-страницы

Браузер и сервер общаются между собой используя протокол, называемый Hypertext Transfer Protocol (HTTP, протокол передачи гипертекста). Хотя вам не нужно беспокоиться относительно деталей протокола, полезным будет понимание того, что он полностью состоит из последовательности запросов, инициированных браузером, и ответов, сгенерированных сервером. Таким образом, браузер подключается к Web-серверу и посылает запрос, который включает в себя, как минимум, адрес желаемого ресурса (URL) и версию протокола HTTP, используемую браузером. Браузер также может включать в запрос дополнительные данные; таким образом браузер отправляет HTML-формы на сервер.

Для ответа на запрос сервер отправляет ответ, состоящий из наборов заголовков и тела ответа. Заголовки содержат информацию о теле, такую как тип его данных (например, HTML, обычный текст или изображение), а тело ответа содержит сами данные, которые затем отображаются браузером. Сервер также может отправлять ответ-ошибку, который сообщит браузеру о том, что его запрос не может быть обработан по некоторой причине.

И это почти все. После того, как браузер получил завершенный ответ от сервера, между сервером и браузером не происходит никакого общения до тех пор, пока браузер не решит запросить страницу у сервера в следующий раз2). Это основное ограничение Web-программирования – для кода, выполняемого на сервере, не существует способа воздействовать на то, что пользователь увидит в браузере, до тех пор, пока браузер не сделает новый запрос к серверу3).

Некоторые Web-страницы, называемые статическими (static) страницами, являются просто файлами с разметкой на языке HTML, хранимые на Web-сервере и считываемые, когда приходит соответствующий запрос. Динамические (dynamic) страницы, с другой стороны, состоят из HTML, генерируемого при каждом запросе страницы браузером. Например, динамическая страница может быть сгенерирована путем осуществления запроса к базе данных, а затем конструирования HTML для представления результатов этого запроса4).

При генерировании ответа на запрос код, выполняемый на сервере, получает четыре основных части информации, на основании которой он работает. Первой является запрошенный адрес (URL). Но обычно URL используется самим Web-сервером для определения того, какой код ответственен за генерирование ответа. Затем, если URL содержит знак вопроса, то все, что следует за ним, рассматривается как строка запроса (query string), которая обычно игнорируется самим Web-сервером и передается им коду, который будет генерировать ответ. В большинстве случаев строка запроса содержит набор пар имя-значение. Запрос от браузера может также содержать POST-данные, которые также обычно состоят из пар имя-значение. POST-данные обычно используются для передачи содержимого форм HTML. Пары имя-значение, переданные либо через строку запроса, либо через дополнительные данные, обычно называют параметрами запроса (query parameters).

В заключение, для того, чтобы связать между собой последовательность отдельных запросов от одного и того же браузера, код, выполняющийся на сервере, может установить cookie путем отправки специального заголовка в своем ответе браузеру; этот заголовок будет содержать некий набор данных. После установки cookie браузер будет слать его при каждом запросе, отправляемом этому сервер. Браузер не заботится о данных, хранимых в cookie, — он просто отправляет их обратно на сервер, и код, работающий там, может обрабатывать их так, как захочет.

Это все базовые элементы, на основании которых основано 99 процентов кода, выполняемого на Web-сервере. Браузер отправляет запрос, сервер находит код, который будет обрабатывать запрос, и запускает его, а код использует параметры запроса и cookies для определения того, что именно нужно сделать.

AllegroServe

Вы можете отдавать Web-страницы с помощью Common Lisp разными способами; существует по крайней мере три реализации Web-серверов с открытым исходным кодом, написанных на Common Lisp, а также подключаемые модули, такие как mod_lisp5) и Lisplets6), которые позволяют Web-серверу Apache или любому контейнеру Java Servlet делигировать обработку запросов серверу Lisp, работающему в отдельном процессе.

В этой главе мы будем использовать Web-сервер с открытым исходным кодом AllegroServe, изначально написанный John Foderaro из Franz Inc. AllegroServe включен в версию Allegro, доступную с сайта Franz для использования с этой книгой. Если вы не используете Allegro, то вы можете использовать PortableAllegroServe, ответвление (fork) кода AllegroServe, которое включает в себя уровень (layer) совместимости с Allegro, что позволяет PortableAllegroServe работать почти на всех реализациях Common Lisp. Код, который мы напишем в этой главе и в главе 29, должен работать как на стандартном AllegroServe, так и на PortableAllegroServe.

AllegroServe реализует модель программирования сходную по духу с Java Servlets – каждый раз, когда браузер запрашивает страницу, AllegroServe разбирает запрос и ищет объект, называемый сущностью (entity), который будет обрабатывать запрос. Некоторые классы сущностей, предоставляемые как часть AllegroServe, умеют обрабатывать статическое содержимое: либо отдельные файлы, либо содержимое каталога. Другие, которые я буду обсуждать большую часть главы, запускают произвольный код на Lisp для генерирования ответа7).

Но перед тем как начать, вам необходимо знать, как запускать AllegroServe и как настроить его для обработки файлов. Первым шагом является загрузка кода AllegroServe в ваш образ Lisp. В Allegro вы можете просто набрать (require :aserve). В других реализациях Lisp (а также в Allegro), вы можете загрузить PortableAllegroServe путем загрузки файла INSTALL.lisp, находящегося в корне каталога portableaserve. Загрузка AllegroServe создаст три новых пакета: NET.ASERVE, NET.HTML.GENERATOR и NET.ASERVE.CLIENT8).

После загрузки сервера, вы можете запустить его с помощью функции start из пакета NET.ASERVE. Чтобы иметь простой доступ к символам, экспортированным из пакета NET.ASERVE, из пакета COM.GIGAMONKEYS.HTML (который мы скоро обсудим), а также остальных частей Common Lisp, нам нужно создать новый пакет:

CL-USER> (defpackage :com.gigamonkeys.web
            (:use :cl :net.aserve :com.gigamonkeys.html)
)

#<The COM.GIGAMONKEYS.WEB package>

Теперь переключитесь на этот пакет с помощью следующего выражения IN-PACKAGE:

CL-USER> (in-package :com.gigamonkeys.web)
#<The COM.GIGAMONKEYS.WEB package>
WEB>

Теперь мы можем использовать имена, экспортированные из NET.ASERVE, без указания квалификатора. Функция start запускает сервер. Она принимает множество именованных параметров, но единственный нужный нам — :port, который указывает номер порта, на котором сервер будет принимать запросы. Возможно вам понадобится использовать большой номера порта, такой как 2001, вместо порта по умолчанию для HTTP-серверов, 80, поскольку в Unix-подобных операционных системах только администратор (root) может использовать порты с номером меньше 1024. Для запуска AllegroServe на порту 80 под Unix вам необходимо запустить Lisp с правами администратора (root), а затем использовать параметры :setuid и :setgid чтобы заставить start переключить пользовательский контекст после открытия этого порта. Вы можете запустить сервер на порту 2001 с помощью следующей команды:

WEB> (start :port 2001)
#<WSERVER port 2001 @ #x72511c72>

Теперь сервер выполняется в вашей среде Lisp. Возможно, что при попытке запуска сервера вы получите ошибку вида "port already in use". Это означает, что данный порт уже используется каким-то сервером на вашей машине. В таком случае самым простым решением будет использование другого порта, передав другой аргумент функции start, а затем использование нового значение во всех адресах, встречаемых на протяжении данной главы.

Вы можете продолжить взаимодействие с Lisp с помощью REPL, поскольку AllegroServe запускает отдельные нити для обработки запросов браузеров. Это означает, среди прочего, что вы можете использовать REPL для того, чтобы заглянуть во "внутренности" сервера во время его работы, что делает тестирование и отладку намного более легкой, по сравнению с тем, когда сервер представляет собой "черный ящик".

Предполагая, что вы запустили Lisp на той же машине, где находится и ваш браузер, вы можете проверить, что сервер запущен путем перехода в браузере по следующему адресу: http://localhost:2001/. В данный момент вы получите в браузере сообщение об ошибке page-not-found (страница не найдена), поскольку вы пока ничего не опубликовали. Но сообщение об ошибке придет от AllegroServe; это видно по строке внизу страницы. С другой стороны, если браузер отображает ошибку, сообщающую что-то вроде "The connection was refused when attempting to contact localhost:2001", то это означает, что сервер не запущен, или вы запустили его на порту с номером, отличным от 2001.

Теперь мы можем публиковать файлы. Предположим, что у нас есть файл hello.html в каталоге /tmp/html со следующим содержимым:

<html>
 <head>
 <title>Hello</title>
 </head>
 <body>
 <p>Hello, world!</p>
 </body>
</html>

Вы можете опубликовать его с помощью функции publish-file.

WEB> (publish-file :path "/hello.html" :file "/tmp/html/hello.html")
#<NET.ASERVE::FILE-ENTITY @ #x725eddea>

Аргумент :path задает путь, который будет использоваться в URL, запрашиваемом браузером, а аргумент :file является именем файла на файловой системе. После вычисления выражения publish-file вы можете задать в браузере адрес http://localhost:2001/hello.html, и он должен отобразить что-то наподобие того, что изображено на рисунке 26-2.

Рисунок 26-2. http://localhost:2001/hello.html

Вы также можете опубликовать целый каталог с помощью функции publish-directory. Но сначала давайте избавимся от уже опубликованной сущности с помощью следующего вызова publish-file:

WEB> (publish-file :path "/hello.html" :remove t)
NIL

Теперь вы можете опубликовать каталог /tmp/html/ целиком (включая его подкаталоги) с помощью функции publish-directory.

WEB> (publish-directory :prefix "/" :destination "/tmp/html/")
#<NET.ASERVE::DIRECTORY-ENTITY @ #x72625aa2>

В данном случае, аргумент :prefix указывает начало пути адресов URL, которые будут обрабатываться данной сущностью. Так что, если сервер получает запрос http://localhost:2001/foo/bar.html, то путь будет /foo/bar.html, который начинается с /. Затем этот путь транслируется в имя файла путем замены префикса (/) на аргумент :destination (/tmp/html/). Так что URL http://localhost:2001/hello.html будет преобразован в запрос файла /tmp/html/hello.html.

Генерирование динамического содержимого с помощью AllegroServe

Публикация сущностей, генерирующих динамическое содержимое, практически также проста, как и публикация статического содержимого. Функции publish и publish-prefix являются "динамическими" аналогами publish-file и publish-directory. Основная их идея заключается в том, что вы публикуете функцию, которая будет вызываться для генерирования ответа на запрос либо к определенному адресу (URL), либо к любому адресу с заданным префиксом. Эта функция будет вызвана с двумя аргументами: объектом, представляющим запрос, и опубликованной сущностью. Большую часть времени нам не нужно будет ничего делать с опубликованной сущностью за исключением ее передачи набору макросов, которые вскоре будут описаны. С другой стороны, мы будем использовать объект запроса для получения информации, переданной браузером: параметров запроса, переданных в строке URL, или данных, посланных формами HTML.

В качестве простого примера использования функции для генерирования динамического содержимого, давайте напишем функцию, которая будет генерировать страницу с различными случайными числами при каждом обращении к ней.

(defun random-number (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (format
       (request-reply-stream request)
       "<html>~@
        <head><title>Random</title></head>~@
        <body>~@
        <p>Random number: ~d</p>~@
        </body>~@
        </html>~@
       "

       (random 1000)
)
)
)
)

Макросы with-http-response и with-http-body являются частью AllegroServe. Первый из макросов начинает процесс генерирования ответа HTTP и может быть использован, также как в нашем примере, для указания таких вещей, как тип возвращаемых данных. Он также обрабатывает различные требования, указанные в стандарте HTTP, такие как обработка запросов If-Modified-Since. Макрос же with-http-body фактически отправляет заголовки HTTP ответа, а затем вычисляет свое тело, которое должно содержать код, генерирующий содержимое ответа. Внутри with-http-response, но перед with-http-body, вы можете добавить или изменить значение заголовков HTTP, которые будут отправлены в ответе. Функция request-reply-stream также является частью AllegroServe и возвращает поток, в который вы должны записывать данные предназначенные для отправления браузеру.

Как видно из этой функции, вы можете просто использовать FORMAT для вывода HTML в поток, возвращенный вызовом request-reply-stream. В следующем разделе я покажу вам более удобные способы программного генерирования HTML9).

Теперь мы готовы к публикации данной функции.

WEB> (publish :path "/random-number" :function 'random-number)
#<COMPUTED-ENTITY @ #x7262bab2>

Также, как и в функции publish-file, аргумент :path указывает "путевую часть" (path part) адреса, указание которой будет приводить к вызову данной функции. Аргумент :function указывает либо имя функции, либо сам функциональный объект. Использование имени функции, как показано в этом примере, позволяет вам в дальнейшем переопределить функцию не выполняя заново процесс публикации для того, чтобы AllegroServe стал использовать новое определение. После вычисления данного вызова вы можете указать в браузере адрес http://localhost:2001/random-number для получения страницы со случайным числом, как это показано на рисунке 26-3.

Рисунок 26-3. http://localhost:2001/random-number

Генерирование HTML

Хотя использование FORMAT для генерирования HTML вполне приемлемо для генерирования простых страниц, наподобие приведенной выше, по мере того, как вы начнете создавать более сложные, было бы лучше иметь более краткий способ генерирования HTML. Для генерирования HTML из представления в виде s-выражений существует несколько библиотек, включая htmlgen, которая поставляется вместе с AllegroServe. В этой главе мы будем использовать библиотеку FOO,10), которая использует примерно ту же модель, что и htmlgen, и чью реализацию мы рассмотрим более подробно в главах 30 и 31. Сейчас, однако, нам нужно лишь знать как использовать FOO.

Генерирование HTML из Lisp вполне естественна, так как s-выражения и HTML по своему существу изоморфны. Мы можем представить HTML-элементы с помощью s-выражений, рассматривая каждый элемент HTML как список, "помеченный" соответствующим первым элементом, таким как ключевым символом с таким же именем, что имеет тег HTML. Таким образом, HTML <p>foo</p> представляется s-выражением (:p "foo"). Так как элементы HTML также, как и списки в s-выражениях, могут быть вложенными, эта схема распространяется и на более сложный HTML. Для примера вот такой HTML:

<html>
 <head>
 <title>Hello</title>
 </head>
 <body>
 <p>Hello, world!</p>
 </body>
</html>

может быть представлен в виде следующего s-выражения:

(:html
  (:head (:title "Hello"))
  (:body (:p "Hello, world!"))
)

Элементы HTML с атрибутами несколько усложняют дело, но не создают непреодолимых проблем. FOO предоставляет два способа включения атрибут в тег. Одним из них является добавление после первого элемента списка пар ключ-значение. Первый же элемент, который следует за парами ключ-значение, и который сам не является ключевым символом, обозначает начало содержимого элемента. Таким образом, такой HTML:

<a href="foo.html">This is a link</a>

будет представляться следующим s-выражением:

(:a :href "foo.html" "This is a link")

Другой предоставляемый FOO синтаксис заключается в группировке имени тега и его атрибутов в отдельный список следующим образом:

((:a :href "foo.html") "This is link.")

FOO может использовать представление HTML в виде s-выражений двумя способами. Функция emit-html получает представляющее HTML s-выражение и выводит соответствующий HTML.

WEB> (emit-html '(:html (:head (:title "Hello")) (:body (:p "Hello, world!"))))
<html>
 <head>
   <title>Hello</title>
 </head>
 <body>
   <p>Hello, world!</p>
 </body>
</html>
T

Однако, emit-html не всегда является наиболее эффективным способом генерирования HTML, так как ее аргументом должно быть законченное s-выражениее, предоставляющее HTML, который нужно сгенерировать. Хотя генерировать такое представление легко, действовать так не всегда эффективно. Например, представим, что нам нужно сгенерировать страницу HTML, содержащую список из 10,000 случайных чисел. Мы можем построить соответствующее s-выражение используя шаблон квазицитирования, а затем передать его в функцию emit-html:

(emit-html
  `(:html
     (:head
       (:title "Random numbers")
)

     (:body
       (:h1 "Random numbers")
       (:p ,@(loop repeat 10000 collect (random 1000) collect " "))
)
)
)

Однако этот код должен построить дерево, содержащее 10000-элементный список, перед тем как он сможет даже начать генерировать HTML, и все это s-выражение станет мусором как только HTML будет сгенерирован. Для избежания такой неэффективности FOO также предоставляет макрос html, позволяющий вам встраивать произвольный код Lisp в середину s-выражения, по которому будет генерироваться HTML.

Литеральные значения, такие как строки и числа, входа html будут подставлены в генерируемый HTML. Также, символы интерпретируются как ссылки на переменные, которые они именуют, и сгенерированный код будет брать их значения во время выполнения. Таким образом оба следующих выражения:

(html (:p "foo"))

(let ((x "foo")) (html (:p x)))

сгенерируют следующее:

<p>foo</p>

Формы списков, которые не начинаются с ключевых символов, рассматриваются как код и встраиваются в генерируемый код. Любые значения, возращаемые встроенным кодом, будут проигнорированы, но такой код сам может генерировать HTML путем вызова html. Например, для генерирования содержимого списка в виде HTML, мы можем написать следующее:

(html (:ul (dolist (item (list 1 2 3)) (html (:li item)))))

что сгенерирует следующий HTML:

<ul>
 <li>1</li>
 <li>2</li>
 <li>3</li>
</ul>

Если вы захотите выдать значение формы, вы должны обернуть его в псевдотег :print. Таким образом, выражение:

(html (:p (+ 1 2)))

сгенерирует такой HTML после вычисления и отбрасывания значения 3:

<p></p>

Для выдачи 3 вы должны написать такой код:

(html (:p (:print (+ 1 2))))

Или же вы можете вычислить значение и сохранить его в переменной вне вызова html следующим образом:

(let ((x (+ 1 2))) (html (:p x)))

Таким образом вы можете использовать макрос html для генерирования списка случайных чисел следующим образом:

(html
  (:html
    (:head
      (:title "Random numbers")
)

    (:body
      (:h1 "Random numbers")
      (:p (loop repeat 10 do (html (:print (random 1000)) " ")))
)
)
)

Версия, использующая макрос html, будет эффективнее, чем использующая emit-html. И не только из-за того, что теперь не нужно генерировать s-выражение, представляющее страницу целиком, но и из-за того, что большая часть работы по интерпретации s-выражения, которую emit-html осуществляет во время выполнения, будет сделана однократно, во время раскрытия макроса, а не каждый раз при выполнении кода.

Вы можете управлять тем, куда будет отправлен вывод html и emit-html, с помощью макроса with-html-output, который также является частью библиотеки FOO. Вы можете использовать макросы with-html-output и html для переписывания random-number следущим образом:

(defun random-number (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (with-html-output ((request-reply-stream request))
        (html
          (:html
            (:head (:title "Random"))
            (:body
              (:p "Random number: " (:print (random 1000)))
)
)
)
)
)
)
)

Макросы HTML

Другой возможностью FOO является то, что он позволяет вам определять "макросы" HTML, которые могут преобразовывать произвольные формы в представляющие HTML s-выражения, которые понимает макрос html. Например, предположим, что вы заметили, что часто создаете страницы следующего вида:

(:html
  (:head (:title "Some title"))
  (:body
    (:h1 "Some title")
    ... stuff ...
)
)

Вы можете определить макрос HTML, представляющий этот образец, следующим образом:

(define-html-macro :standard-page ((&key title) &body body)
  `(:html
     (:head (:title ,title))
     (:body
      (:h1 ,title)
      ,@body
)
)
)

Теперь вы можете использовать "тег" :standard-page в ваших представляющих HTML s-выражениях, и он будет раскрыт перед интерпретацией или компиляцией этих выражений. Например, следующий код:

(html (:standard-page (:title "Hello") (:p "Hello, world.")))

сгенерирует такой HTML:

<html>
 <head>
   <title>Hello</title>
 </head>
 <body>
   <h1>Hello</h1>
   <p>Hello, world.</p>
 </body>
</html>

Параметры запроса

Конечно, генерирование HTML является только половиной темы web-программирования. Еще одной вещью, которую вы должны уметь делать, является получение ввода от пользователя. Как я рассказывал в разделе "30-секундное введение в web-программирование на стороне сервера", когда браузер запрашивает страницу у web-сервера, он может послать параметры запроса в адресе URL или POST-данные, и оба этих источника рассматриваются как ввод для кода на стороне сервера.

AllegroServe, как и большинство других каркасов web-программирования, берет на себя заботу о разборе обоих этих источников ввода для вас. В то время когда ваша опубликованная функция вызывается, все пары ключ-значение из строки запроса и/или POST-данных уже декодированы и помещены в ассоциативный список (alist), который вы можете получить из объекта запроса с помощью функции request-query. Следующая функция возвращает страницу, отображающую все полученные ею параметры запроса:

(defun show-query-params (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (with-html-output ((request-reply-stream request))
        (html
          (:standard-page
           (:title "Query Parameters")
           (if (request-query request)
             (html
               (:table :border 1
                       (loop for (k . v) in (request-query request)
                          do (html (:tr (:td k) (:td v)))
)
)
)

             (html (:p "No query parameters."))
)
)
)
)
)
)
)


(publish :path "/show-query-params" :function 'show-query-params)

Если вы укажете своему браузеру URL со строкой запроса подобной такой:

http://localhost:2001/show-query-params?foo=bar&baz=10

вы получите страницу, подобную показанной на рисунке 26-4.

Рисунок 26-4. http://localhost:2001/show-query-params?foo=bar&baz=10

Для генерирования POST-данных нам нужна форма HTML. Следующая функция генерирует простую форму, которая посылает свои данные show-query-params:

(defun simple-form (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (let ((*html-output* (request-reply-stream request)))
        (html
          (:html
            (:head (:title "Simple Form"))
            (:body
             (:form :method "POST" :action "/show-query-params"
               (:table
                (:tr (:td "Foo")
                     (:td (:input :name "foo" :size 20))
)

                (:tr (:td "Password")
                     (:td (:input :name "password" :type "password" :size 20))
)
)

               (:p (:input :name "submit" :type "submit" :value "Okay")
                   (:input ::type "reset" :value "Reset")
)
)
)
)
)
)
)
)
)


(publish :path "/simple-form" :function 'simple-form)

Перейдите в вашем браузере по адресу http://localhost:2001/simple-form; вы должны увидеть страницу, подобную изображенной на рисунке 26-5.

Если вы заполните форму значениями "abc" и "def", щелкните на кнопку Okay, то получите страницу, сходную с изображенной на рисунке 26-6.

Рисунок 26-5. http://localhost:2001/simple-form

Рисунок 26-6. Результат посылки простой формы

Однако, чаще всего вам не нужно проходить по всем параметрам запроса: обычно вам нужно просто получить определенный параметр. Например, вы можете захотеть модифицировать random-number так, чтобы предельное значение, передаваемое в функцию RANDOM, предоставлялось как параметр запроса. В таком случае используется функция request-query-value, получающая объект запроса и имя параметра, значение которого вы хотите получить. Она возвращает либо значение параметра в виде строки, либо NIL, если такой параметр не предоставлен. "Параметризованная" версия random-number может выглядеть следующим образом:

(defun random-number (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (let* ((*html-output* (request-reply-stream request))
             (limit-string (or (request-query-value "limit" request) ""))
             (limit (or (parse-integer limit-string :junk-allowed t) 1000))
)

        (html
          (:html
            (:head (:title "Random"))
            (:body
              (:p "Random number: " (:print (random limit)))
)
)
)
)
)
)
)

Так как request-query-value может возвращать как NIL, так и пустую строку, мы должны обрабатывать оба этих случая при разборе параметра и его преобразования в число, которое будет передано RANDOM. Мы можем обработать значение NIL при связывании переменной limit-string, связывая ее с "" если параметра "limit" нет. Затем мы можем использовать аргумент функции PARSE-INTEGER :junk-allowed для гарантии того, что она вернет либо NIL (если не сможет разобрать целое число из переданной строки), либо целое число. В разделе "Небольшой каркас приложений" мы разработаем несколько макросов для облегчения работы по получению параметров запроса и преобразования их в различные типы.

Cookies

В AllegroServe вы можете послать заголовок Set-Cookie, который укажет браузеру сохранить cookie и посылать его со всеми последующими запросами, путем вызова функции set-cookie-header внутри тела with-http-response, но перед вызовом with-http-body. Первый аргумент этой функции должен быть объектом запроса, а остальные аргументы — ключевые аргументы, используемые для установки различных свойств cookie. Обязательными являются лишь два аргумента: :name и :value, оба из которых являются строками. Остальные возможные аргументы, влияющие на посылаемый браузеру cookie: :expires, :path, :domain и :secure.

Из этих аргументов нам следует обратить внимание лишь на :expires. Он управляет тем, как долго браузер должен сохранять cookie. Если :expires равен NIL (по умолчанию), браузер сохранит cookie только до завершения своей работы. Другие возможные значения: :never, что означает, что cookie должен сохраняться навсегда, или всемирное (universal) время, как возвращается GET-UNIVERSAL-TIME или ENCODE-UNIVERSAL-TIME. Значение :expires равное нулю указывает клиенту немедленно удалить существующие cookie11).

После того как вы установили cookie, вы можете использовать функцию get-cookie-values для получения ассоциативного списка (alist), содержащего по паре имя-значение на каждый cookie, посланный браузером. Из этого списка вы можете получить значения отдельных cookie с помощью ASSOC и CDR.

Следующая функция отображает имена и значения всех cookie, посланных браузером:

(defun show-cookies (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (with-html-output ((request-reply-stream request))
        (html
          (:standard-page
           (:title "Cookies")
           (if (null (get-cookie-values request))
             (html (:p "No cookies."))
             (html
               (:table
                 (loop for (key . value) in (get-cookie-values request)
                    do (html (:tr (:td key) (:td value)))
)
)
)
)
)
)
)
)
)
)


(publish :path "/show-cookies" :function 'show-cookies)

При первой загрузке страницы http://localhost:2001/show-cookies она должна отобразить сообщение "No cookies" как показано на рисунке 26-7, так как вы еще ничего не установили.

Рисунок 26-7. http://localhost:2001/show-cookies без установленных cookie

Для установки cookie нам понадобится другая функция, такая как эта:

(defun set-cookie (request entity)
  (with-http-response (request entity :content-type "text/html")
    (set-cookie-header request :name "MyCookie" :value "A cookie value")
    (with-http-body (request entity)
      (with-html-output ((request-reply-stream request))
        (html
          (:standard-page
           (:title "Set Cookie")
           (:p "Cookie set.")
           (:p (:a :href "/show-cookies" "Look at cookie jar."))
)
)
)
)
)
)


(publish :path "/set-cookie" :function 'set-cookie)

Если вы откроете в браузере http://localhost:2001/set-cookie, он должен отобразить страницу, показанную на рисунке 26-8. Вдобавок сервер пошлет заголовок Set-Cookie с cookie по имени "MyCookie" со значением "A cookie value". Если вы нажмете на ссылку Look at cookie jar, вы попадете на страницу /show-cookies, где увидите новый cookie, как показано на рисунке 26-9. Так как вы не задали аргумент :expires, браузер продолжит посылать cookie при каждом запросе пока вы не выйдете из него.

Рисунок 26-8. http://localhost:2001/set-cookie

Рисунок 26-9. http://localhost:2001/show-cookies после установки cookie

Небольшой каркас приложений

Хотя AllegroServe предоставляет достаточно прямой доступ ко всем базовым возможностям, необходимым для написания кода, выполняющегося на стороне сервера (доступ к параметрам запроса как из строки запроса, таки и из POST-данных; возможность установки cookie и получения их значений; и, конечно же, возможность генерирования ответа для посылки браузеру), приходится писать некоторое количество раздражающе повторяющегося кода.

Например, каждая генерирующая HTML функция, которую вы будете писать, будет получать в качестве аргументов запрос и сущность, а затем будет содержать вызовы with-http-response, with-http-body и, если вы будете использовать FOO для генерирования HTML, with-html-output. Далее, функции, которым нужно получать параметры запроса, будут содержать множество вызовов request-query-value и еще больше кода для преобразования получаемых строк к нужным типам. И наконец вам нужно не забыть опубликовать функции.

Для уменьшения повторяющегося кода, который вам нужно писать, мы можем раализовать небольшой каркас поверх AllegroServe в целях облегчения определения функций, обрабатывающих запросы по определенным URL.

Базовым приближением будет определение макроса define-url-function, которыймы будем использовать для определения функций, которые будут автоматически публиковаться с помощью publish. Этот макрос будет раскрываться в DEFUN, содержащий соответствующий шаблонный код, а также в код публикации функции под URL с таким же, как у функции, именем. Он также возьмет на себя заботу по генерации кода извлечения значений параметров запроса и cookies и связывания их с переменными, объявленными в списке параметров функции. Таким образом, базовой формой определения define-url-function будет такая:

(define-url-function name (request query-parameter*)
  body
)

где body будет кодом, выдающим код HTML страницы. Он будет обернут в вызов макроса html из FOO, и поэтому для простых страниц может не содержать ничего, кроме представляющего HTML s-выражения.

Внутри тела переменные параметров запроса будут связаны со значениями параметров запроса с такими же именами или значениями cookie. В простейшем случае значением параметра запроса будет строка, полученная из параметра запроса или поля POST-данных с таким же именем. Если же параметр запроса задается списком, вы также можете задать автоматическое преобразование типа, значение по умолчанию, и то, нужно ли получать значение параметра из и сохранять его в cookie. Полный синтаксис для параметра запроса выглядит так:

name | (name type [default-value] [stickiness])

type должен быть именем, распознаваемым define-url-function. Мы скоро обсудим как определять новые типы. default-value должно быть значением данного типа. И, наконец, stickness, если предоставлен, указывает, что значение параметра должно быть взято из cookie с соответствующим именем в случае, если параметр запроса не предоставлен, а также что в ответе должен быть послан заголовок Set-Cookie, который сохранит значение cookie с этим именем. Таким образом, сохраняемый параметр (sticky parameter), после явного предоставления значения в параметре запроса, сохранит это значение при последующих запросах страницы даже если параметр запроса не предоставляется.

Имя используемого cookie зависит от значения stickness: при значении :global cookie будет иметь то же имя, что и параметр. Таким образом, различные функции, использующие глобальные сохраняемые параметры с одинаковым именем, будут разделять значение. Если stickness равно :package, то имя cookie будет сконструировано из имен параметра и пакета, в котором находится имя функции; это позволит функциям одного пакета разделять значения не заботясь о возможных конфликтах с парметрами функций других пакетов. И, наконец, параметр со значением stickness равным :local будет использовать имя cookie, составленное из имен параметра, пакета, в котором находится имя функции, и самого имени функции, что делает его уникальным для этой функции.

Например, вы можете использовать define-url-function для замены предыдущего 11-строчного определения random-page 5-строчной версией:

(define-url-function random-number (request (limit integer 1000))
  (:html
    (:head (:title "Random"))
    (:body
      (:p "Random number: " (:print (random limit)))
)
)
)

Если вы хотите, чтобы аргумент limit сохранялся, вы должны изменить объявление limit следующим образом: (limit integer 1000 :local).

Реализация

Я разъясню реализацию define-url-function "сверху вниз". Сам макрос выглядит следующим образом:

(defmacro define-url-function (name (request &rest params) &body body)
  (with-gensyms (entity)
    (let ((params (mapcar #'normalize-param params)))
      `(progn
         (defun ,name (,request ,entity)
           (with-http-response (,request ,entity :content-type "text/html")
             (let* (,@(param-bindings name request params))
               ,@(set-cookies-code name request params)
               (with-http-body (,request ,entity)
                 (with-html-output ((request-reply-stream ,request))
                   (html ,@body)
)
)
)
)
)

         (publish :path ,(format nil "/~(~a~)" name) :function ',name)
)
)
)
)

Давайте рассмотрим ее по шагам, начиная с первых строк.

(defmacro define-url-function (name (request &rest params) &body body)
  (with-gensyms (entity)
    (let ((params (mapcar #'normalize-param params)))

Up to here you're just getting ready to generate code. Мы генерируем с помощью GENSYM символ для дальнейшего использования в качестве имени параметра сущности в DEFUN. Затем мы нормализуем параметры, преобразуя обычные символы в списочную форму с помощью следующей функции:

(defun normalize-param (param)
  (etypecase param
    (list param)
    (symbol `(,param string nil nil))
)
)

Другими словами, объявления параметра как просто символа — это тоже самое, что и объявление несохраняемого строкового параметра без значения по умолчанию.

Затем идет PROGN. Мы должны раскрывать макрос в PROGN так как нам нужно сгенерировать код, осуществляющий две вещи: определение функции с помощью DEFUN и вызов publish. Определение функции должно идти первым: таким образом, если в ее определении будет ошибка, то функция не будет опубликована. Первые две строки DEFUN являются уже привычным нам шаблонным кодом:

(defun ,name (,request ,entity)
  (with-http-response (,request ,entity :content-type "text/html")  

Теперь мы можем приступить к настоящей работе. Следующие две строки генерируют привязки параметров, заданных в define-url-function (кроме request), а также код, вызывающий set-cookie-header для сохраняемых параметров. Конечно же реальная работа осуществляется во вспомогательных функциях, которые мы вскоре увидим12).

(let* (,@(param-bindings name request params))
  ,@(set-cookies-code name request params)

Оставшаяся часть кода более шаблонна: мы помещаем тело из определения define-url-function в соответствующий контекст with-http-body, with-html-output и макроса html. Затем идет вызов publish.

(publish :path ,(format nil "/~(~a~)" name) :function ',name)

Выражение (format nil "/~(~a~)" name) вычисляется во время раскрытия макросов, генерируя строку, состоящую из /, за которым следует преобразованное к нижнему регистру имя функции, почти определенной нами. Эта строка становится аргументом :path функции publish, а имя функции – аргументом :function.

Теперь давайте взглянем на вспомогательные функции, используемые при генерировании формы DEFUN. Для генерирования привязок параметров нам нужно пройтись по параметрам и собрать код, сгенерированный param-binding для каждого из них. Такой код для каждого параметра будет списком, содержащим имя связываемой переменной и код, который вычисляет ее значение. Точный код для вычисления значения будет зависеть от типа параметра, того, является ли он сохраняемым, и от наличия значения по умолчанию. Так как мы уже нормализовали параметры, мы можем использовать DESTRUCTURING-BIND для их разбора в param-binding.

(defun param-bindings (function-name request params)
  (loop for param in params
     collect (param-binding function-name request param)
)
)


(defun param-binding (function-name request param)
  (destructuring-bind (name type &optional default sticky) param
    (let ((query-name (symbol->query-name name))
          (cookie-name (symbol->cookie-name function-name name sticky))
)

      `(,name (or
               (string->type ',type (request-query-value ,query-name ,request))
               ,@(if cookie-name
                     (list `(string->type ',type (get-cookie-value ,request ,cookie-name)))
)

               ,default
)
)
)
)
)

Функция string->type, используемая для преобразования к желаемым типам полученных из параметров запроса и cookies строк, является обобщенной функцией со следующей сигнатурой:

(defgeneric string->type (type value))

Для того, чтобы иметь возможность использования определенного имени в качестве имени типа параметра запроса, нам нужно просто определить метод string->type. Нам нужно определить по меньшей мере метод, специализированный по строковому типу, так как это тип по умолчанию. Конечно же это очень просто. Так как браузеры порой посылают формы с пустыми строками для индикации отсутствия значения, нам нужно преобразовывать пустые строки в NIL, как и делает следующий метод:

(defmethod string->type ((type (eql 'string)) value)
  (and (plusp (length value)) value)
)

Мы можем добавить преобразования для других типов, нужных нашему приложению. Например, чтобы иметь возможность использования в качестве типа параметров запроса integer, а следовательно возможность обработки параметра limit функции random-page, мы можем определить следующий метод:

(defmethod string->type ((type (eql 'integer)) value)
  (parse-integer (or value "") :junk-allowed t)
)

Еще одной вспомогательной функцией, используемой кодом, генерируемым param-binding, является get-cookie-value, которая является небольшим синтаксическим сахаром вокруг функции get-cookie-values, предоставляемой AllegroServe. Она выглядит следующим образом:

(defun get-cookie-value (request name)
  (cdr (assoc name (get-cookie-values request) :test #'string=))
)

Функции, вычисляющие имена параметров запроса и cookies, довольно прямолинейны:

(defun symbol->query-name (sym)
  (string-downcase sym)
)


(defun symbol->cookie-name (function-name sym sticky)
  (let ((package-name (package-name (symbol-package function-name))))
    (when sticky
      (ecase sticky
        (:global
         (string-downcase sym)
)

        (:package
         (format nil "~(~a:~a~)" package-name sym)
)

        (:local
         (format nil "~(~a:~a:~a~)" package-name function-name sym)
)
)
)
)
)

Для генерирования кода, устанавливающего cookies для сохраняемых параметров, нам снова нужно пройтись по списку парметров, на этот раз собирая код для каждого сохранямого параметра. Мы можем использовать формы LOOP when и collect it для собирания только не-NIL значений, возвращенных set-cookie-code.

(defun set-cookies-code (function-name request params)
  (loop for param in params
       when (set-cookie-code function-name request param) collect it
)
)


(defun set-cookie-code (function-name request param)
  (destructuring-bind (name type &optional default sticky) param
    (declare (ignore type default))
    (if sticky
      `(when ,name
         (set-cookie-header
          ,request
          :name ,(symbol->cookie-name function-name name sticky)
          :value (princ-to-string ,name)
)
)
)
)
)

Одним из преимуществ определения макросов в терминах вспомогательных функций, как здесь, является то, что так легко удостовериваться, что отдельные части генерируемого кода выглядят правильно. Например, вы можете проверить, что такой вызов set-cookie-code:

(set-cookie-code 'foo 'request '(x integer 20 :local))

генерирует такой код:

(WHEN X
  (SET-COOKIE-HEADER REQUEST
    :NAME "com.gigamonkeys.web:foo:x"
    :VALUE (PRINC-TO-STRING X)
)
)

Подразумевая, что этот код находится в контексте, в котором x является именем переменной, все выглядит хорошо.

И еще раз, макросы позволяют нам свести код, которые необходимо писать, к его сути: в нашем случае это данные, которые нам нужно извлечь из запроса, и HTML, который мы хотим сгенерировать. Этот каркас не претендует на то, чтобы являться наивысшим достижением в области создания каркасов построения web-приложений, — он является просто небольшим синтаксическим сахаром, немного упрощающим написание простых приложений, наподобие такого, что мы напишем в главе 29.

Но перед тем, как приступить к этому, нам нужно написать "внутренности" приложения, для которых приложение, которое мы напишем в главе 29, будет пользовательским интерфейсом. Мы начнем в следующей главе с написания улучшенной версии базы данных, написанной нами ранее в главе 3, которую мы будем использовать для хранения данных ID3, извлеченных из файлов MP3.

1)Новичкам в Web-программировании вероятно понадобится дополнить это введение информацией из одного-двух учебников с более глубоким охватом. Вы можете найти хорошую подборку доступных online учебников по адресу http://www.jmarshall.com/easy/.
2)Загрузка отдельной Web-страницы может привести к выполнению множества запросов – для отображения HTML-страницы, содержащей изображения, браузер должен выполнить отдельный запрос для каждого из них, а затем вставить их на соответствующие места страницы.
3)Большая часть сложности Web-программирования заключается в попытках обойти это основное ограничение, чтобы предоставить пользователю больше возможностей, таких как интерактивность приложений, выполняющихся на компьютере пользователей.
4)К сожалению, слово динамичный (dynamic) имеет много значений в мире Web. Фраза "динамичный HTML" (Dynamic HTML) относится к HTML, который содержит встроенный код, обычно на языке JavaScript, который может выполняться браузером без дополнительного общения с сервером. При осторожном использовании, динамичный HTML может улучшить работу Web-приложения, поскольку, даже при использовании высокоскоростных соединений, выполнение запроса к серверу, получение результата, и отображение новой страницы может занять заметное количество времени. Что еще более запутывает дело, динамически генерируемые страницы (страницы генерируемые на сервере) могут также содержать динамичный HTML (код для выполнения на клиенте). В этой книге мы столкнемся только с динамически генерируемым обычным, не динамичным HTML.
5)http://www.fractalconcept.com/asp/html/mod_lisp.html
6)http://lisplets.sourceforge.net/
7)AllegroServe также предоставляет каркас (framework), названный Webactions, который аналогичен JSP в Java — вместо написания кода, который генерирует HTML, с помощью Webactions вы можете писать страницы, которые являются HTML, но с небольшим количеством специального кода, разворачивающегося в настоящий код при обработке страницы. В этой книге я не буду описывать Webactions.
8)Загрузка PortableAllegroServe также создаст дополнительные пакеты для библиотек, обеспечивающих совместимость, но нас в основном интересуют три вышеперечисленных пакета.
9)Спецификатор ~@, за которым следует знак новой строки, заставляет FORMAT игнорировать пробельные знаки после этого знака новой строки, что позволяет вам красиво отформатировать код без фактического добавления пробельных знаков в HTML. Поскольку пробельные знаки обычно не являются значимыми в HTML, это никак не влияет не работу браузера, но делает сгенерированный код HTML более удобным для чтения людьми.
10)FOO – это рекурсивный тавтологический акроним для "FOO Outputs Output".
11)За информацией о смысле остальных параметров обращайтесь к документации AllegroServe и RFC 2109, разъясняющей механизм cookie.
12)Нам нужно использовать LET* вместо LET, чтобы позволить формам значений параметров по умолчанию ссылаться на параметры, идущие в списке ранее. Например, вы можете написать такое:
(define-url-function (request (x integer 10) (y integer (* 2 x))) ...)
и значение y, не будучи предоставлено, будет удвоенным значением x.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru