Создание CGI-программы гостевой книги
Если вы внимательно изучили примеры, приведенные выше, то уже должны быть способны заставить работать простые CGI-программы. А как насчет более сложных? Одна из распространенных задач — создание CGT-программы для управления гостевой книгой, чтобы посетители вашего Web-узла могли записывать в нее свои собственные сообщения*.
* Как мы отметим ниже, это приложение можно было бы назвать программой Webchat (переговоров через Web).
Форма, используемая для создания гостевой книги, довольно проста, она даже проще, чем некоторые из наших форм, посвященных мороженому. Она, правда, имеет некоторые особенности, но не беспокойтесь, по мере продвижения к финишу мы преодолеем все трудности.
Вероятно, вы хотите, чтобы сообщения в гостевой книге сохранялись и по завершении посещения вашего узла тем или иным пользователем, поэтому вам нужен файл, куда они будут записываться. Гостевая CGI-программа (вероятно) работает не под вашим управлением, поэтому у нее, как правило, не будет права на обновление вашего файла. Следовательно, первое, что необходимо сделать,— это создать для нее файл с широкими правами доступа. Если вы работаете в UNIX-системе, то можете сделать (из своего shell) для инициализации файла программы гостевой книги следующее:
touch /usr/tmp/chatfile chmod 0666 /usr/tmp/chatfile
Отлично, но как обеспечить одновременную работу с программой гостевой книги нескольких пользователей? Операционная система не блокирует попытки одновременного доступа к файлам, поэтому если вы будете недостаточно осторожны, то получите файл, в который записывают сообщения все пользователи одновременно. Чтобы избежать этого, мы используем Perl-функцию flock, позволяющую пользователю получить монопольный доступ к файлу, который мы разрешаем обновить. Это будет выглядеть примерно так:
use Fcnti qw(:flock); # импортирует LOCK_EX, LOCKJ3H, LOCK_NB flock(CHANDLE, LOCK_EX) || bail ("cannot flock $CHATNAME: $!");
Аргумент lock_ex функции flock — вот что позволяет нам обеспечить монопольный доступ к файлу*.
Функция flock представляет собой простой, но универсальный механизм блокировки, несмотря на то, что его базовая реализация существенно изменяется от системы к системе. Она не возвращает управление до тех пор, пока файл не будет разблокирован. Отметим, что блокировки файлов носят чисто рекомендательный характер: они работают только тогда, когда все процессы, обращающиеся к файлу, соблюдают эти блокировки одинаково. Если три процесса соблюдают блокировки, а один не соблюдает, то не функционирует ни одна из блокировок.
* В версиях Perl до 5.004 вы должны превратить в комментарий use Fcnti и в качестве аргумента функции flock использовать просто 2.
Объектно-ориентированное программирование на Perl
Наконец пришло время научить вас пользоваться объектами и классами — и это важнее всего. Хотя решение задачи построения вашего собственного объектного модуля выходит за рамки данной книги, это еще не повод для того, чтобы вы не могли использовать существующие объектно-ориентированные библиотечные модули. Подробная информация об использовании и создании объектных модулей приведена в главе 5 книги Programming Perl и на man-странице perltoot(l).
Мы не будем углубляться здесь в теорию объектов, и вы можете просто считать их пакетами (чем они и являются!) удивительных, великолепных вещей, вызываемых косвенно. Объекты содержат подпрограммы, которые делают все, что вам нужно делать с объектами.
Пусть, например, модуль CGI.pm возвращает объект $query, который представляет собой входные данные пользователя. Если вы хотите получить параметр из этого запроса, вызовите подпрограмму par am () :
$query->param("answer");
Данная запись означает: "Выполнить подпрограмму param () с объектом $query, используя "answer" как аргумент". Такой вызов в точности соответствует вызову любой другой подпрограммы, за исключением того что вы используете имя объекта, за которым следует синтаксическая конструкция ->. Кстати, подпрограммы, связанные с объектами, называются методами.
Если вы хотите получить значение, возвращенное подпрограммой param (), воспользуйтесь обычным оператором присваивания и сохраните это значение в обычной переменной $he_said:
$he_said = $query->param("answer");
Объекты выглядят как скаляры; они хранятся в скалярных переменных (таких как переменная $ query в нашем примере), и из них можно составлять массивы и хеши. Тем не менее их не следует рассматривать как строки и числа. По сути дела, это особый вид ссылок, их нельзя рассматривать как обычные ссылки. Объекты следует трактовать как особый, определяемый пользователем тип данных.
Тип конкретного объекта известен как его класс. Имя класса обычно состоит из имени модуля без расширения рт, и к тому же термины "класс" и "модуль" часто используются как эквиваленты. Таким образом, мы можем говорить о CGI-модуле или о CGI-классе. Объекты конкретного класса создает и контролирует модуль, реализующий этот класс.
Доступ к классам осуществляется путем загрузки модуля, который выглядит точно так же, как любой другой модуль, за исключением того что объектно-ориентированные модули обычно ничего не экспортируют. Вы можете рассматривать класс как фабрику, которая производит совершенно новые объекты. Чтобы класс выдал один из таких объектов, нужно вызвать специальный метод, который называется конструктор.
Вот пример:
$query = CGI->new(); # вызвать метод new() в классе "CGI"
Здесь мы имеем дело с вызовом метода класса. Метод класса выглядит точно так же, как метод объекта (о котором мы говорили секунду назад), за исключением того что вместо использования объекта для вызова метода мы используем имя класса, как будто он сам — объект. Метод объекта говорит "вызвать функцию с этим именем, которая относится к данному объекту", тогда как метод класса говорит "вызвать функцию с этим именем, которая относится к данному классу".
Иногда то же самое записывается так:
$query = new CGI; # то же самое
Вторая форма по принципу действия идентична первой. Здесь меньше знаков препинания, благодаря чему в некоторых случаях она более предпочтительна. Однако она менее удобна в качестве компонента большого выражения, поэтому в нашей книге мы будем использовать исключительно первую форму.
С точки зрения разработчика объектных модулей, объект — это ссылка на определяемую пользователем структуру данных, часто на анонимный хеш. Внутри этой структуры хранится всевозможная интересная информация. Воспитанный пользователь, однако, должен добираться до этой информации (с целью ее изучения или изменения), не рассматривая объект как ссылку и не обращаясь непосредственно к данным, на которые он указывает, а используя только имеющиеся методы объектов и классов. Изменение данных объекта другими средствами — это нечестная игра, после которой о вас обязательно станут говорить и думать плохо. Чтобы узнать о том, что представляют собой и как работают вышеупомянутые методы, достаточно прочитать документацию на объектный модуль, которая обычно прилагается в pod-формате.
Объекты в модуле CGI.pm
CGI-модуль необычен в том смысле, что его можно рассматривать либо как традиционный модуль с экспортируемыми функциями, либо как объектный модуль. Некоторые программы пишутся гораздо легче с помощью объектного интерфейса к модулю CGI.pm, нежели с помощью процедурного интерфейса к данному модулю. Наша гостевая книга — одна из таких программ. Мы получаем доступ к входной информации, которую пользователь ввел в форму, через CGI-объект и можем, при желании, с помощью этого же объекта генерировать новый HTML-код для отправки обратно пользователю.
Сначала, однако, нам нужно создать этот объект явно. Для CGI.pm, как и для многих других классов, метод, который позволяет создавать объекты,— это метод класса new () *.
* В отличие от C++ Perl не считает new ключевым словом; вы совершенно свободно можете использовать такие методы-конструкторы, как gimme_another() или fred.0. Тем не менее большинство пользователей в итоге приходят к тому, что называют свои конструкторы во всех случаях new ().
Данный метод конструирует и возвращает новый CGI-объект, соответствующий заполненной форме. Этот объект содержит все данные, введенные пользователем в форму. Будучи вызванным без аргументов, метод new () создает объект путем чтения данных, переданных удаленным броузером. Если в качестве аргумента указан дескриптор файла, он читает этот дескриптор, надеясь найти в нем данные, введенные в форму в предыдущем сеансе работы с броузером.
Через минуту мы покажем вам эту программу и поясним ее работу. Давайте предположим, что она называется guestbook
и находится в каталоге cgi-bin. Хоть она и не похожа ни на один из тех сценариев, которые мы рассмотрели выше (в которых одна часть выводит HTML-форму, а вторая читает данные, введенные в форму пользователем, и отвечает на них), вы увидите, что она, тем не менее, выполняет обе эти функции. Поэтому отдельный HTML-документ, содержащий форму гостевой книги, нам не нужен. Пользователь мог бы сначала запустить нашу программу, просто щелкнув мышкой на такой ссылке:
Please sign our <А HREF="http://www.SOMEWHERE.org/cgi-bin/guestbook">guestbook</A>.
Затем программа загружает в броузер HTML-форму и, на всякий случай, предыдущие сообщения гостей (в ограниченном количестве), чтобы пользователь мог их просмотреть. Пользователь заполняет форму, передает ее, и программа читает то, что передано. Эта информация добавляется в список предыдущих сообщений (хранящийся в файле), который затем вновь выводится в броузер, вместе со свежей формой. Пользователь может продолжать чтение текущего набора сообщений и передавать новые сообщения, заполняя предлагаемые формы, столько раз, сколько сочтет необходимым.
Вот наша программа. Перед тем как разбирать ее поэтапно, вы, возможно, захотите просмотреть программу целиком.
#!/usr/bin/peri -w
use 5.004;
use strict; # установить объявления и взятие в кавычки use CGI qw(:standard); # импортировать сокращения согласно :standard use Fcnti qw(:flock); # импортирует LOCK_EX, LOCKJ3H, LOCK_NB
sub bail ( # функция обработки ошибок
my $error = "@ ";
print hi("Unexpected Error"), p($error), end html;
die $error;
!
my(
$CHATNAME, # имя файла гостевой книги $MAXSAVE, # какое количество хранить $TITLE, # название и заголовок страницы @cur, # все текущие записи
Sentry, # одна конкретная запись ) ;
$TITLE = "Simple Guestbook";
$CHATNAME = "/usr/tmp/chatfile"; # где все это в системе находится $MAXSAVE =10;
print header, start_html($TITLE), hi ($TITLE);
$cur ” CGI->new(); # текущий запрос if ($cur->param("message")) ( # хорошо, мы получили сообщение
• $cur->param("date", scalar localtime); # установить текущее время Sentries = ($cur); # записать сообщение в массив }
# открыть файл для чтения и записи (с сохранением предыдущего содержимого) open(CHANDLE, "+< $CHATNAME") II bail("cannot open $CHATNAME: $!");
# получить эксклюзивную блокировку на гостевую книгу
# (LOCK_EX == exclusive lock)
flock(CHANDLE, LOCK_EX) || bail("cannot flock $CHATNAME: $!");
# занести в $MAXSAVE старые записи (первой — самую новую) while (!eof(CHANDLE) && Sentries < $MAXSAVE) (
$entry = CGI->new(\*CHANDLE); t передать дескриптор файла по ссылке
push Sentries, $entry;
}
seek(CHANDLE, 0, 0) 11 bail("cannot rewind $CHATNAME: $!");
foreach $entry (Sentries) (
$entry->save(\*CHANDLE); # передать дескриптор файла по ссылке } truncate(CHANDLE, tell(CHANDLE)) || bail("cannot truncate $CHATNAME: $!");
close(CHANDLE) || bail ("cannot close $CHATNAME: $!");
print hr, start form; # hr()проводит горизонтальную линию: <HR> print p("Name:", $cur->textfield(
-NAME => "name")) ;
print p("Message:" $cur->textfield(
-NAME => "message",
-OVERRIDE => 1, # стирает предыдущее сообщение
-SIZE => 50)) ;
print p(submit("send"), reset("clear"));
print end_form, hr;
print h2("Prior Messages");
foreach $entry (Sentries) f
printf("%s [%s]: %s",
$entry->param("date"),
$entry->param("name"),
$entry->param("message")) ;
print br() ;
} print end_html;
На рис. 19.5 вы видите изображение, которое появляется на экране после запуска этой программы.
Рис. 19.5. Форма простой гостевой книги
Обратите внимание на то, что программа начинается с оператора
usa 5.004;
Если вы хотите запускать ее с помощью более ранние версии Perl 5, то нужно превратить в комментарий строку
use Fcnti qw(:flock)
и заменить lock_ex в первом вызове flock на z.
Поскольку каждое выполнение программы приводит к возврату HTML-формы в броузер, который обратился к программе, то программа начинается с задания HTML-кода:
print header, start_html($TITLE), hi($TITLE) ;
Затем создается новый CGI-объект:
$cur = CGI->new(); # текущий запрос
if ($cur->param("message")) ( # хорошо, мы получили сообщение
$cur->param("date", scalar localtime); # установить текущее время
Sentries = ($cur); # записать сообщение в массив
>
Если нас вызывают посредством заполнения и передачи формы, то объект $cur должен содержать информацию о тексте, введенном в форму. Форма, которую мы предлагаем (см. ниже), содержит два поля ввода: поле имени для ввода имени пользователя и поле сообщения для ввода сообщения. Кроме того, приведенный выше код ставит на введенные в форму данные (после их получения) метку даты. Передача в метод param() двух аргументов — это способ присваивания параметру, заданному первым аргументом, значения, указанного во втором аргументе.
Если нас вызывают не посредством передачи формы, а выполняя щелчок мышью на ссылке Please sign our guestbook, то объект запроса, который мы создаем, будет пуст. Проверка if даст значение "ложь", и в массив Sentries никакой элемент занесен не будет.
В любом случае мы переходим к проверке наличия записей, созданных ранее в нашем сохраняемом файле. Эти записи мы будем считывать в массив @entries. (Вспомните о том, что мы только что сделали текущие данные, если они имеются в форме, первым элементом этого массива.) Но сначала мы должны открыть сохраняемый файл:
open(CHANDLE, "+< $CHATNAME") || bail("cannot open $CHATNAME: $!");
Эта функция открывает файл в режиме неразрушающего чтения-записи. Вместо open можно использовать sysopen (). При таком способе посредством единственного вызова открывается старый файл, если он существует (без уничтожения его содержимого), а в противном случае создается новый файл:
# нужно импортировать две "константы" из модуля Fcnti для sysopen use Fcnti qw( 0_RDWR 0_CREAT );
sysopen(CHANDLE, $CHATFILE, 0_RDWRI0_CREAT, 0666) || bail "can't open $CHATFILE: $!";
Затем мы блокируем файл, как описывалось выше, и переходим к считыванию текущих записей из $ мах save в Sentries:
flock(CHANDLE, LOCK_EX) 11 bail("cannot flock $CHATNAME: $!");
while (!eof(CHANDLE) &S Sentries < $MAXSAVE) {
$entry = CGI ->new(\*CHANDLE); # передать дескриптор файла по ссылке
push Sentries, $entry;
}
Функция eof — встроенная Perl-функция, которая сообщает о достижении конца файла. Многократно передавая в метод new () ссылку на дескриптор сохраняемого файла*, мы выбираем старые записи, по одной при каждом вызове. Затем мы обновляем файл так, чтобы он включал новую запись, которую мы (возможно) только что получили:
seek(CHANDLE, 0, 0) || bail("cannot rewind $CHATNAME: $!");
foreach $entry (Sentries) {
$entry->save(\*CHANDLE); # передать дескриптор файла по ссылке } truncate(CHANDLE, tell (CHANDLE)) || bail("cannot truncate $CHATNAME: $!");
close (CHANDLE) || bailC'cannot close $CHATNAME: $!");
Функции seek, truncate и tell —встроенные Perl-функции, описания которых вы найдете в любом справочнике по языку Perl. Здесь seek переставляет указатель позиции в начало файла, truncate усекает указанный файл до заданной длины, a tell возвращает текущее смещение указателя позиции в файле по отношению к началу файла. Назначение этих строк программы — сохранить в файле только самые последние записи $maxsave, начиная с той, которая была сделана только что.
Метод save () обеспечивает собственно создание записей. Его можно вызвать здесь как $entry->save, поскольку $entry — это CGI-объект, созданный с помощью CGl->new ().
Формат записи сохраняемого файла выглядит следующим образом (запись завершается знаком "=", стоящим в отдельной строке):
ИМЯ1=ЗНАЧЕНИЕ1 ИМЯ2=ЗНАЧЕНИЕ2 ИМЯЗ=ЗНАЧЕНИЕЗ
Теперь пора возвратить обновленную форму броузеру и его пользователю. (Это будет, конечно, первая форма, которую он видит, в том случае, если он щелкнул на ссылке Please sign our guestbook.) Сначала некоторые предварительные действия:
print hr, start_form; # hr() проводит горизонтальную линию: <HR>
Как мы уже упоминали, CGI. pm позволяет нам использовать либо прямые вызовы функций, либо вызовы методов через CGI-объект. В нашей программе для создания базового HTML-кода мы обратились к простым вызовам функций, а для задания полей ввода формы продолжаем пользоваться методами объектов:
print pC'Name:", $cur->textfield( -NAME => "name")) ;
print p("Message:" $cur->textfield(
-NAME => "message",
-OVERRIDE => 1, # стирает предыдущее сообщение
-SIZE => 50)) ;
print p(submit("send"), reset("clear"));
print end_form, hr;
* Фактически она представляет собой glob-ссылку, а не ссылку на дескриптор файла, но в данном случае это почти то же самое.
Метод textfieid() возвращает поле ввода текста для нашей формы. Первый из двух приведенных выше вызовов генерирует HTML-код поля ввода текста с HTML-атрибутом, NAME="name", а второй — создает поле с атрибутом NAME="message" .
Компоненты формы, создаваемые модулем CGI.pm, по умолчанию устойчивы: они сохраняют свои значения до следующего вызова. (Но лишь в течение одного "сеанса" работы с формой, считая с того момента, когда пользователь щелкнул на ссылке Please sign our guestbook.) Это значит, что поле name = "name", созданное в результате первого вызова textfield(), будет содержать значение имени пользователя, если он уже хотя бы один раз в этом сеансе заполнял и передавал форму. Таким образом, поле ввода, которое мы сейчас создаем, будет иметь следующие HTML-атрибуты:
NAME="name" VALUE="Sam Smith"
Совсем другое дело — второй вызов text field (). Мы не хотим, чтобы поле сообщения содержало значение старого сообщения. Поэтому пара аргументов -override => 1 указывает: "Выбросить предыдущее значение этого текстового поля и восстановить значение по умолчанию". Пара аргументов -size => 50 задает размер (в символах) отображаемого поля ввода. Помимо показанных здесь, могут быть и другие необязательные пары аргументов: -DEFAULT => 'начальное значение' И -MAXLENGTH => п, где n — максимальное число символов, которое может принять данное поле.
Наконец, к удовольствию пользователя, мы выводим текущий перечень сохраняемых сообщений, включающий, естественно, то, которое он только что передал:
print h2("Prior Messages");
foreach $entry (Sentries) {
printf("%s [%s]: %s",
$entry->param("date"),
$entry->param("name"),
$entry->param("message"));
print br ();
} print end_html;
Как вы, без сомнения, догадываетесь, функция h2 задает HTML-заголовок второго уровня. В остальной части кода мы просто последовательно формируем текущий список сохраняемых записей (это тот же список, который мы ранее записали в сохраняемый файл), выводя из каждой дату, имя и сообщение.
Пользователи могут работать с этой формой гостевой книги, непрерывно набирая сообщения и нажимая кнопку передачи. Это имитирует электронную доску объявлений, позволяя пользователям видеть новые сообщения друг друга сразу же после их передачи. Общаясь друг с другом подобным образом, пользователи многократно вызывают одну и ту же CGI-программу;
предыдущие значения компонентов формы автоматически сохраняются до следующего вызова. Это особенно удобно при создании многоступенчатых форм — например, тех, которые используются в так называемых приложениях "тележек для покупок", когда вы, перемещаясь по виртуальному магазину, последовательно "делаете покупки" и форма все их запоминает.