В 1998 году гостевая книга была мерилом технической зрелости сайта. Статический HTML был для тех, кто не умеет программировать. Работающая гостевая книга означала: разработчик понимает CGI.
Стек: Perl 5 на Unix-сервере (Apache на Linux или Solaris), спецификация CGI для обмена данными между сервером и браузером, текстовый файл для хранения данных. MySQL в 1998 году стоил как отдельный тарифный план хостинга - $40-80 в месяц сверху. Текстовых файлов вполне хватало для гостевой книги с несколькими десятками посетителей в день.
Как работал CGI
CGI - Common Gateway Interface - стандарт запуска программ веб-сервером с передачей вывода браузеру. Механизм: сервер получает POST или GET запрос, запускает указанную программу (скрипт Perl), передаёт данные формы через переменные среды и STDIN, возвращает STDOUT программы браузеру как тело HTTP-ответа.
Критическое ограничение: каждый CGI-запрос порождал новый процесс. Каждая загрузка страницы запускала Perl с нуля, загружала скрипт, выполняла его и завершала процесс. Никакого состояния между запросами. Никакого пула соединений. При высокой нагрузке - медленно и ресурсоёмко. Для гостевой книги с пятьюдесятью посетителями в день - вполне приемлемо.
Полный скрипт гостевой книги
#!/usr/bin/perl
# guestbook.pl - гостевая книга на Perl 5 CGI, 1998 год
# Положить в cgi-bin/, chmod 755, chmod 777 для папки с данными
use strict;
use warnings;
use CGI;
use POSIX qw(strftime);
my $GUESTBOOK_FILE = '/home/mysite/data/guestbook.txt';
my $MAX_ENTRIES = 100;
my $cgi = new CGI;
my $action = $cgi->param('action') || 'view';
if ($action eq 'post') {
handle_post($cgi);
} else {
show_guestbook($cgi);
}
# ---------------------------------------------------------------
sub handle_post {
my ($cgi) = @_;
my $name = $cgi->param('name') || '';
my $email = $cgi->param('email') || '';
my $message = $cgi->param('message') || '';
# Убираем HTML-теги - в 1998 году никаких rich-text редакторов
for my $var ($name, $email, $message) {
$var =~ s/[<>&"']//g;
$var =~ s/\n/ /g;
}
if (length($name) < 2 || length($message) < 5) {
print_header();
print "<p><font color='red'><b>Ошибка:</b> Имя и сообщение обязательны.</font></p>\n";
print "<p><a href='guestbook.pl'>Вернуться назад</a></p>\n";
print_footer();
return;
}
my $timestamp = strftime('%d.%m.%Y в %H:%M', localtime);
my @entries = read_entries();
unshift @entries, {
name => $name,
email => $email,
message => $message,
timestamp => $timestamp,
};
@entries = @entries[0 .. $MAX_ENTRIES - 1] if @entries > $MAX_ENTRIES;
write_entries(\@entries);
# Редирект через meta refresh - HTTP 302 был ненадёжным в CGI-контексте
print "Content-Type: text/html\n\n";
print "<html><head><meta http-equiv='refresh' content='0;url=guestbook.pl'>\n";
print "</head><body>Спасибо! <a href='guestbook.pl'>Нажмите здесь</a>, если не перешло автоматически.</body></html>\n";
}
# ---------------------------------------------------------------
sub show_guestbook {
my ($cgi) = @_;
print_header();
print <<'FORM';
<h2>Оставить запись</h2>
<form method="POST" action="guestbook.pl">
<input type="hidden" name="action" value="post">
<table border="0" cellpadding="4" cellspacing="0">
<tr>
<td align="right"><b>Имя:</b> *</td>
<td><input type="text" name="name" size="30" maxlength="60"></td>
</tr>
<tr>
<td align="right"><b>E-mail:</b></td>
<td><input type="text" name="email" size="30" maxlength="80"></td>
</tr>
<tr>
<td align="right" valign="top"><b>Сообщение:</b> *</td>
<td><textarea name="message" rows="5" cols="45"></textarea></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Оставить запись"></td>
</tr>
</table>
</form>
<hr>
<h2>Записи</h2>
FORM
my @entries = read_entries();
if (!@entries) {
print "<p><i>Записей пока нет - будьте первым!</i></p>\n";
}
for my $entry (@entries) {
print "<div style='border-bottom:1px solid #cccccc; padding:6px 0; margin-bottom:6px;'>\n";
print "<b>" . $entry->{name} . "</b>";
print " <small><i>" . $entry->{timestamp} . ":</i></small><br>\n";
print $entry->{message} . "\n";
print "</div>\n";
}
print_footer();
}
# ---------------------------------------------------------------
sub read_entries {
my @entries;
return @entries unless -f $GUESTBOOK_FILE;
open(my $fh, '<', $GUESTBOOK_FILE) or die "Не могу открыть файл: $!";
my $entry = {};
while (my $line = <$fh>) {
chomp $line;
if ($line =~ /^NAME:(.*)$/) { $entry->{name} = $1 }
elsif ($line =~ /^EMAIL:(.*)$/) { $entry->{email} = $1 }
elsif ($line =~ /^MSG:(.*)$/) { $entry->{message} = $1 }
elsif ($line =~ /^TIME:(.*)$/) { $entry->{timestamp} = $1 }
elsif ($line eq '---') {
push @entries, $entry if $entry->{name};
$entry = {};
}
}
close($fh);
return @entries;
}
sub write_entries {
my ($entries) = @_;
open(my $fh, '>', $GUESTBOOK_FILE) or die "Не могу записать файл: $!";
for my $e (@$entries) {
print $fh "NAME:" . $e->{name} . "\n";
print $fh "EMAIL:" . $e->{email} . "\n";
print $fh "MSG:" . $e->{message} . "\n";
print $fh "TIME:" . $e->{timestamp} . "\n";
print $fh "---\n";
}
close($fh);
}
sub print_header {
# Пустая строка после заголовка - обязательное требование CGI
print "Content-Type: text/html; charset=windows-1251\n\n";
print "<html>\n<head><title>Гостевая книга</title></head>\n";
print "<body bgcolor='#FFFFFF' text='#000000'>\n";
print "<h1>Гостевая книга</h1>\n";
}
sub print_footer {
print "<hr><small>Работает на Perl CGI / Apache / Linux</small>\n";
print "</body></html>\n";
}
Деплой в 1998 году: FTP, Telnet, chmod
Чтобы запустить CGI-скрипт на shared-хостинге в 1998 году:
- Загрузить скрипт по FTP в папку
cgi-bin/- веб-сервер выполнял CGI только из этой директории - Подключиться по Telnet:
telnet myhost.ru 23 - Выставить права:
chmod 755 guestbook.pl - Создать папку с данными и дать ей права на запись:
chmod 777 /home/mysite/data/
chmod 777 - это была общепринятая практика. И одновременно - дыра в безопасности: любой другой пользователь на том же shared-сервере мог перезаписать ваш файл данных. Все это понимали. Делали именно так, потому что альтернатива требовала прав root, которых хостер не давал.
Самая частая ошибка - 500 Internal Server Error:
# Три причины, которые знал каждый Perl-разработчик в 1998 году:
# 1. Неверный путь к интерпретатору в shebang
head -1 guestbook.pl
which perl
# Должны совпадать
# 2. Windows-переносы строк (CRLF) на Unix-сервере - убивали shebang
file guestbook.pl
# Если "CRLF line terminators":
perl -pi -e 's/\r\n/\n/g' guestbook.pl
# 3. Читать лог Apache (если был доступ по SSH)
tail -20 /var/log/apache/error.log
Кодировка: windows-1251 в Рунете
В 1998-2001 году в Рунете шла негласная война кодировок. Московские сайты - windows-1251. Академические и питерские - KOI8-R (стандарт Unix-систем). Один и тот же текст, открытый в неправильной кодировке, превращался в нечитаемый набор символов.
Решение в Perl:
# Рунет 1998-2001: всё в windows-1251, CGI-скрипты тоже
print "Content-Type: text/html; charset=windows-1251\n\n";
# Если данные приходили из формы в KOI8-R и нужно конвертировать:
use Encode;
my $text_utf = Encode::decode('koi8-r', $raw_koi8);
my $text_win = Encode::encode('cp1251', $text_utf);
Рунет полностью переходил на UTF-8 очень медленно - Яндекс перешёл в 2009 году.
Почему PHP вытеснил Perl в веб-разработке
К 2000 году PHP 4 активно захватывал рынок веб-разработки. Причина не в том, что PHP был мощнее - Perl объективно гибче и выразительнее. Причина - в простоте деплоя.
PHP-страница:
- Лежит рядом с HTML-файлами в
public_html/ - Выполняется через
mod_phpавтоматически - Не требует отдельного
cgi-bin/ - Не требует
chmod 755 - Нет зависимости от пути к интерпретатору
Гостевая книга на Perl требовала понимания Unix-прав, протокола CGI и модуля CGI.pm до того, как вы могли вывести хоть что-то в браузер. PHP позволял написать <?php echo "Привет"; ?> в HTML-файл - и это работало немедленно.
Эта простота и решила судьбу веб-рынка: PHP выиграл сайтостроительство, Perl остался в системном администрировании и биоинформатике.
Гостевая книга на Perl CGI была первым практическим уроком: как веб-сервер общается с программой, как парсить данные форм, как читать и писать постоянное хранилище. Паттерны, актуальные в любом веб-фреймворке и сегодня.