Введение в разработку 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-сервер. На данный момент серверов несколько.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» и так далее.