Мыльная жемчужина Insider (brain_insider@mail.ru) Хакер, номер #099, стр. 130 Что такое SOAP и зачем он бывает нужен Когда проекты так или иначе перерастают масштабы одного сервера, естественным образом возникает задача доступа к удаленному коду. Именно ее призван решать SOAP (Simple Object Access Protocol). Этот протокол появился в результате развития идеи простого построения распределенных проектов, когда возможно прозрачное использование удаленного кода - так, как если бы код подключался локально. Зри в корень В основе SOAP лежит обмен XML-сообщениями (eXtensible Markup Language — расширяемый язык разметки), которые, как известно, представляют собой способ структурирования информации, передаваемой между взаимодействующими машинами. SOAP действительно предоставляет максимально упрощенный способ взаимодействия с удаленным кодом. В идеальном случае использование удаленных классов происходит полностью прозрачно для пользователя. Грубо говоря, повесив на стороне сервера соответствующий демон, который будет передавать запросы целевому классу, и написав класс-оболочку для отправки SOAP-запросов и обработки ответов, мы можем обращаться к методам интересующего нас модуля напрямую, независимо от того, каким образом реально происходит передача данных, как если бы мы подключали этот модуль локально. Работа по преобразованию запроса в XML-сообщение, его передаче и последующему декодированию ответа полностью ложится на конкретную реализацию SOAP. В общем случае программист, указав псевдоним подключаемого класса и его реальное местоположение, должен получать к нему прямой доступ. В этом заключается идея: скрыть подробности получения доступа к запрашиваемому классу. Проиллюстрируем примером. Допустим, у нас есть два сервера с двумя различными проектами. В умах программистов возникла мысль использовать часть функционала одного проекта во втором. В больших проектах перетаскивание части функционала с одного сервера на другой – это зачастую бешеный геморрой с копированием всей иерархии классов или ее фрагмента, проблемы совместимости кода и прочие маленькие повседневные радости рядового программиста… Да и зачем копировать код, если все уже реализовано, нужно только получить удаленный доступ к этим классам? В таком случае на стороне сервера вешается демон, который принимает запросы к определенным классам по псевдонимам, передает управление этим классам и перенаправляет ответ. От клиента требуется только отправка правильных запросов. Генерировать правильные запросы можно вручную, основываясь на каком-либо стандартизированном описании сервиса, либо используя стандартную реализацию SOAP, которая скрывает все детали транспорта данных. Или, к примеру, рассмотрим ситуацию с зло-админом, который написал собственные изощренные скрипты для контроля состояния системы и повесил их на все подконтрольные машины (допустим, что речь идет о каком-то кластере). Можно, конечно, открыть десяток туннелей на все машины и внимательно следить за работой, быстро переключая окошки. Но намного проще сделать простой интерфейс для доступа к необходимым функциям, повесить по SOAP-демону на каждой такой машине и написать один клиент, который бы собирал информацию со всех машин по унифицированному интерфейсу и пихал ее на домашнюю страничку нашего изобретательного администратора. Стоит заметить, что интерфейс может использоваться не только для мониторинга, но и для управления… о, сколько замечательных возможностей здесь открывается для пытливого ума :). В общем, SOAP - это действительно простой способ для удаленного доступа к объектам со всеми вытекающими отсюда возможностями. Это полностью текстовый протокол, поэтому он идеально ложится поверх протоколов прикладного уровня (SMTP, FTP и, конечно же, HTTP). Разумеется, SOAP не лишен и недостатков. Например, для него характерна избыточность трафика, которая является следствием все той же «текстовости», а сам трафик довольно непросто фильтровать на предмет недокументированного или несанкционированного доступа к расшаренным классам. Неправильно организованный SOAP-доступ к функционалу проекта - это одна большая дыра. Представь, если какой-нибудь очень продвинутый веб-программист решит через одного демона расшарить все классы своего проекта и при этом забудет хоть как-то настроить уровни доступа к критическим участкам кода… Красота! Точнее, ужас, конечно ;). SOAP и Perl «Почему именно Perl?» - наверняка, спросит скептически настроенный читатель. Ну, во-первых, потому, что Perl - лучший скриптовый язык и именно на него ложится нагрузка по написанию административных приблуд, то есть в случае с решением задач о распределении административного кода, скорее всего, придется столкнуться именно с Perl'ом. А кроме того, Perl - один из самых распространенных языков web-программирования, и на его примере можно получить представление о том, как пишутся web-сервисы. Но и это еще не все! В Perl уже реализована полновесная и простая поддержка SOAP (радостно вспоминаем про CPAN). Сделано это в наборе пакетов SOAP::Lite. В данном случае «lite» означает легкость использования SOAP, а не ограниченность предоставляемого набора функций. Эти модули дают возможность полностью контролировать все стадии работы SOAP: процесс формирования XML-сообщения (самые отъявленные эстеты могут вручную формировать XML-представление передаваемых данных), выбор способа передачи (протокол прикладного уровня, поверх которого будет происходить передача данных) и различные варианты адресации целевых классов. В общем, возможностей полно, хотя реализованы они не без косяков. Итак, перейдем к делу. Лучший способ разобраться, как что работает, - пощупать своими руками. Не вдаваясь в подробности, напишем что-нибудь рабочее, а потом рассмотрим, как оно функционирует. Уточню, что цель этой статьи - дать не полное описание всех возможностей технологии и ее реализации в Perl (на это потребовался бы объем не одного журнала), а вводное представление о предмете и наводки для дальнейшего изучения. Для начала представим, что у нас есть мегаполезный класс, который мы очень хотим сделать доступным извне (этот скрипт станет зародышем возможного класса мониторинга системы): На всякий пожарный поясню: этот класс выполняет ls -l (листинг содержимого каталога) в текущем каталоге и возвращает ссылку на массив, содержащий результат работы команды. Итак, мы хотим заюзать этот класс удаленно, чтобы всегда иметь возможность бдительно следить за тем, что же творится у нас в нашем подконтрольном каталоге. Для этого мы должны запустить на серверной машине демона, который бы перенаправлял SOAP-запросы на целевой класс. Попробуем разобраться, что же у нас тут происходит. Шестая строчка нужна нам для того, чтобы нашего демона ничто не отвлекало от работы ;). Седьмая и восьмая перенаправляют вывод в error.log. Строки 9-14, собственно, создают SOAP-демона, который будет слушать порт 6660 на предмет появления запросов. Принимать наш демон будет только запросы, посланные на URI http://localhost/MyClass, которые будут перенаправлены классу MyClass. Этим заведует процедура dispatch_with, задающая соответствие между псевдонимом (URI) и реальным классом, которому будет передано управление. Следует запомнить, что в данном случае URI - только псевдоним, а реальное местоположение SOAP-сервера (в нашем случае реализованного в виде HTTP-демона) задается в клиенте методом proxy. Строка «$daemon->handle» стартует демона. Пока оставим подробности и просто запустим получившийся скрипт: >./demon.pl & Итак, демон заработал и, очевидно, трепетно ждет наших запросов. Как их ему отправить? Для этого напишем следующий простой скрипт (сокращённый вариант скрипта представлен на Листинге 3, полный вариант смотри на диске). Что делает этот скрипт? Основной действующий кусок кода здесь - это функция-оболочка sendRequest, которая, создав объект SOAP::Lite с параметрами uri и proxy, вызывает метод $func. Собственно, это все, что необходимо для отправки SOAP-запросов. Вообще говоря, чтобы сделать все красиво, ничего не мешает нам написать прокси-класс, который внешне содержит все те же методы, что и целевой класс, но дергает функцию SendRequest с соответствующими параметрами и возвращает результат. Это создаст иллюзию выполнения локального кода. Итак, запустив наш скрипт, мы увидим список файлов в текущем каталоге, чего, собственно, нам и хотелось. Бдительный читатель может спросить меня: «А зачем это нам функция log? Может быть, для развлечения Форба, который, как и любой другой взломщик, любит читать логи?» Так вот функция log представляет собой процедуру-обработчик отладочного вывода и складывает в файл log.log все trace-сообщения, проходящие через клиент. Эта фича включается строкой «use SOAP::Lite +trace => [ transport=> \&log ];». Давай посмотрим на листинг 4. Здесь мы видим HTTP-запрос и HTTP-ответ, в полях данных которых содержатся xml-сообщения запроса и ответа. Почему HTTP? А потому, что в нашем примере представлен именно HTTP-демон, который висит на выделенном порту. Собственно, ничего особо интересного в этих сообщениях нет, и при должном знании XML они легко читаются. Но даже если XML нам не знаком, иметь представление о том, как именно передаются данные, надо. Тело xml-сообщения состоит из тэгов <soap:Envelope> и <soap:Body>, в которые вложены тэги запроса-ответа и структуры данных, передаваемые между клиентом и сервером. Их интерпретацией занимаются соответствующие модули из набора SOAP::Lite, но, вообще говоря, формат сообщения можно полностью переписать, встроив собственные сериализатор и десериализатор. Существует возможность менять форму xml-представления отдельных классов, но необходимость ее использования возникает не так часто. В соответствии с основной идеей SOAP, мы вполне вправе ожидать на стороне клиента те структуры данных, которые были возвращены целевым классом, на который была передана обработка. Правда, с этим связан и ряд сложностей. Например, при передаче посредством SOAP объектов классов, на выходе клиента будет хэш с данными, а не объект класса (хотя в Perl эта разница не сразу и не всем ясна :)). Ответственность за возврат данных ожидаемого типа лежит на прокси-классе, который должен создать соответствующие объекты и скопировать данные из полученных хешей. Как бы то ни было, это самый безопасный способ получить то, что ожидаешь. Далее, в коде sendRequest строки 23-32 нужны, если в качестве ответа вернется массив, ибо метод result в таком случае возвращает только первый элемент этого массива, а paramsout - остальные его элементы (можно использовать метод paramsall, который возвращает весь массив сразу). Поэтому чтобы избежать всего этого мутного геморроя, лучше передавать ссылки на объекты и получать результат всегда методом result(). Из трейс-лога видно, что в заголовках запроса и ответа по умолчанию для контента указана кодировка utf-8, хотя сами данные передаются в той кодировке, в которой поступают от целевого класса. На это стоит обратить особое внимание и, в случае необходимости, вручную перекодировать данные, либо поменять значение атрибута кодировки, так как подобная лажа может вызвать серьезные недоразумения во взаимодействии сервисов. Вот мы и рассмотрели простейший пример того, как работает SOAP::Lite. Фактически, этого достаточно для реализации многих типовых задач. Полное описание всего набора модулей SOAP::Lite со всеми подробностями легко можно отыскать на search.cpan.org (краткое описание основных пакетов есть на врезке). Здесь же мы попытаемся в общем и целом разобраться в том, что это вообще такое, как оно пашет и на что следует обратить внимание в начале работы. Как все это работает Для начала рассмотрим, что происходит на стороне клиента. Как можно было заметить, в нашем клиентском коде мы нигде в явном виде не создавали объекта клиентской части (смотри врезку), равно как и объекта сериализации и т.д. Дело в том, что все эти сущности создаются автоматически, во время доступа к серверной части. Это и подобные действия скрыты в классе SOAP::Lite, который реализует всю логику работы SOAP и предоставляет набор методов для прямого доступа ко всем создаваемым промежуточным объектам, если это необходимо. Также предоставляется набор методов для быстрого обращения к функциям вспомогательных классов. Вот самые необходимые из них: serializer - доступ к классу SOAP::Serializer. Вообще говоря, обращаться к нему есть смысл, только если нас не устраивает то, как SOAP::Lite представляет структуры данных в xml. В этом случае сериализатор можно переопределить. uri - метод, который фактически является псевдонимом serializer->uri. Напомню, что URI - это псевдоним класса, который обязательно должен быть задан. Именно URI определяет, на какой класс будет передано управление на серверной стороне. proxy - задает фактическое месторасположение класса, к которому мы обращаемся. transport - обеспечивает доступ к классу SOAP::Transport. namespace - задает базовое пространство имен xml-сообщения. encoding - задает атрибут encoding в xml-сообщении, который по умолчанию равен «UTF-8». После создания объекта SOAP::Lite мы сможем отправлять запрос напрямую, вызвав интересующий нас метод как метод объекта SOAP::Lite (в примере так и сделано), либо с помощью метода call, который предоставляет простой доступ к формированию некоторых частей xml-сообщения. Например, можно использовать такой вызов: SOAP::Lite->uri(...)->proxy(...)->call(prefix:metod => @params); Мы получим тэг метода с заданным префиксом: <prefix:metod><\prefix:metod>. После вызова удаленного метода и получения ответа, он разбирается модулем SOAP::Deserializer в объект класса SOAP::SOM. Собственно, SOAP::SOM - это и есть «объект ответа SOAP», к некоторым полям которого мы и получаем доступ в нашей функции sendRequest посредством методов result и paramsout. Этот объект позволяет получить доступ к значениям и атрибутам конкретных тэгов xml-сообщения, однако на практике это используется редко. Более подробная информация по классу SOAP::SOM есть на CPAN. Итак, что происходит на стороне клиента, более или менее понятно. А что происходит на стороне сервера? На стороне сервера все еще проще, так как мы просто создаем объект нужного нам транспортного класса и задаем ему правила перенаправления с помощью метода dispatch_with(uri => 'имя класса', uri2 => 'какой-нибудь другой класс'). Помимо этого метода, есть также dispatch_to(путь, имя класса), который жестко привязывает все запросы к какому-то одному пакету. Способов транспортировки, как видно на врезке, существует немереное количество, и есть из чего выбрать, основываясь на специфике решаемой в данный момент задачи. Миссия завершена Итак, мы рассмотрели основные возможности библиотеки SOAP::Lite, реализующей работу SOAP в Perl. На самом деле, их у нее намного больше. Вспомнить хотя бы возможность переписывать сериализатор, о которой я уже упоминал. Также можно использовать аутентификацию или организовывать доступ через SSL, формировать данные запроса напрямую в xml с помощью все того же метода SOAP::Lite->call и многое другое. Однако для абсолютного большинства практических задач вполне хватает и того набора базовых возможностей, которые я попытался описать в статье. Материала в Сети по данной теме полно - изучай, пробуй и находи то, что может быть полезно конкретно тебе! #!/usr/bin/perl -w use strict; package MyClass; sub new { my $class = shift; my $self = {}; bless $self, $class; return $self; } sub metod { open FD, 'ls -l|'; my @str = <FD>; return \@str; } 1; #!/usr/bin/perl -w use strict; use lib ('/home/insider/soaptest'); use SOAP::Transport::HTTP; use SOAP::Lite; $SIG{PIPE} = $SIG{INT} = 'IGNORE'; open (STDOUT, ">>/home/insider/soaptest/data/error.log") || open (STDOUT, ">/dev/null"); open (STDERR, ">>/home/insider/soaptest/data/error.log") || open (STDERR, ">/dev/null"); my $daemon = SOAP::Transport::HTTP::Daemon -> new (LocalPort => 6660) -> dispatch_with({ 'http://localhost/MyClass' => 'MyClass', }) ; print "Contact to SOAP server at ", $daemon->url, "\n"; $daemon->handle; 1; #!/usr/bin/perl -w ... use SOAP::Lite +trace => [ transport=> \&log ]; use SOAP::Transport::HTTP; my $res = sendRequest('metod', '1')."\n"; ... sub sendRequest { my ($func, $params) = @_; my $proxy = "http://$host:$port/"; my $soap = SOAP::Lite -> uri($uri) -> proxy($proxy); if (!$soap || $@) { die ("SOAP creation error"); } my $res = $soap->$func($params); my $out = $res->result(); my @resout = $res->paramsout(); ... } sub log { open LOG, ">>$logpath/log.log"; print LOG $_[0]->as_string . "\n"; close LOG; } 1; POST http://localhost:6660/ HTTP/1.1 Accept: text/xml Accept: application/soap Content-Type: text/xml; charset=utf-8 SOAPAction: "http://localhost/MyClass#metod" ... <?xml version="1.0" encoding="UTF-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><metod xmlns="http://localhost/MyClass"><c-gensym3 xsi:type="xsd:int">1</c-gensym3></metod></soap:Body></soap:Envelope> HTTP/1.1 200 OK ... SOAPServer: SOAP::Lite/Perl/0.69 <?xml version="1.0" encoding="UTF-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><metodResponse xmlns="http://localhost/MyClass"><soapenc:Array soapenc:arrayType="xsd:string[8]" xsi:type="soapenc:Array"><item xsi:type="xsd:string">total 12 </item><item xsi:type="xsd:string">-rw-r--r-- 1 ..................... 124 Feb 4 19:49 LocalUse.pl </item><item xsi:type="xsd:string"> ... </item></soapenc:Array></metodResponse></soap:Body></soap:Envelope> Модули библиотеки Библиотека SOAP::Lite состоит из следующих основных модулей: SOAP::Lite - основной пакет, реализующий логику работы SOAP; SOAP::Transport - пакет, ответственный за транспортную часть; SOAP::Data - класс, предоставляющий вспомогательный функционал для сериализации данных; SOAP::Header - аналогичен SOAP::Data, но данные записываются не в тело, а в заголовок xml-сообщения (<soap:Header><\soap:Header>); SOAP::Parser - разбор xml-сообщения; SOAP::Serializer - сериализация данных; SOAP::Deserializer - десериализация результатов работы Parser'a; SOAP::SOM – объект, получаемый на выходе SOAP::Deserializer; SOAP::Trace - отладочные сообщения. SOAP::Transport::HTTP.pm SOAP::Transport::HTTP::Client - клиентский интерфейс HTTP; SOAP::Transport::HTTP::Server - серверный интерфейс HTTP; SOAP::Transport::HTTP::CGI – CGI-реализация серверной части; SOAP::Transport::HTTP::Daemon - демон-реализация серверной части; SOAP::Transport::HTTP::Apache - mod_perl реализация серверной части. SOAP::Transport::POP3.pm SOAP::Transport::POP3::Server - серверная часть для передачи поверх POP3-протокола. SOAP::Transport::MAILTO.pm SOAP::Transport::MAILTO::Client - клиентский интерфейс для SMTP/sendmail. SOAP::Transport::LOCAL.pm SOAP::Transport::LOCAL::Client - клиентский интерфейс для локального использования SOAP. SOAP::Transport::TCP.pm SOAP::Transport::TCP::Server - серверная часть для передачи через TCP-сокеты; SOAP::Transport::TCP::Client - клиентская часть для передачи через TCP-сокеты. SOAP::Transport::IO.pm SOAP::Transport::IO::Server - серверная часть для передачи через IO. Более подробную информацию ищи на search.cpan.org (SOAP::Lite). INFO Если повезет, в следующем «Кодинге» мы продолжим и завершим тему программирования SOAP. DVD Все исходники к статье ты найдешь на нашем супергигантском DVD. |