В предыдущей статье по этой теме (мартовский ][) мы коротко разобрали основы работы с SOAP на Perl. А сегодня мы немного расширим область нашего интереса и рассмотрим SOAP в контексте реального практического применения.

SOAP редко бывает нужен сам по себе, без сторонних библиотек. Понятное дело, что для более-менее жизненного примера нам потребуется некоторый набор таких библиотек. Итак, коротко опишем то, чем сегодня воспользуемся.

 

Что такое POE и зачем оно нам нужно

POE - это пример использования концепции state-машины на базе конечных автоматов, которая позволяет удобно организовать многопоточность в Perl. Тут следует оговориться, что на самом деле мы получаем псевдомногопоточность за счет конвейерного перераспределения ресурсов между короткими обработчиками событий (об этом ниже), на которые дробится исполняемый код программы. В некоторых случаях это дает существенный прирост скорости, но всегда нужно помнить, что на самом деле POE строго последователен и фактически эмулирует подсистему управления процессами, только на более высоком уровне абстракции и в большем временном масштабе. Однако сегодня мы не будем яростно вгрызаться в особенности этой
библиотеки, поскольку не она является предметом нашего интереса, а лишь коротко рассмотрим те инструменты, которыми воспользуемся в нашем примере. Ну и, как обычно, если вдруг тебе не хватит, добавка на CPAN.

Работает POE очень просто. Огрубляя, можно сказать, что POE обеспечивает создание потоков выполнения, управление ими и обмен сообщениями между ними. Хоть это и не верно по сути, такую картинку вполне можно использовать на первых порах. Вообще же идеология POE основана на сессиях и обработке событий. Все основные внутренние функции, которые реализуют логику управления сессиями, собраны в двух модулях: POE::Kernel и POE::Session. В процессе создания сессии мы определяем обработчики некоторых событий, самыми важными из которых являются: _start и _stop, которые генерируются при порождении сессии и при ее уничтожении. Другими словами, эти события как бы говорят сессии, что она принята на
исполнение ядром. Ядро же манипулирует сессиями и распределяет между ними вычислительные ресурсы, эмулируя, подсистему управления процессами. Когда мы создаем объект POE::Session, мы фактически сообщаем ядру о себе и поручаем ему управление своей сессией. Ядро пихает ее в свою внутреннюю очередь и начинает ей как-то рулить - как именно, нас сейчас не интересует. Различные сессии могут общаться друг с другом, генерируя сообщения и подсовывая их ядру на имя другой сессии. Это как посылка, в поле получателя которой стоит псевдоним другой сессии и за отправку которой отвечает ядро. Каждое такое сообщение будет обработано сессией, если в ней определен обработчик одноименного события (можно
описать обработчик _default, в который будет падать все, что не было подхвачено конкретными именованными обработчиками). Это можно рассматривать как на порождение события или как на обмен управляющими сообщениями. Причем события можно порождать разными особо извращенными способами - с задержкой, по расписанию и т.д. Очень удобно то, что ядро берет на себя все проблемы с организацией асинхронной коммуникации между сессиями. Таким образом, можно считать, что это фреймворк для организации мультисессионных приложений. Вот его-то мы и заюзаем. С точки зрения программирования делается все это не просто, а очень просто, а как конкретно, станет ясно из кода примеров, поэтому сейчас для экономии
места мы не будем на этом останавливаться.

Рассмотрим POE как набор модулей. Он построен по многоуровневой схеме, когда поверх слоя ядра (POE::Kernel и POE::Session) накручиваются слои, которые так или иначе скрывают его работу. Например, есть слой POE::Wheel, который реализует набор стандартных задач средствами POE. Кстати сказать, существует модуль POE::Wheel::Run, который позволяет сделать тру-многопоточную обработку событий посредством отфоркивания детей при сохранении event-сообщения с ними. Можно написать модули, используя которые ты вообще не будешь знать, что все сделано многопоточно и поверх POE. Такие дела.

 

SOAP и POE

Библиотеки для реализации SOAP посредством POE, конечно же, уже тоже созданы и есть на CPAN. Что нам дает работа с SOAP через POE? С учетом всего вышесказанного у нас получается эмуляция многопоточной обработки. Совершенно очевидно, что она дает примерно то же, что тру-многопоточность дает веб-серверам, например, всеми любимому Apache, - возможность одновременно обрабатывать несколько запросов. Так, если твой SOAP-демон реализует доступ к какому-то ресурсоемкому или затратному по времени коду (например, доступ к базе и предварительная обработка результатов с последующей передачей клиенту), а количество клиентов растет и всем им нужно обеспечить приемлемое время доступа, то придется
заморачиваться многопоточной обработкой. Так вот POE все берет на себя и действительно существенно ускоряет работу SOAP-демона.

Замечу, что POE::Component::Server::SOAP работает на базе POE::Component::Server::SimpleHTTP, но в библиотеке модулей CPAN есть также модуль POE::Component::Server::SimpleHTTP::PreFork, который, как ясно из названия, обеспечивает действительно многопоточную обработку запросов. Переписать POE::Component::Server::SOAP с поддержкой POE::Component::Server::SimpleHTTP::PreFork, чтобы ускорить работу демона, - задача совсем не сложная. Модуль POE::Component::Server::SOAP использует функции SOAP::Lite, который достаточно подробно обсуждался в прошлой статье, а также реализует некоторую прослойку между приемом запроса и отправкой ответа, давая возможность более гибко диспатчить запросы и
обеспечивая собственно распараллеливание. Эффект распараллеливания достигается за счет того, что прием и обработка сообщения делятся на несколько этапов, каждый из которых обрабатывается в рамках своей сессии. Получается конвейер, и клиентам не нужно ждать, пока отработает предыдущее соединение, чтобы был принят их запрос. Этот модуль гарантирует, что до и после все будет работать так же, как если бы его не было. Сериализатор, десериализатор, передача и прием - все нам уже хорошо знакомо.

Но у модуля есть и свои недостатки. К примеру, если ты пишешь что-то жутко корпоративное, заморочен стандартами и тебе нужен тотальный контроль над NameSpace’ами в оберточных тэгах, то придется помудрить с Serializer'ом, конструктором и генерацией сообщений ответа внутри самого модуля POE::Component::Server::SOAP. Но это тема для отдельной статьи, а сейчас мы воспользуемся всем готовеньким, поскольку в данном случае нам хватит того, что предоставляет модуль POE::Component::Server::SOAP сам по себе.

По идее, этот модуль работает этот модуль работает следующим образом: в самом начале мы порождаем объект сервера, который будет отвечать за отправку ответов на запросы.

POE::Component::Server::SOAP->new(
'ALIAS' => 'MyServer',
'ADDRESS' => 'localhost',
'PORT' => 31337,
'HOSTNAME' => 'MyHost.com',
);

Вообще говоря, несмотря на используемый ООП-style (стиль объектно-ориентированного программирования), там не происходит никакого порождения объекта, а создается сессия с псевдонимом MyServer, которая понимает несколько основных событий: DONE, FAULT, RAWDONE, ADDMETHOD, DELMETHOD. Присвоение псевдонима сессии делает ее неубиваемой для ядра POE, и эта сессия будет висеть как демон, поскольку ядро считает, что именованная сессия нужна для обработки событий, генерируемых другими сессиями (работает в пассивном режиме). Это как раз то, что нам нужно для сервера.

DONE - событие, которое посылается, когда все готово. В качестве аргумента принимает объект SOAP::Response, который автоматически сериализуется, как если бы мы использовали SOAP::Lite.

FAULT - что-то не так. Принимает SOAP::Response, строку кода ответа (наш собственный код, который будет понятен вызывающей стороне), строку ошибки, строку пояснения ошибки и строку, символизирующую объект, вызвавший ошибку. Назначение этих строк условны - туда можно понапихать любой информации, лишь бы нам было весело.

RAWDONE - то же, что и DONE, только данные в SOAP::Response->content не сериализуются.

ADDMETHOD - регистрирует метод, который может быть принят сервером. Принимает следующие аргументы: псевдоним сессии обработки (об этом ниже), имя метода. Фактически тут мы говорим, что при попадании к нашей сессии сервера запроса на выполнение метода, определенного во втором параметре, мы диспатчим ее на сессию, определенную первым параметром.

DELMETHOD - удаляет метод.

Это лишь краткое описание того, что нам потребуется (по традиции отсылаю тебя на CPAN - там круто, его читает даже Форб).

После того как мы создали объект сервера, мы создаем сессию, в которой определяем основные обработчики _start, _stop и обработчики методов, которые зарегистрируем в сессии сервера с помощью события ADDMETHOD. На эту сессию будет передастся управление, когда сессия сервера получит запрос. То есть метод, запрошенный клиентом через SOAP на стороне сервера, будет порождать одноименное событие, которое должно быть адекватно обработано.

