Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу).
Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу). Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу). Homepage Карта сайта Версия для печати

Джентльменский набор Web-разработчика   Ларри Уолл о Perl6   Наблы Система Orphus
 

3. Борьба с 500-й Ошибкой закончилась  ее полной капитуляцией, или почему я так люблю связывать потоки

[21 августа 2001 г.]

Вы когда-нибудь пробовали унять головную боль без таблетки?.. Тогда вы должны по достоинству оценить проблему, которая возникает при программировании на «чистом» Perl. Ничто так не отпугивает человека, начинающего изучать Perl, как постоянные надоедливые сообщения Apache (сервер такой) о 500-й ошибке, как только человек допускает малейшую неточность в своем скрипте.

Лирическое отступление 
Вот что говорят программисты, всю жизнь использующие Perl для CGI-скриптов: «500-я ошибка?.. Ну и что? Я лично всегда держу в соседнем окне терминала запущенную программу Unix tail -f имя_лога_Apache и чувствую себя прекрасно — туда выводятся все сообщения об ошибках».

Да, есть такой способ, но если мы отлаживаем скрипты под Windows, это не так уж и удобно. В самом деле, приходится все время вертеть головой: редактор — браузер — окно ошибок. Но вспомним, как поступает PHP, когда в скрипте обнаруживается ошибка? Он просто выводит ее текст в браузер. И это удобно. Бесспорно удобно — не нужно дополнительное окно, а также исчезает постоянная головная боль с 500-й ошибкой.

Лирическое отступление 
Программист на Perl В этом месте любители PHP обычно злорадствуют и потирают верхние конечности, а знатоки Perl — беспомощно разводят руками, делая вид, что ничего особенного не произошло.

Но сейчас мы сделаем так, чтобы и в Perl сообщение об ошибках выводилось в браузер, а не в логи. Вначале разберемся, почему вообще возникает сообщение о 500-й ошибке. А дело все в стандарте на протокол HTTP. Он требует, чтобы перед тем, как браузер получит тело документа-ответа, ему прислали так называемый заголовок типа документа. Например:

#!/usr/bin/perl -w
# Обратите внимание на эту строчку!
print "Content-type: text/html\n\n";
# Дальше выводим тело документа.
print "It works!";

Попробуйте убрать строку, которая выводит заголовок Content-type, и 500-я ошибка вам обеспечена. Таким образом, мы должны всегда выводить заголовок типа документа перед любым текстом. Этого можно достичь, например, так:

#!/usr/bin/perl -w
# Обратите внимание на эту строчку!
BEGIN { print "Content-type: text/html\n\n"; }
# Дальше выводим тело документа.
print "It works!";

Теперь 500-я ошибка никогда не появится, зато в случае синтаксической ошибки в скрипте перед пользователем предстанет пустая страница, что еще хуже. Кроме того, такой подход отвратителен: выводя первой строкой скрипта заголовок Content-type, мы тем самым лишаемся права выводить какие-нибудь другие заголовки в браузер — в том числе, устанавливать Cookies. Но как же быть? Ведь нам, с одной стороны, нужно сначала запустить скрипт, перехватить все сообщения об ошибках и вывести тело документа (в котором также могут выводиться и некоторые заголовки). С другой — в самом начале выводить в браузер Content-type. Головоломка, однако.

Решение лишь одно. Придется буферизовать весь вывод скрипта (записывать его в переменную-буфер). Затем, в самом конце, когда скрипт уже лежит на смертном одре, еле дышит и готов завершиться, выводить по порядку: накопленные заголовки; содержимое буфера; наконец, сообщения об ошибках.

Лирическое отступление 
Слава всевышнему, Perl имеет для такого «финта ушами» все средства.

Сначала о том, как накапливать сообщения об ошибках. Для этого нужно присвоить адрес функций-обработчиков элементам $SIG{__WARN__} и $SIG{__DIE__} (для перехвата предупреждений и ошибок соответственно). Как только произойдет недоразумение, Perl вызовет тот или иной обработчик, который должен молча отработать и добавить в массив ошибок @Errors новое сообщение. В браузер при этом ничего не выводится.

# массив ошибок
@Errors=();
# "глушим" выходной поток STDERR, чтобы не 
# засорять файлы журнала сервера
open(STDERR,">/dev/null") 
    or open(STDERR,">nul") 
    or die "Can't find null device!";
# устанавливаем обработчики
$SIG{__WARN__}=sub { push @Errors, "Warning: ".shift) };
$SIG{__DIE__} =sub { push @Errors, "Fatal: ".shift) };

С ошибками вроде бы разобрались. Теперь о том, как накапливать заголовки. В PHP скрипт может в процессе своей работы вызвать функцию Header() и передать ей строку — заголовок, который отправится серверу перед телом документа. Поступим так и на Perl. В нашем Perl-скрипте функция Header() будет просто добавлять указанный заголовок в массив заголовков @Headers, ничего при этом не выводя.

# массив заголовков
@Headers=();
# void Header(string $head)
sub Header
{   push(@Headers,@_);
    return 1;
}

И самое сложное — буферизация вывода. Perl — настолько могучий язык, что в нем имеется возможность перехватить обращение к той или иной переменной (на чтение или запись) специальной функцией-обработчиком. Процесс назначения переменной обработчиков называется «связыванием». Известно, что вывод происходит при помощи print, а она неявно использует файловую переменную STDOUT, связанную по умолчанию с браузером пользователя. Нам нужно всего лишь перехватить вывод для STDOUT.

Вот что у нас получится в результате.

#!/usr/bin/perl -w

# Все функции-обработчики имеют фиксированные имена 
# и должны располагаться в одном пакете. Так что
# временно переключаемся на пакет Output.
{{{
package Output;
# буфер для накопления вывода
$Buf='';
# массив ошибок
@Errors=();
# массив заголовков
@Headers=();

# "глушим" выходной поток STDERR, 
# чтобы не засорять файлы журнала
open(STDERR,">/dev/null") 
    or open(STDERR,">nul") 
    or die "Can't find null device!";
# устанавливаем обработчики
$SIG{__WARN__}=sub { push @Errors, "Warning: ".shift };
$SIG{__DIE__} =sub { push @Errors, "Fatal: ".shift };

# void Header(string $head)
sub main::Header
{   push(@Headers,@_);
    return 1;
}

# функции-обработчики
sub TIEHANDLE { return bless({}); }
sub PRINT { shift; $Buf.=join('',@_); return 1; }
sub BINMODE { binmode(THE_REAL_STDOUT); }
# сохраняем статус "старого" STDOUT
*THE_REAL_STDOUT=*STDOUT;
# связываем STDOUT с обработчиками
tie(*STDOUT,'Output');

# функция-финализатор: гарантированно 
# вызывается перед завершением скрипта.
END {
    untie *STDOUT;
    # выводим заголовки
    print join "\n",
        (@Headers,"Content-type: text\html","") 
        if @Headers;
    # выводим документ
    print "\n",$Buf;
    # выводим сообщения об ошибках
    print "\n<p>".join "<br>\n", 
        map { s{^\w+:}{<b>$&</b>}s; $_ } 
        @Errors if @Errors;
}
}}}

### 

### Далее идет код тестового скрипта, который
### иллюстрирует работу всего этого механизма.
### 

print $a;                     # предупреждение
Header("X-Powered-by: test"); # заголовок
print __PACKAGE__;            # тело
Код может показаться довольно внушительным, но это только кажется: нужно просто поместить его в модуль и подключать последний в скриптах при помощи use. Напоминаю, что, используя такую технологию, вы полностью избавляетесь от надоедливой 500-й ошибки, а значит, доводите комфорт своей работы почти до уровня PHP.

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

 
Рекламный блок
   

Важное объявление:
    автор категорически против копирования и распространения в Интернете всех статей «Куроводства» с возрастом, меньшим 6 месяцев. Печальный опыт «расползания» чрезвычайно устаревших ошибочных версий статьи про Apache действительно объясняет такое решение.

Орфография на «Куроводстве»:
    если вы заметили орфографическую, стилистическую или другую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Выделенный текст будет немедленно отослан вебмастеру, а Вы даже ничего и не заметите — настолько быстро все произойдет.

На заметку:
    если вы уже вскипели насчет дизайна этой страницы, то присмотритесь повнимательнее к названию, почитайте FAQ, сходите по лебедевским местам, как это уже предлагалось выше. Можно ли считать пародию плагиатом? Надеюсь, что нет.

Параметры этой страницы
   
GZip

Ссылки от спонсоров
    Продажа готовых ООО в компании "RS Право". | Смотрите описание литературный перевод у нас на сайте.


Дмитрий Котеров | 21 августа 2001 г. ©1999-2016 | Генеральный спонсор: Хостинг «Джино» | Контакт Вернуться к оглавлению