Автор: Дмитрий Шаматрин.
С разрешения автора оригинальных статей цикла я публикую цикл на Хабре.
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, worldn"]];
};
Сохраняем это приложение в файле app.psgi, или любом другом с расширением psgi. Смотрим на особенности. Потом на код. Потом опять на особенности. Все сходится. Запускаем.
При запуске perl app.psgi он «молча» отрабатывает, но приложение не запущено.
Основные PSGI-серверы
Для того, чтобы запускать PSGI-приложения нам необходим PSGI-сервер. На данный момент серверов несколько.
- Twiggy
- Starman
- 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, который передается в функцию;
- $res — Plack::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();
};
Запускаем. Первая часть готова.
Перейдя по адресу localhost:8080/?string=1 мы увидим ответ, который скажет нам о том, что строка есть. Переход же по адресу 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). Стоит сделать важное замечание: в данном приложении для упрощения все функции помещены в один файл. Для более сложных приложений так делать, конечно, не рекомендуется.
Теперь подключим компонент для переворачивания строки в виде приложения, которое будет находиться по адресу 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; };
};
Адрес для проверки — 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() };
};
Ссылка для проверки:
localhost:8080/palindrome?string=argentina%20Manit%20negra
В дальнейших статьях будут рассмотрены более углубленные темы: middleware, сессии, cookie, обзор серверов, с примерами для каждого конкретного + небольшие бенчмарки, особенности и тонкости PSGI/Plack, PSGI под нагрузкой, обзор способов разворачивания PSGI-приложений, PSGI-фреймворки, профилирование, Starman + Nginx, запуск CGI-скриптов в PSGI-режиме или «У меня CGI приложение, но я хочу PSGI» и так далее.
Автор: inquisitor_ua