POE::Session->create(
'inline_states' => {
'_start' => \&setup_service,
'_stop' => \&shutdown_service,
'_default' => \&dispatch_manager,
},
);

Если мы не знаем, какие запросы будем обрабатывать (например, думаем о возможном расширении), и берем их, к примеру, из какого-нибудь конфига, то лучше всего определить обработчик _default, в который будут сыпаться все запросы.

Ух, с теорией закончили. Я понимаю, что человеку, который сталкивается с этим первый раз, все это кажется весьма мутным и запутанным. На самом деле все очень просто. Можно покурить доки, а потом на свежую голову написать пару приложений на базе сессий - и считай, знания у тебя в кармане (вообще, это универсальный способ во что-то въехать). Есть и другой способ - прочитай книжку по конечным автоматам, и тогда концепция event-driven сессий покажется тебе простой и понятной, однако кто в наше время XML и Web 2.0 заморачивается такими олдскульными вещами? Итак, обратимся к практической части.

 

Что мы хотим сделать

А хотим мы распределение вычислений на базе совсем новых технологий. Мы, как продвинутые парни, будем использовать обмен XML-сообщениями, а не передачу мутной бинарщины в собственном, только что выдуманном формате. Какие преимущества нам это даст, я пока не решил, но какие-то, безусловно, даст (в этом месте я всегда вспоминаю такие слова, как «интеграция» и «совместимость»).

Итак, один сервер и куча клиентов, которые его дергают. Клиент будет уметь ждать конкретное событие на локальной машине. Когда такое событие, например освобождение ресурсов процессора, произойдет, клиент пошлет запрос Главному Серваку и получит от него задания на выполнение. Задания могут быть любыми - на этот код можно повесить какой угодно функционал, поскольку мы рассматриваем общий подход к такого рода задачам. Например, можно заставить ботов DDoS’ить удаленные машины по списку или снифать трафик на предмет чего-то интересного - применений тьма, с минимальными доработками можно сделать традиционный тру-хакерским пример - распределение заданий на перебор паролей (но я тебе этого не
говорил; и вообще, если что, я все эти слова прочитал на заборе и, что они значат, не знаю). Также боты могут быть дернуты по команде. В общем, тут большой простор для фантазии, но поскольку нас сегодня интересует транспорт через SOAP, мы не будем останавливаться на том, что конкретно будет делать наш бот. Бот получит задание и данные и кинет их в класс-заглушку, написать которую ты сможешь самостоятельно.

Кто-то захочет возразить: «На фига все это делать на SOAP?» Конечно, это расточительно с точки зрения ресурсов. Конечно, это не самый рациональный способ написания бота. Такого бота хорошо видно. Однако моя цель - продемонстрировать возможности технологии на каких-либо реальных примерах и тем самым навести тебя на мысли о том, как еще можно использовать эту замечательную штуку - SOAP. SOAP - вещь чрезвычайно гибкая, простая и одновременно мощная. И поверь, написание ботов на его основе - не такая уж безумная идея ;).

 

Код сервера

#!/usr/bin/perl -w
use strict;
use POE;
use POE::Component::Server::SOAP;
use SOAP::Lite; 
use FuncList;
POE::Component::Server::SOAP->new(
'ALIAS' => 'MyServer',
'ADDRESS' => 'localhost',
'PORT' => 31337,
'HOSTNAME' => 'MyHost.com/control',
);
POE::Session->create(
'inline_states' => {
'_start' => \&setup_service,
'_stop' => \&shutdown_service,
'_default' => \&dispatch_manager,
},
);

$poe_kernel->run;
exit 0;
sub setup_service {
my $kernel = $_[KERNEL];
my @fl = @{$FuncList::list}; 
$kernel->alias_set( 'ControlServer' );
foreach(@fl)
{
$kernel->post( 'MyServer', 'ADDMETHOD', 'ControlServer', $_ );
}
}

sub shutdown_service {
my @fl = @{$FuncList::list}; 
foreach(keys @fl)
{
$_[KERNEL]->post( 'MyServer', 'DELMETHOD', 'ControlServer', $_ );
} 
}

sub dispatch_manager {
my $method = $_[ARG0];
my @params = @{$_[ARG1]};
my $response = ${$_[ARG1]}[0];
my $content = '';
if($method eq 'getJob')
{
#тут код выбора задания и запихивания его в $content
}elsif($method eq 'setAnswer')
{
#код обработки ответа
}else
{
#дефолтный код
}
$response->content ($сontent);
$_[KERNEL]->post( 'GeoIP', 'RAWDONE', $response );
}

