...

воскресенье, 11 января 2015 г.

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


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий