Выпуск 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
, который передается в функцию;$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();
};
Запускаем. Первая часть готова.
Перейдя по адресу 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 г. →