Итак, что мы видим на врезке «Код сервера»? 6-11 строчки - создание объекта сервера. 13-19 - создание управляющей сессии. 21 строка - запуск ядра POE.

В setup_service мы регистрируем набор функций, которые будем обрабатывать. Пусть этот набор будет задаваться в модуле FuncList. Это даст нам задел расширяемости нашего управляющего сервера и ботов. Если вместо массива list использовать хэш {функция => модуль}, то можно динамически подключать эти модули и передавать им управление. То есть доработка будет заключаться в копировании в папку сервера нового управляющего модуля. Конечно, это касается только серверной части, в смысле управления раздачей заданий.

Чтобы добавить поддержку нового функционала ботам, им необходимо будет отправить этот модуль. Но, вообще говоря, очень легко сделать функцию getUpdates и заставить ботов раз в несколько пусков проверять наличие апдейтов. Если они будут, сервер сам отправит в качестве данных код модуля, а бот легко его проинсталлирует и сможет заюзать в дальнейшем. Все делается просто: папка модулей-обработчиков заданий и динамическое подключение с помощью:

require $file_name;

Задание ботам можно посылать в виде хэша:

{
handler => 'HandlerName', #класс обработчика
data => {}, #пакет данных на обработку
}

Ну ладно, вернемся к разбору сервера. Как видно, во всех хэндлерах доступна служебная переменная $_[KERNEL], которая является ссылкой на ядро, позволяющей вызывать методы ядра, такие как post (бросить исключением в другую сессию). Куда обратиться за более подробной информацией, ты знаешь, а тут я приведу только те сведения, которые необходимы для понимания кода. Параметры, переданные сессии, доступны через $_[ARG0]...$_[ARG9]. В случае обработчика _default параметр $_[ARG0] - имя события, по которому был дернут этот обработчик (так как на него падают все неразобранные события).

$_[ARG1] содержит ссылку на массив параметров, переданных событию. В случае модуля POE::Component::Server::SOAP этим параметром будет объект Server::SOAP::Response, про который нам нужно знать только то, что посредством аксессора content в нем можно задать выходные данные. Можно получить также доступ к информации об IP клиента, к методу, который был вызван, к URI и аргумента запроса через методы $response->connection->remote_ip(), $response->soapmethod(), $response->soaprequest->uri(), $response->soapbody() соответственно. Вот, собственно, и все. Остальной код обработчика dispatch_manager говорит сам за себя (в комментариях не нуждается), и каждый сможет доработать его напильником по вкусу -
каркас есть.

 

Код клиента

#!/usr/bin/perl -w
##код клиент-бота для перебора паролей
use strict;
use POE;
use POE::Wheel::FollowTail;
use SOAP::Transport::HTTP;
use SOAP::Lite;
use Control::Pereborcheg;

my $input_event_name = 'titka';

POE::Session->create(
'inline_states' => {
'_start' => \&start_wheel,
'_stop' => \&stop_wheel,
$input_event_name => \&StartJob,
},
);
sub start_wheel
{
my $file_name = 'iostat -w 5 |';
$_[HEAP]->{wheel} = POE::Wheel::FollowTail->new(
Filename => $file_name, # File to tail
PollInterval => 1, # How often to check it
InputEvent => $input_event_name, # Event to emit upon input
);
$_[KERNEL]->alias_set('wheeler');
}
sub stop_wheel
{
delete $_[HEAP]->{wheel};
}

sub StartJob
{
my ($heap, $line, $wheel) = @_[HEAP, ARG0, ARG1];
my $counter = $heap->{counter}++;
my $cpu = (split /\s+/, $line)[-1];
if($cpu =~ /^\d+$/ && $cpu > 90)
{
my $uri = 'MyHost.com/Control'; #псевдоним
my $host = 'real_host.com:31337'; #реальное местоположение
my $func_request = 'getTask'; #имя метода
my $func_request = 'setAnswer';
#посылаем сообщение о готовности
my $out = SOAP::Lite
-> uri($uri)
-> proxy($host)
-> $func_request($param)
-> result;
my $do = Control::Pereborcheg->new();
$out = $do->process($out);
$out = SOAP::Lite
-> uri($uri)
-> proxy($host)
-> $func_response($out)
-> result;
}
}

$poe_kernel->run();

Перейдем к клиентской части. Сразу обращаем внимание на код из соответствующей врезки, его мы будем изучать. Как мы уже договаривались выше, пусть бот у нас ждет определенного события, например освобождения ресурсов, после чего запускается наш скрипт, посылающий запрос на сервер и принимающий от него задание. Заложим возможность расширения функционала, но сейчас реализуем только одну функцию - перебор паролей. Более конкретно: наш бот ждет падения уровня загрузки CPU ниже, скажем, 10%, после которого отправляет запрос на получение задания, обрабатывает его и сразу отсылает на сервер результат обработки. Как работает сервер, мы разобрались, теперь рассмотрим клиента. Клиентское сообщение с
сервером реализовано в хэндлере StartJob, содержимое которого нам известно из предыдущей статьи. Скажу только, что мы обращаемся к двум методам - getTask и setAnswer, которых должно хватить для обработки чего угодно. Чуть более интересно самое начало этого метода StartJob, в котором мы обрабатываем строку вывода команды iostat. Вывод этой команды, суть которого интуитивно понятна, выглядит примерно так:

[00:00:10 Sat Feb 31] [insider@whitehouse.gov ~]$ iostat -w 1
tty ad0 ad1 ad2 cpu
tin tout KB/t tps MB/s KB/t tps MB/s KB/t tps MB/s us ni sy in id
0 -66 9.21 8 0.07 12.95 3 0.04 4.24 1 0.00 2 0 2 0 96
0 231 0.00 0 0.00 0.00 0 0.00 0.00 0 0.00 0 0 0 0 100
0 78 0.00 0 0.00 0.00 0 0.00 0.00 0 0.00 0 0 0 0 100
...

То есть чтобы отрезать последнюю колонку, мы делаем (split /\s+/, $line)[-1]. А поскольку каждые 20 строк повторяется заголовок «tty...cpu», чтобы его игнорировать, мы используем строку «$cpu =~ /^\d+$/». Тут, вроде, все ясно.

Еще более интересна первая строчка, в которой мы получаем параметр $_[HEAP], представляющий собой уникальную для каждой сессии свалку всякой шняги.

Порядок параметров определяется модулем, порождающим событие, которое перехватывается хэндлером StartJob, - POE::Wheel::FollowTail. Помнишь я говорил о многоуровневой иерархии модулей POE? Вот тут она во всей красе, POE::Wheel - это набор зависимых от родительской сессии модулей для выполнения разных типичных задач. Про этот модуль нам достаточно знать только, что он позволяет следить за пополнением файла и генерит событие каждый раз, когда добавляется строка. Это-то событие мы и перехватываем в StartJob. А следим мы за пополнением «файла», поскольку механизм пайпов великолепно поддерживается перловыми файл-хэндлерами. Собственно, пайпы в свое время и были введены для того, чтобы избавить
администраторов от необходимости задействовать временные файлы в своих скриптах. Поэтому файл в стандартном выводе и пайп - братья-близнецы.

Ну а дальше все ясно. Мы используем модуль-заглушку Control::Pereborcheg. Но тут (в соответствии с тем, о чем говорилось выше) легко можно применить более изощренную технику подключения модулей-обработчиков заданий.

 

Заключение

Итак, в этой статье мы рассмотрели куда более полезный пример использования SOAP, чем представленный в мартовском номере. Весь код работоспособен, однако для реального применения требует некоторой доработки, которую я оставлю на твое усмотрение. Как ты понимаешь, тема обширная, а подробному руководству нет места в рамках журнала. В мои задачи входило продемонстрировать тебе ворох идей и пути их реализации. К слову, я бы сделал возможность отмены заданий и какую-никакую систему распределения нагрузки ;). Ну и заглушки неплохо бы дописать, конечно. В общем, есть куда двигаться. К примеру, если доводка бота покажется тебе слишком простой задачей, можешь попытаться как-нибудь скрыть его от
бдительного ока владельцев машины, если ты, конечно, вдруг вздумаешь использовать ботов не на своих компах (нет, не делай этого!), но это уже тема отдельной статьи... Ладно, что-то я увлекся. Экспериментируй, пробуй свои идеи и не бойся на первых порах забивать гвозди микроскопом. Метод свободного эксперимента никогда не бывает проигрышным - ничего не получится, так хоть развлечешься. Короче, вперед!


Полную версию статьи
читай в сентябрьском номере
Хакера!

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии