|
<< К списку статей <<На главную страницу |
![]() |
|
Как ни странно, но SOAP (Simple Object Access Protocol, кросс-платформенная, кросс-языковая технология запуска объектов) - это действительно просто, хотя когда я только начинал с ним работать на Delphi, никак не мог понять с какой стороны к нему подступиться. В действительности, при проектировании SOAP приложений необходимо выполнять совсем немного условий, после чего все будет прекрасно работать, и эти простые условия я и постараюсь тут рассмотреть.
Прежде всего, а зачем нужен этот самый SOAP? Основных плюса два: SOAP является public стандартом межпрограммного взаимодействия, и клиенту не надо ничего знать о сервере - ни о его языке, ни о платформе; SOAP интерфейсы являются самодокументирующимися, т.е. сервер обязан предоставить клиенту подробное описание интерфейса, его функций, входных и выходных параметров.
Основное условие при программировании SOAP: сервер должен быть stateless, т.е. результат выполнения запроса не должен зависеть от предыдущих команд, полученных сервером. Это означает, что все параметры сессии должны храниться на клиенте и передаваться серверу в составе запроса (если необходимо). Этим обеспечивается высокая устойчивость и масштабируемость системы (клиент может быть переключен на другой сервер, даже не подозревая об этом), хотя ряд вкусностей обычной двухзвенки становится недоступным:
Замечание: чтобы запустить SOAP приложение,
прежде всего нужен Web-сервер, если его у вас
нет - дальше можете не читать. Для отладки можно
воспользоваться WebAppDebugger из меню Tools, тогда серверную часть
надо создавать как WebAppDebugger Executable (см.ниже, о работе
с WebAppDebugger - см. в документации к Delphi и в конце данной статьи)
Примечание от Ivan Babikov: можно найти в каталоге Indy IdHTTPWebBrokerBridge.pas,
научиться делать SOAP executable и читать дальше 
Подобные примеры рассмотрены в любой литературе, посвященной разработке SOAP
на Delphi.
Запустите Delphi и выберите в меню File | New | Other..., перейдите на закладку
WebServices и выберите Soap Server Application.
Вам будет предложено на выбор 5 вариантов:
Выберите CGI Stand-alone Executable, как наиболее простой для отладки формат, потом приложение можно будет легко преобразовать в любой другой. Вся хитрость в том, что если вся логика приложения сосредоточена в написанных Вами модулях, то Вы просто создаете новое приложение нужного типа, подключаете к ниму свои модули, и оно работает!
После того, как Вы нажмете ОК, будет сгенерировано новое приложение, содержащее WebModule с тремя компонентами:
Сохраните созданное приложение, это будет скелет нашего сервера.
Примечание: если заменить THTTPSoapDispatcher и THTTPSoapPascalInvoker
компонентами, поддерживающими другой транспортный механизм, отличный
от HTTP, то можно заставить работать приложение, например, с обменом
по e-mail.
Странность:
позднее я заметил, что WebModule, создаваемый для WebAppDebugger,
немного отличается от остальных вариантов приложений строками
uses WebReq;
initialization
WebRequestHandler.WebModuleClass := TWebModule2;
в чем тут дело я не разбирался, но без них приложние под WebAppDebugger-ом
не работает.
Займемся наполнением его логикой. Поскольку и серверу и клиенту потребуются описания структур передаваемых данных и интерфейсов, то лучше их вынести в отдельный модуль, а всю серверную реализацию - в другой. Для этого создайте два модуля (File | New | Unit) и сохраните один из них под именем CentimeterInchIntf.pas, а другой - CentimeterInchImpl.pas. Внутри CentimeterInchIntf.pas наберите следующее:
unit CentimeterInchIntf;
interface
uses
Types;
type
ICmInch = interface(IInvokable)
['{C53E42A9-8488-4521-BCB4-60863FF09E83}']
function Cm2Inch(Inch: Double): Double; stdcall;
function Inch2Cm(Cm: Double): Double; stdcall;
end;
implementation
uses
InvokeRegistry;
initialization
InvRegistry.RegisterInterface(TypeInfo(ICmInch));
end.
Таким образом мы определили интерфейс ICmInch, предоставляющий две функции: преобразования сантиметров в дюймы и дюймов в сантиметры, и зарегистрировали его в InvokeRegistry.
Примечание: строку ['{C53E42A9-8488-4521-BCB4-60863FF09E83}'] - GUID нашего сервера, не надо копировать из этого примера, а надо сгенерировать в редакторе Delphi нажатием Ctrl-Shift-G
Разберемся с реализацией. В CentimeterInchImpl.pas определим потомка TInvokableClass, реализующего наш интерфейс ICmInch:
unit CentimeterInchImpl;
interface
uses
CentimeterInchIntf, InvokeRegistry;
type
TCmInch = class(TInvokableClass, ICmInch)
public
function Cm2Inch(Inch: Double): Double; stdcall;
function Inch2Cm(Cm: Double): Double; stdcall;
end;
implementation
const
CmPerInch = 2.54;
function TCmInch.Cm2Inch(Inch: Double): Double;
begin
Result := Inch / CmPerInch
end;
function TCmInch.Inch2Cm(Cm: Double): Double;
begin
Result := Cm * CmPerInch
end;
initialization
InvRegistry.RegisterInvokableClass(TCmInch)
end.
Как видите, в TCmInch мы реализовали обе функции интерфейса ICmInch, и также зарегистрировали наш новый invokable class в InvokeRegistry (вообще, все что будет передаваться по сети надо в нем регистрировать, за исключением скалярных типов).
Скомпилируем наше приложение и поместим полученный EXE-файл в каталог,
откуда Web-сервер может запускать скрипты, например, /cgi-bin/, и обратимся
к нему из браузера:
http://localhost/cgi-bin/MyWebService.exe/wsdl
В случае WebAppDebugger путь будет выглядеть так:
http://localhost:1024/MyWebService.exe/wsdl
Обратите внимание на дополнительный PathInfo в конце нашего URL.
Должно получиться примерно следующее:
WebService Listing
Port Type Namespace URI Documentation WSDL IWSDLPublish urn:WSDLPub-IWSDLPublish WSDL for IWSDLPublish ICmInch urn:CentimeterInchIntf-ICmInch WSDL for ICmInch
Если нажать на ссылку WSDL for ICmInch, то мы получим полное WSDL описание нашего интерфейса.
Создайте новое приложение (обычного типа), укажите в секции uses наш интерфейсный модуль CentimeterInchIntf, поместите на главную форму две кнопки, два поля ввода и компонент THTTPRIO с палитры WebServices.
У компонента HTTPRIO1 в свойстве WSDLLocation укажите путь к WSDL вашего сервиса (например, http://localhost/cgi-bin/MyWebService.exe/wsdl/ICmInch), затем из выпадающего списка у свойства Service выберите ICmInchService, а у Port - ICmInchPort (если выпадающие списки пустые, значит что-то не работает...). После этого в обработчиках кнопок OnClick напишите:
procedure TForm1.btnCm2InchClick(Sender: TObject);
var
Cm: Double;
begin
Cm := StrToFloatDef(edCm.Text,0);
edInch.Text := FloatToStr((HTTPRIO1 as ICmInch).Cm2Inch(Cm))
end;
procedure TForm1.btnInch2CmClick(Sender: TObject);
var
Inch: Double;
begin
Inch := StrToFloatDef(edInch.Text,0);
edCm.Text := FloatToStr((HTTPRIO1 as ICmInch).Inch2Cm(Inch))
end;
Теперь после компиляции и запуска приложения можно преобразовывать
сантиметры в дюймы и наоборот.
Q: А что делать, если SOAP сервер написан кем-то другим и у нас нет
интерфейсного модуля?
A: Тогда надо воспользоваться Web Service Importer,
который находится в меню File | New | Other..., на закладке
WebServices. Этот мастер сгенерирует интерфейсный модуль по WSDL сервиса.
Наш компонент THTTPSOAPPascalInvoker уже знает как пересылать скалярные типы и динамические массивы (последние надо предварительно зарегистрировать в InvokeRegistry, см. ниже), однако для пересылки сложных типов, таких как static array, interface, record, set или class, необходимо сначала описать их как потомков класса TRemotable, обладающего RunTime Type Information (RTTI). Например, если мы хотим объявить класс, возвращающий курс валюты и ее наименование, то наш интерфейсный модуль будет выглядеть так:
unit CurrencyIntf;
interface
uses
InvokeRegistry;
type
TCurrency = class(TRemotable)
private
FExchangeRate: double;
FName: string;
public
property ExchangeRate: Double read FExchangeRate write FExchangeRate;
property Name: string read FName write FName;
end;
TCurrencyArray = array of TCurrency;
implementation
initialization
RemClassRegistry.RegisterXSClass(TCurrency);
RemClassRegistry.RegisterXSInfo(TypeInfo(TCurrencyArray));
end.
Здесь мы дополнительно объявили динамический массив TCurrencyArray,
на случай если захотим его передавать (обратите внимание на различие в
командах регистрации класса и массива).
На самом деле полный синтаксис команды регистрации класса несколько шире,
желающие могут прочитать о нем в документации к Delphi:
RemClassRegistry.RegisterXSClass(TXSDateTime, XMLSchemaNameSpace, 'dateTime', True);
Замечание: если имеется тип, который в WSDL документе является скалярным,
но не имеет прямого соответствия в Object Pascal (например, DateTime)
то в качестве базового класса следует использовать TRemotableXS, который
объявляет два метода XSToNative и NativeToXS для преобразования
строкового представления в Object Pascal и обратно (эти методы надо,
естественно, реализовать).
В составе Delphi поставляется модуль XSBuiltIns, в котором уже
реализовано много полезных функций (однако в версии 6.0 там были ошибки
в обработке даты, если национальные настройки в системе были не английские).
Возникает интересный вопрос с созданием-уничтожением
объектов, передаваемых
в качестве параметров. Вот что об этом говорится в документации к TRemotable:
"Со стороны сервера потомки TRemotable, являющиеся входными параметрами,
автоматически создаются при распаковке (unmarshal) вызова метода,
и автоматически уничтожаются после упаковки (marshal) выходных
параметров для передачи клиенту.
Потомки TRemotable, созданные внутри метода, вызванного через
invokable interface, автоматически уничтожаются после того как
их значение упаковано (marshal) для передачи клиенту.
Клиент, вызывающий invokable interface, отвечает за создание
объектов, используемых как входные параметры и за уничтожение
всех потомков TRemotable, которые он создал, а также
полученных в результате вызова метода."
Здесь все совсем просто. Находясь в проекте Soap Server Application,
выберите в меню File | New | Other..., перейдите на закладку
WebServices и выберите Soap Server Data Module. Дальнейшее не
отличается от разработки обычного MIDAS приложения, с двумя особенностями:
сервер обязан быть stateless - получил запрос, ответил и забыл (например,
CGI модуль в буквальном смысле завершается после каждого вызова),
и иметь не более одного SoapDataModule.
Поместите на полученный модуль компоненты доступа к данным
(например, TClientDataset), установите у них все необходимые для
работы свойства. Поместите TDataSetProvider, соедините его с компонентом
доступа к данным.
Скомпиллируйте приложение и положите его туда, где оно может быть запущено Web-сервером (почему-то мне не удалось запустить его под WebAppDebugger, вероятно я использовал не тот WebModule, см. замечание выше).
В клиентском приложении поместите на форму TSoapConnection и TClientDataset, в SoapConnection.URL укажите путь к интерфейсу вашего сервера:
http://localhost/cgi-bin/CGIProject1.exe/soap/ISOAPDataMod42можно использовать конкретный интерфейс SoapDataModule, а можно и более общий - IAppServer. В TClientDataset.RemoteServer укажите на TSoapConnection. Теперь, поставив TClientDataset.Active:=true, получим наши данные на клиента.
Если для отрытия датасета на сервере ему требуются какие-то параметры,
то удобно будет вместо установки Active:=true использовать запрос DataRequest
(напомню, что для SOAP приложений все параметры должны передаваться
в составе единого запроса, т.е. нельзя сначала установить параметры, а потом
запросить данные, т.к. с большой вероятностью второй запрос уйдет к другому
экземпляру сервера). Это выглядит так.
На клиенте:
procedure TForm1.Button1Click(Sender: TObject);
begin
Screen.Cursor:=crSQLWait;
try
BiolifeCDS.Data := BiolifeCDS.DataRequest(NeedOrderNum);
finally
Screen.Cursor:=crDefault;
end;
end;
На сервере:
function TSOAPDataMod42.dspBiolifeDataRequest(Sender: TObject;
Input: OleVariant): OleVariant;
begin
with (Sender as TDataSetProvider) do
begin
Query1.ParamByName('Num').AsInteger:=Input;
Query1.Open;
Result := Data;
end;
end;
т.е. серверу можно передать любые данные (передаваемый тип - OleVariant) и получить назад данные опять же любого типа, например datapacket, как в приведенном случае.
Если вы изменили данные на клиенте и хотите их сохранить на сервер, то есть несколько способов это сделать. Самый простой - установить TDataSetProvider.ResolveToDataSet:=false и вызвать у TClientDataset метод ApplyUpdates. Запросы обновления пусть формирует сам TDataSetProvider, а контроль (довольно слабый, однако) за формированием этих запросов можно осуществлять с помощью свойств TField.ProviderFlags.
Другой способ: установить TDataSetProvider.ResolveToDataSet:=true, однако в этом случае в событии TDataSetProvider.OnBeforeApplyUpdates придется открыть связанный датасет, так чтобы в него была загружена та запись, которую собираетесь изменять. Зато теперь можно задействовать методы датасета BeforeInsert-BeforePost.
И последний вариант: воспользоваться своим собственным методом, добавленным к интерфейсу, как это ранее было проделано с Cm2Inch, и передавать ему ClientDataset.Delta, или набор инструкций для обновления, или то что подсказывает Ваша фантазия разработчика. Например:
procedure TForm1.SendButtonClick(Sender: TObject);
var
X: ISOAPDataMod42;
begin
Screen.Cursor:=crSQLWait;
try
X:=httprio1 as ISOAPDataMod42;
cdsErrors.Data:=X.SaveChanges(BiolifeCDS.Delta);
if cdsErrors.Active and (cdsErrors.RecordCount>0)
then raise Exception.Create('Ошибка!')
else BiolifeCDS.MergeChangeLog;
finally
Screen.Cursor:=crDefault;
end;
end;
function TSOAPDataMod42.SaveChanges(Input: OleVariant): OleVariant;
var
ErrorCount: integer;
begin
cdsTmp.Data:=Input;
//... здесь какая-нибудь предобработка полученных данных ...
cdsTmp.Data:=dspBiolife.ApplyUpdates(cdsTmp.Data,0,ErrorCount);
if ErrorCount<>0 then begin
//... и тут можно что-то сделать, например Rollback транзакции
end;
Result := cdsTmp.Data;
end;
В данном примере метод SaveChanges принимает датапакет типа Delta и возвращает таблицу ошибок, возникших при обновлении.
Для любопытных:
формат передаваемого пакета данных описан в статье
The Express Way to the Internet, Part 2,
однако в реальности он передается в виде бинарного (base64Binary) пакета
в том же формате, что и файл (*.cds), описания этого формата мне найти не
удалось.
Чтобы посмотреть, как реально выглядят пакеты, передаваемые по сети,
можно воспользоваться
программой tcpTrace,
или логом WebAppDebugger-а.
Тут тоже все просто, но - несколько необычно, поскольку деталь должна передаваться как вложенный в мастер-таблицу датасет, поскольку обычная мастер-деталь связка для TClientDataSet невозможна. Делается это так.
В Soap Server Data Module помещаются датасеты для мастер таблицы и для детали и, как обычно, связываются через TDataSource. Туда же помещается один TDatasetProvider, который связывается с мастер-таблицей. Сервер компилируется и кладется туда, где может быть запущен Web-сервером.
На форму клиента кладется TSoapConnection и два TClientDataSet, у первого из которых (это будет мастер) устанавливаются свойства RemoteServer (указывает на TSoapConnection) и ProviderName (указывает на TDatasetProvider нашего сервера). Далее, у первого датасета создаются persistent поля: выберите Add All Fields в редакторе полей, последним в списке добавленных будет поле типа TDataSetField, имеющее имя нашей деталь-таблицы на сервере.
У второго TClientDataSet (это будет наша деталь) установите единственное свойство: DataSetField (выберите из списка название для TDataSetField первого датасета). Теперь, если соединить наши TClientDataSet-ы с гридами, то мы увидим наши данные - отдельно мастер таблицу и деталь.
Есть альтернативный вариант: мастер и деталь передаются отдельными датасетами, при этом деталь загружается целиком, а для выбора набора записей детали, соответствующих конкретной строке мастера, используется фильтрация на клиенте, однако здесь будут проблемы с синхронным обновлением мастера и детали.
Если вы уже наигрались с WebAppDebugger-ом и CGI, то можете замахнуться на "так сказать, Шекспира", модуль ISAPI/NSAPI.
Вообще, перейти от одного вида приложения к другому (скажем, от WebAppDebugger к CGI) крайне просто. Создаем новое приложение нужного нам типа, добавляем в него все наши модули и новое приложение работает! Если вы конечно не помещали никакого кода в автоматически генерируемые модули, чего лично я не рекомендую делать.
Еще следует помнить об упоминавшихся ранее отличиях WebModule
для WebAppDebugger и CGI, а также о разнице в работе CGI и ISAPI приложений:
в первом случае создается по экземпляру приложения на коннект (т.е. с каждым
приложением работает один юзер), во втором -
приложение одно, но на каждый коннект внутри приложения создается отдельный
поток, что требует аккуратного обращения с общими данными,
а в остальном - никаких проблем
.
Примечание: SOAP сервера, скомпилированные на Delphi 7 (и, вероятно, выше)
чувсвительны к Locale сервера, на котором они работают - именно опираясь на нее они
преобразуют русские буквы из Win1251 в UTF8. Т.е. в национальных настройках
операционной системы сервера необходимо выставить страну "Россия",
иначе вместо русских букв получите крякозюбры...
или добавьте в код инициализации любого модуля строку
SetThreadLocale($419);что заставит ваш модуль использовать именно русскую кодировку по умолчанию.
И еще пара замечаний:
- для нормальной работы SOAP приложений, необходимо
зарегистрировать stdvcl40.dll с помощью tregsvr.exe или regsvr32.exe,
DLL должна лежать в каталоге, прописанном в PATH;
- если вы устанавливаете SOAP клиента на Win2003, то
в Свойствах системы необходимо отключить Data Execution Prevention для вашей
программы, иначе особенности реализации TRIO в Delphi до версии 2005
приводят к Access Violation:
WebAppDebugger - это некий локальный WWW-сервер для отладки, запускается из меню Tools. В отличие от настоящего сервера использует для поиска вашего модуля не его расположение в файловой системе на диске, а его COM имя. Все отличие WebAppDebugger Executable от CGI в том и заключается, что при первом запуске он регистрируется как COM-объект Windows. Т.е. для работы его надо один раз запустить из командной строки. Кстати, если вы всю логику помещаете в свои модули, а не в те что генерит мастер в Delphi, то достаточно сгенерировать приложение нужного типа, перецепить на него модули с вашей логикой и все заработает.
Как со всем этим взлететь. Среди примеров к Delphi идет ServerInfo.exe - его надо найти, скомпилить при необходимости, и один раз запустить. Далее вы сможете его запускать нажав на ссылку на первой закладке в WebAppDebugger. Запустится браузер с веб-страничкой, в которой перечислены зарегистрированные в СОМ web-сервисы. Ищете свой, добираетесь по ссылкам на страничках до его WSDL, и копируете строку из адреса браузера (только отрежьте последнее имя интерфейса, строка должна оканчиваться на wsdl, например http://localhost:8081/URServerDebug.UniversalReports/wsdl) - это и будет то что надо запихнуть в THTTPRIO.wsdl или куда вы его там хотите. Отдельно WSDL файл создавать обычно не нужно - его автоматически выдаcт ваша серверная компонента при обращении по ссылке с окончанием /wsdl. В этом и плюс: описание объекта обновляется вместе с самим объектом.
Localhost - это сетевая TCP ссылка на самого себя, т.е. на ваш же компьютер, но обращение идет через сетевой интерфейс. Ни на какую конкретную директорию или программу она не указывает. По сути, это всего лишь IP адрес = 127.0.0.1
/cgi-bin/ - директория на настоящем WWW сервере, относительно корня документов. Если поставите у себя Apache и покопаетесь в его конфигурационном файле, то многое поймете.
Успехов! Konstantin Beliaev, 2003-2009
В качестве дополнительной литературы советую посмотреть:
Пожелания по поводу развития данной статьи приветствуются на: KonstB@newmail.ru