Выпуск 2. Апрель 2013

Debug-fu в стиле Perl | Содержание | Обзор CPAN за март 2013 г.

Введение в разработку web-приложений на PSGI/Plack

PSGI/Plack — современный способ написания web-приложений на Perl. Практически каждый фреймворк так или иначе поддерживает или использует эту технологию. В статье представлено краткое введение, которое поможет быстро сориентироваться и двигаться дальше.

Мы живем в такое время, когда технологии и подходы в области web-разработки меняются очень быстро. Сначала был CGI, потом, когда его стало недостаточно, появился FastCGI. FastCGI решал главную проблему CGI. В CGI при каждом обращении было необходимо перезапускать серверную программу, обмен данными происходил при помощи STDIN и STDOUT. В FastCGI взаимодействие с сервером происходит через TCP/IP или Unix Domain Socket. Теперь у нас есть PSGI.

Что это такое?

PSGI, как говорит его разработчик Tatsuhiko Miyagawa, это «Перловый суперклей для веб-фреймворков и веб-серверов». Ближайшие родственники — WSGI (Python) и Rack (Ruby). Идея тут вот в чем. Разработчик очень часто тратит довольно много времени, чтобы адаптировать свое приложение под как можно большее количество движков, а PSGI предоставляет единый интерфейс для работы с различными серверами, что сильно упрощает жизнь.

Особенности

Безусловно, формат статьи не позволяет описать полностью все нюансы, поэтому здесь и далее будут только ключевые моменты.

  • для обмена информацией между клиентом и сервером используется $env (представляет из себя ссылку на хеш);
  • PSGI приложение — ссылка на Perl-функцию, которая принимает в качестве параметра $env;
  • функция возвращает ссылку на массив, который состоит из 3 элементов: HTTP статус, [HTTP заголовки], [Тело ответа];
  • функция может вернуть и ссылку на другую функцию, но это будет рассмотрено в других более углубленных статьях;
  • расширение файла, содержащего код запуска приложения, должно быть .psgi.

На данном этапе это все, что нужно для того, чтобы начать разбираться с кодом непосредственно.

PSGI-приложение

Ниже приведен код простейшего PSGI-приложения.

my $app = sub {
    my $env = shift;

# Производим необходимые манипуляции с $env
    return [200, ['Content-Type' => 'text/plain'], ["hello, world\n"]];
};

Сохраняем это приложение в файле app.psgi, или любом другом с расширением psgi. Смотрим на особенности. Потом на код. Потом опять на особенности. Все сходится. Запускаем.

При запуске perl app.psgi он «молча» отрабатывает, но приложение не запущено.

Основные PSGI-серверы

Для того, чтобы запускать PSGI-приложения нам необходим PSGI-сервер. На данный момент серверов несколько.

  • Starman
  • Twiggy
  • Feersum
  • Corona

Кратко о PSGI-серверах

  • Starman — pre-forking сервер; работает довольно быстро, многое умеет из коробки, поддержку unix domain sockets, например;
  • Twiggy — асинхронный сервер, базируется на AnyEvent;
  • Feersum — субъективно, самый быстрый из этого всего списка; основная часть реализована в виде XS-модулей. Базируется на EV;
  • Corona — асинхронный сервер, базируется на Coro.

Все эти сервера доступны на CPAN. В дальнейшем мы будем использовать Starman, затем сменим его на Twiggy, а затем на Feersum. Каждой задаче свой сервер.

Запуск приложения

Приложение абсолютно одинаково запустится на любом из этих серверов, может быть, под Corona его придется чуть видоизменить. После установки сервера, а в нашем случае это Starman, в /usr/bin или /usr/local/bin должен появиться исполняемый файл starman. Запуск производится следующей командой:

/usr/local/bin/starman app.psgi

По умолчанию PSGI-серверы используют 5000 порт. Мы можем его изменить, запустив приложение с ключом --port 8080, например. Напомним, что PSGI — спецификация. В данном случае мы использовали эту спецификацию для написания простейшего web-приложения. Очевидно, что для нормальной разработки нам необходимо реализовать и множество вспомогательных функций, от получения GET-параметров до получения данных cookie. Этого всего не было бы без необходимого функционала.

Plack

Plack — это реализация PSGI (в Perl есть стандартный модуль Pack, потому реализация получила имя Plack). Plack существенно облегчает нам жизнь, как разработчикам. Он содержит в себе огромное количество функций для работы с $env.

В базовой комплектации Plack состоит из довольно большого количества модулей. На данном этапе нас интересуют только эти:

  • Plack
  • Plack::Request
  • Plack::Response
  • Plack::Builder
  • Plack::Middleware

Plack::Request и Plack::Response возвращают различные значения типа Hash::MultiValue, на которые стоит обратить внимание.

Hash::MultiValue

Модуль, автором которого тоже является Tatsuhiko Miyagawa, представляет собой хеш, но с одним нюансом. Он может хранить несколько значений по одному ключу. Например: $hash->get('key') вернет value, если же значений по ключу несколько, то оно вернет последнее, а если нужны все значения, то можно воспользоваться функцией $hash->get_all('key'), тогда результат будет ('value1','value2'). Hash::MultiValue также учитывает контекст вызова, так что будьте внимательны.

Plack::Request

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

  • env — возвращает $env;
  • method — возвращает метод запроса: GET, POST, OPTIONS, HEAD, и т.д.;
  • path_info — важный метод; возвращает локальный путь к текущему скрипту;
  • parameters — возвращает параметры (x-www-form-url-encoded, параметры адресной строки) в виде Hash::MultiValue;
  • uploads — возвращает параметры (переданные при помощи multipart-form-data) тоже в виде Hash::MultiValue.

Plack::Response

  • status — устанавливает статус (код ответа HTTP), будучи вызванным без параметров, возвращает ранее установленный статус;
  • headers — устанавливает заголовки ответа;
  • finalize — точка выхода, последняя функция приложения; возвращает PSGI-ответ согласно спецификации.

Plack::Builder

Рассматривать методы не будем, отметим только, что это весьма гибкий маршрутизатор. Например, он позволяет устанавливать обработчик (PSGI- приложение) на локальный адрес:

my $app = builder {
    mount "/" => builder { $my_cool_app; };
};

Результат — обращения по адресу / будут перенаправлены в соответствующее PSGI-приложение. В данном случае это $my_cool_app.

Маршруты могут быть вложенными, например:

my $app = builder {
    mount "/" => builder {
        mount "/another" => builder { $my_another_cool_app; };
        mount "/"        => builder { $my_cool_app; };
    };
};

И эти маршруты могут быть вложенными. В этом примере, все, что не попадает в /another отправляется в /.

Plack::Middleware

Базовый класс для создания middleware-приложений. Middleware это «промежуточное программное обеспечение». Используется тогда, когда нужно модифицировать PSGI-запрос или готовый PSGI-ответ, а также предоставить специфические условия для запуска определенной части приложения.

Перепишем приложение на Plack

use strict;
use Plack;
use Plack::Request;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);

    $res->body('Hello World!');

    return $res->finalize();
};

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

На что надо обратить внимание. $app — ссылка на функцию. Очень часто, когда идет быстрое написание нечто подобного, забывается символ ; после окончания ссылки на функцию или создание Plack::Request без передачи $env. Стоит быть внимательным.

Для проверки синтаксиса можно использовать perl -c app.psgi.

Вот еще один важный момент касательно написания PSGI-приложений: при формировании тела ответа стоит убедиться, что там находятся байты, а не символы (например, UTF-8). Обнаруживается такая ошибка весьма сложно. Ее наличие приводит к пустому ответу сервера с ошибкой в psgi.error:

"Wide character at syswrite"

Запускается наше приложение аналогично предыдущему.

  • $req — это объект типа Plack::Request; $req содержит в себе данные запроса клиента; он получает их из хеша $env, который передается в функцию;
  • $resPlack::Response, это ответ клиенту; строится по запросу при помощи метода new_response, в качестве параметра принимает код ответа (200 в нашем случае);
  • body — устанавливает тело ответа;
  • finalize — преобразование объекта ответа в ссылку на массив PSGI-ответа (который, как было описано выше, состоит из статуса, заголовков и тела ответа).

Да, Hello world это конечно неплохо, но мало функционально. Сейчас, используя весь инструментарий, попробуем написать простейшее приложение (но оно будет гораздо полезнее, правда).

Напишем API, реализующее три функции:

  • первая будет принимать строку в качестве входяшего параметра и говорить о том, что строка успешно принята; адрес для обращения — localhost:8080/;
  • вторая функция будет принимать строку в качестве параметра и возвращать, например, является ли эта строка палиндромом (слово или фраза, которая одинаково выглядит с обеих сторон, например — «Аргентина манит негра»); располагаться будет по адресу localhost:8080/palindrome;
  • третья функция будет принимать в качестве параметра ту же строку и возвращать ее перевернутой; располагаться будет по адресу localhost:8080/reverse.

В результате написания кода у нас должно получиться нечто, умеющее следующие вещи:

  • при обращении на / отвечать что все ок, если передан параметр string;
  • при обращении на /palindrome проверять наличие параметра string, отвечать, является оно палиндромом или нет;
  • при обращении на /reverse отдавать перевернутую строку.

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

$string = scalar reverse $string;

Для определения, является ли строка палиндромом, будем использовать следующую функцию:

sub palindrome {
    my $string = shift;

    $string = lc $string;
    $string =~ s/\s//gs;

    if ($string eq scalar reverse $string) {
        return 1;
    }
    else {
        return 0;
    }
}

Приложение

Plack::Request позволяет получать параметры при помощи метода parameters.

my $params = $req->parameters();

Доработаем приложение и приведем его к виду:

use strict;
use Plack;
use Plack::Request;

my $app = sub {
    my $env    = shift;

    my $req    = Plack::Request->new($env);
    my $res    = $req->new_response(200);
    my $params = $req->parameters();

    my $body;
    if ($params->{string}) {
        $body = 'string exists';
    }
    else {
        $body = 'empty string';
    }

    $res->body($body);

    return $res->finalize();
};

Запускаем. Первая часть готова.

Перейдя по адресу http://localhost:8080/?string=1 мы увидим ответ, который скажет нам о том, что строка есть. Переход же по адресу http://localhost:8080/ вернет нам ошибку.

Остальную логику можно реализовать прямо в этом же приложении, разделяя логику по path_info, которая будет содержать текущий путь. Для справки, разбор path_info может быть реализован следующим образом:

my @path = split '\/', $req->path_info();
shift @path;

И теперь в $path[0] находится необходимый нам путь.

Важно: после внесения изменений в код, сервер необходимо перезапускать!

Plack::Builder

А вот теперь стоит повнимательнее посмотреть на маршрутизатор.

Он дает возможность использовать другие PSGI-приложения в качестве компонентов. Еще очень полезной будет возможность подключать middleware.

Переделаем первое приложение так, чтобы оно использовало маршрутизатор.

use strict;
use Plack;
use Plack::Request;
use Plack::Builder;

my $app = sub {
    my $env = shift;

    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->header('Content-Type' => 'text/html', charset => 'Utf-8');

    my $params = $req->parameters();
    my $body;
    if ($params->{string}) {
        $body = 'string exists';
    }
    else {
        $body = 'empty string';
    }

    $res->body($body);

    return $res->finalize();
};

my $main_app = builder {
    mount "/" => builder { $app; };
};

Теперь $main_app это основное PSGI-приложение. $app присоединяется к нему по адресу /. Кроме того, была добавлена функция для установки заголовков в ответ (через метод header). Стоит сделать важное замечание: в данном приложении для упрощения все функции помещены в один файл. Для более сложных приложений так делать, конечно, не рекомендуется.

Теперь подключим компонент для переворачивания строки в виде приложения, которое будет находиться по адресу http://localhost:8080/reverse.

use strict;
use Plack;
use Plack::Request;
use Plack::Builder;

