Автор: Дмитрий Шаматрин.
С разрешения автора оригинальных статей цикла я публикую цикл на Хабре.
Оригинальная статья на сайте журнала pragmaticperl.com
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 предоставляет единый интерфейс для работы с различными серверами, что сильно упрощает жизнь.
Безусловно, формат статьи не позволяет описать полностью все нюансы, поэтому здесь и далее будут только ключевые моменты.
На данном этапе это все, что нужно для того, чтобы начать разбираться с кодом непосредственно.
Ниже приведен код простейшего PSGI-приложения.
Сохраняем это приложение в файле app.psgi, или любом другом с расширением psgi. Смотрим на особенности. Потом на код. Потом опять на особенности. Все сходится. Запускаем.
При запуске perl app.psgi он «молча» отрабатывает, но приложение не запущено.
Для того, чтобы запускать PSGI-приложения нам необходим PSGI-сервер. На данный момент серверов несколько.
Все эти сервера доступны на CPAN. В дальнейшем мы будем использовать Starman, затем сменим его на Twiggy, а затем на Feersum. Каждой задаче свой сервер.
Приложение абсолютно одинаково запустится на любом из этих серверов, может быть, под Corona его придется чуть видоизменить. После установки сервера, а в нашем случае это Starman, в /usr/bin или /usr/local/bin должен появиться исполняемый файл starman. Запуск производится следующей командой:
По умолчанию PSGI-серверы используют 5000 порт. Мы можем его изменить, запустив приложение с ключом --port 8080, например. Напомним, что PSGI — спецификация. В данном случае мы использовали эту спецификацию для написания простейшего web-приложения. Очевидно, что для нормальной разработки нам необходимо реализовать и множество вспомогательных функций, от получения GET-параметров до получения данных cookie. Этого всего не было бы без необходимого функционала.
Plack — это реализация PSGI (в Perl есть стандартный модуль Pack, потому реализация получила имя Plack). Plack существенно облегчает нам жизнь, как разработчикам. Он содержит в себе огромное количество функций для работы с $env.
В базовой комплектации Plack состоит из довольно большого количества модулей. На данном этапе нас интересуют только эти:
Plack::Request и Plack::Response возвращают различные значения типа Hash::MultiValue, на которые стоит обратить внимание.
Модуль, автором которого тоже является Tatsuhiko Miyagawa, представляет собой хеш, но с одним нюансом. Он может хранить несколько значений по одному ключу. Например: $hash->get('key') вернет value, если же значений по ключу несколько, то оно вернет последнее, а если нужны все значения, то можно воспользоваться функцией $hash->get_all('key'), тогда результат будет ('value1','value2'). Hash::MultiValue также учитывает контекст вызова, так что будьте внимательны.
Модуль, который содержит функции для работы с запросами клиента. Методов содержит много, всегда можно ознакомиться на CPAN. В рамках этой статьи, дальше, мы будем использовать следующие методы:
Рассматривать методы не будем, отметим только, что это весьма гибкий маршрутизатор. Например, он позволяет устанавливать обработчик (PSGI- приложение) на локальный адрес:
Результат — обращения по адресу / будут перенаправлены в соответствующее PSGI-приложение. В данном случае это $my_cool_app.
Маршруты могут быть вложенными, например:
И эти маршруты могут быть вложенными. В этом примере, все, что не попадает в /another отправляется в /.
Базовый класс для создания middleware-приложений. Middleware это «промежуточное программное обеспечение». Используется тогда, когда нужно модифицировать PSGI-запрос или готовый PSGI-ответ, а также предоставить специфические условия для запуска определенной части приложения.
Это простейшее приложение, использующее Plack. Оно совершенно наглядно демонстрирует принцип его работы.
На что надо обратить внимание. $app — ссылка на функцию. Очень часто, когда идет быстрое написание нечто подобного, забывается символ; после окончания ссылки на функцию или создание Plack::Request без передачи $env. Стоит быть внимательным.
Для проверки синтаксиса можно использовать perl -c app.psgi.
Вот еще один важный момент касательно написания PSGI-приложений: при формировании тела ответа стоит убедиться, что там находятся байты, а не символы (например, UTF-8). Обнаруживается такая ошибка весьма сложно. Ее наличие приводит к пустому ответу сервера с ошибкой в psgi.error:
«Wide character at syswrite»
Запускается наше приложение аналогично предыдущему.
Да, Hello world это конечно неплохо, но мало функционально. Сейчас, используя весь инструментарий, попробуем написать простейшее приложение (но оно будет гораздо полезнее, правда).
Напишем API, реализующее три функции:
В результате написания кода у нас должно получиться нечто, умеющее следующие вещи:
Для переворачивания строки будем использовать следующую конструкцию:
Для определения, является ли строка палиндромом, будем использовать следующую функцию:
Plack::Request позволяет получать параметры при помощи метода parameters.
Доработаем приложение и приведем его к виду:
Запускаем. Первая часть готова.
Перейдя по адресу localhost:8080/?string=1 мы увидим ответ, который скажет нам о том, что строка есть. Переход же по адресу localhost:8080/ вернет нам ошибку.
Остальную логику можно реализовать прямо в этом же приложении, разделяя логику по path_info, которая будет содержать текущий путь. Для справки, разбор path_info может быть реализован следующим образом:
И теперь в $path[0] находится необходимый нам путь.
Важно: после внесения изменений в код, сервер необходимо перезапускать!
А вот теперь стоит повнимательнее посмотреть на маршрутизатор.
Он дает возможность использовать другие PSGI-приложения в качестве компонентов. Еще очень полезной будет возможность подключать middleware.
Переделаем первое приложение так, чтобы оно использовало маршрутизатор.
Теперь $main_app это основное PSGI-приложение. $app присоединяется к нему по адресу /. Кроме того, была добавлена функция для установки заголовков в ответ (через метод header). Стоит сделать важное замечание: в данном приложении для упрощения все функции помещены в один файл. Для более сложных приложений так делать, конечно, не рекомендуется.
Теперь подключим компонент для переворачивания строки в виде приложения, которое будет находиться по адресу localhost:8080/reverse.
Адрес для проверки — localhost:8080/reverse?string=test%20string.
2/3 задачи выполнено. Однако, в данном случае уж очень похожие получились $app и $reverse_app. Проведем небольшой рефакторинг. Сделаем функцию, которая будет возвращать другую функцию (иначе, функцию высшего порядка).
Теперь приложение выглядит так:
Так гораздо лучше. Теперь добавим третью и последнюю функцию в наше API и закончим, наконец, приложение. В результате всех доработок получилось приложение вида:
Ссылка для проверки:
localhost:8080/palindrome?string=argentina%20Manit%20negra
В дальнейших статьях будут рассмотрены более углубленные темы: middleware, сессии, cookie, обзор серверов, с примерами для каждого конкретного + небольшие бенчмарки, особенности и тонкости PSGI/Plack, PSGI под нагрузкой, обзор способов разворачивания PSGI-приложений, PSGI-фреймворки, профилирование, Starman + Nginx, запуск CGI-скриптов в PSGI-режиме или «У меня CGI приложение, но я хочу PSGI» и так далее.
С разрешения автора оригинальных статей цикла я публикую цикл на Хабре.
Оригинальная статья на сайте журнала pragmaticperl.com
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-сервер. На данный момент серверов несколько.
- 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» и так далее.