my $app = sub {
    my $env = shift;

    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->header('Content-Type' => 'text/html', charset => 'Utf-8');

    my $params = $req->parameters();
    my $body;
    if ($params->{string}) {
        $body = 'string exists';
    }
    else {
        $body = 'empty string';
    }

    $res->body($body);

    return $res->finalize();
};

my $reverse_app = sub {
    my $env    = shift;

    my $req    = Plack::Request->new($env);
    my $res    = $req->new_response(200);

    my $params = $req->parameters();
    my $body;
    if ($params->{string}) {
        $body = scalar reverse $params->{string};
    }
    else {
        $body = 'empty string';
    }

    $res->body($body);

    return $res->finalize();
};

my $main_app = builder {
    mount "/reverse" => builder { $reverse_app };
    mount "/"        => builder { $app; };
};

Адрес для проверки — http://localhost:8080/reverse?string=test%20string.

2/3 задачи выполнено. Однако, в данном случае уж очень похожие получились $app и $reverse_app. Проведем небольшой рефакторинг. Сделаем функцию, которая будет возвращать другую функцию (иначе, функцию высшего порядка).

Теперь приложение выглядит так:

use strict;
use Plack;
use Plack::Request;
use Plack::Builder;

sub build_app {
    my $param = shift;

    return sub {
        my $env = shift;

        my $req = Plack::Request->new($env);
        my $res = $req->new_response(200);
        $res->header('Content-Type' => 'text/html', charset => 'Utf-8');

        my $params = $req->parameters();
        my $body;
        if ($params->{string}) {
            if ($param eq 'reverse') {
                $body = scalar reverse $params->{string};
            }
            else {
                $body = 'string exists';
            }
        }
        else {
            $body = 'empty string';
        }

        $res->body($body);

        return $res->finalize();
    };
}

my $main_app = builder {
    mount "/reverse" => builder { build_app('reverse') };
    mount "/"        => builder { build_app() };
};

Так гораздо лучше. Теперь добавим третью и последнюю функцию в наше API и закончим, наконец, приложение. В результате всех доработок получилось приложение вида:

use strict;
use Plack;
use Plack::Request;
use Plack::Builder;

sub build_app {
    my $param = shift;

    return sub {
        my $env = shift;

        my $req = Plack::Request->new($env);
        my $res = $req->new_response(200);
        $res->header('Content-Type' => 'text/html', charset => 'Utf-8');

        my $params = $req->parameters();
        my $body;
        if ($params->{string}) {
            if ($param eq 'reverse') {
                $body = scalar reverse $params->{string};
            }
            elsif ($param eq 'palindrome') {
                $body =
                  palindrome($params->{string})
                  ? 'Palindrome'
                  : 'Not a palindrome';
            }
            else {
                $body = 'string exists';
            }
        }
        else {
            $body = 'empty string';
        }

        $res->body($body);

        return $res->finalize();
    };
}

sub palindrome {
    my $string = shift;

    $string = lc $string;
    $string =~ s/\s//gs;

    if ($string eq scalar reverse $string) {
        return 1;
    }
    else {
        return 0;
    }
}

my $main_app = builder {
    mount "/reverse"    => builder { build_app('reverse') };
    mount "/palindrome" => builder { build_app('palindrome') };
    mount "/"           => builder { build_app() };
};

Ссылка для проверки:

http://localhost:8080/palindrome?string=argentina%20Manit%20negra

В дальнейших статьях будут рассмотрены более углубленные темы: middleware, сессии, cookie, обзор серверов, с примерами для каждого конкретного + небольшие бенчмарки, особенности и тонкости PSGI/Plack, PSGI под нагрузкой, обзор способов разворачивания PSGI-приложений, PSGI-фреймворки, профилирование, Starman + Nginx, запуск CGI-скриптов в PSGI-режиме или «У меня CGI приложение, но я хочу PSGI» и так далее.

Дмитрий Шаматрин


Debug-fu в стиле Perl | Содержание | Обзор CPAN за март 2013 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье