SOAP - это просто

Как ни странно, но SOAP (Simple Object Access Protocol, кросс-платформенная, кросс-языковая технология запуска объектов) - это действительно просто, хотя когда я только начинал с ним работать на Delphi, никак не мог понять с какой стороны к нему подступиться. В действительности, при проектировании SOAP приложений необходимо выполнять совсем немного условий, после чего все будет прекрасно работать, и эти простые условия я и постараюсь тут рассмотреть.

Основное условие при программировании SOAP: сервер должен быть stateless, т.е. результат выполнения запроса не должен зависеть от предыдущих команд, полученных сервером. Это означает, что все параметры сессии должны храниться на клиенте и передаваться серверу в составе запроса (если необходимо). Этим обеспечивается высокая устойчивость и масштабируемость системы, хотя ряд вкусностей обычной двухзвенки становится недоступным:

  • нельзя явно управлять транзакциями с клиента (этим занимается сервер),
  • соответственно нельзя заблокировать запись на время редактирования (разве что сделать специальные поля-флаги в БД),
  • нельзя одной командой передать параметры, а другой считать результат - все должно происходить в рамках одной команды
  • нельзя работать с классической связкой мастер-деталь (однако TClientDataset предоставляет для этого великолепное средство: nested datasets - вложенные таблицы),
  • нельзя использовать свойство ClientDataSet.PacketRecords>0, т.к. сервер "не помнит", что он уже передал на клиента, а что - нет, подобную функциональность приходится реализовывать при помощи дополнительных параметров запроса,
  • ...если чего забыл - допишу позже

Замечание: чтобы запустить SOAP приложение, прежде всего нужен Web-сервер, если его у вас нет - дальше можете не читать. Для отладки можно воспользоваться WebAppDebugger из меню Tools, тогда серверную часть надо создавать как WebAppDebugger Executable (см.ниже, о работе с WebAppDebugger - см. документацию к Delphi)
Примечание от Ivan Babikov: можно найти в каталоге Indy IdHTTPWebBrokerBridge.pas, научиться делать SOAP executable и читать дальше ;)

Простое SOAP-приложение

Подобные примеры рассмотрены в любой литературе, посвященной разработке SOAP на Delphi.
Запустите Delphi и выберите в меню File | New | Other..., перейдите на закладку WebServices и выберите Soap Server Application.

Вам будет предложено на выбор 5 вариантов:

  • ISAPI/NSAPI Dinamic Link Libarry - подключаемая библиотека для серверов IIS/Netscape, каждый запрос передается как структура и обрабатывается отдельным тредом,
  • CGI Stand-alone Executable - консольное приложение, получает запрос на стандартный вход, возвращает ответ на стандартный выход, каждый запрос обрабатывается отдельным экземпляром приложения,
  • Win-CGI Stand-alone Executable - приложение Windows, обмен данными происходит через INI-файл (не рекомендуется к использованию, как устаревшее),
  • Apache Shared Module (DLL) - подключаемая библиотека для сервера Apache, каждый запрос передается как структура и обрабатывается отдельным тредом,
  • WebAppDebugger Executable - подключаемая библиотека для отладочного сервера, поставляемого в составе Delphi, поскольку WebAppDebugger является также COM сервером, необходимо указать (произвольное) CoClass Name для COM объекта, с помощью которого будет вызываться ваш веб-модуль.

Выберите CGI Stand-alone Executable, как наиболее простой для отладки формат, потом приложение можно будет легко преобразовать в любой другой. Вся хитрость в том, что если вся логика приложения сосредоточена в написанных Вами модулях, то Вы просто создаете новое приложение нужного типа, подключаете к ниму свои модули, и оно работает!

После того, как Вы нажмете ОК, будет сгенерировано новое приложение, содержащее WebModule с тремя компонентами:

  • THTTPSoapDispatcher - получает входящие SOAP пакеты и передает их компоненту, определенному его Dispatcher property (обычно THTTPSoapPascalInvoker),
  • THTTPSoapPascalInvoker - получает входящий SOAP запрос, находит в Invocation Registry вызываемый метод, выполняет (invokes) его, формирует ответ и передает его обрабно THTTPSoapDispatcher,
  • TWSDLHTMLPublish - формирует WSDL (Web Services Description Language), описание данных и интерфейсов, поддерживаемых Вашим модулем.

Сохраните созданное приложение, это будет скелет нашего сервера.


Займемся наполнением его логикой. Поскольку и серверу и клиенту потребуются описания структур передаваемых данных и интерфейсов, то лучше их вынести в отдельный модуль, а всю серверную реализацию - в другой. Для этого создайте два модуля (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, которые он создал, а также полученных в результате вызова метода."

Передача Dataset-а

Здесь все совсем просто. Находясь в проекте Soap Server Application, выберите в меню File | New | Other..., перейдите на закладку WebServices и выберите Soap Server Data Module. Дальнейшее не отличается от разработки обычного MIDAS приложения, с двумя особенностями: сервер обязан быть stateless - получил запрос, ответил и забыл (например, CGI модуль в буквальном смысле завершается после каждого вызова), и иметь не более одного SoapDataModule.
Поместите на полученный модуль компоненты доступа к данным (например, TClientDataset), установите у них все необходимые для работы свойства. Поместите TDataSetProvider, соедините его с компонентом доступа к данным.

Скомпиллируйте приложение и положите его туда, где оно может быть запущено Web-сервером (почему-то мне не удалось запустить его под WebAppDebugger).

В клиентском приложении поместите на форму TSoapConnection и TClientDataset, в SoapConnection.URL укажите путь к интерфейсу вашего сервера:

http://localhost/cgi-bin/CGIProject1.exe/soap/ISOAPDataMod42
можно использовать конкретный интерфейс SoapDataModule, а можно и более общий - IAppServer. В TClientDataset.RemoteServer укажите на TSoapConnection. Теперь, поставив TClientDataset.Active:=true, получим наши данные на клиента.

Если для отрытия датасета на сервере ему требуются какие-то параметры, то удобно будет вместо установки Active:=true использовать запрос DataRequest. Это выглядит так.
На клиенте:

 procedure TForm1.Button1Click(Sender: TObject);
  begin
    Screen.Cursor:=crSQLWait;
    try
    OrderCDS.Data := OrderCDS.DataRequest(NeedOrderNum);
    finally
    Screen.Cursor:=crDefault;
    end;
  end;

На сервере:
 function TSOAPOrderDM.dspOrderDataRequest(Sender: TObject;
    Input: OleVariant): OleVariant;
  begin
    with (Sender as TDataSetProvider)  do
    begin
      Order_.ParamByName('Num').AsInteger:=Input;
      Order_.Open;
      Result := Data;
    end;
  end;

Если вы изменили данные на клиенте и хотите их сохранить на сервер - вызовите у TClientDataset метод ApplyUpdates, только не устанавливайте у TDataSetProvider.ResolveToDataSet=true. Запросы обновления пусть формирует сам TDataSetProvider, а контроль за формированием этих запросов можно осуществлять с помощью свойств TField.ProviderFlags.

Для любопытных: формат передаваемого пакета данных описан на community.borland.com, однако в реальности он передается в виде бинарного (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-ы с гридами, то мы увидим наши данные - отдельно мастер таблицу и деталь.

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


В качестве дополнительной литературы советую посмотреть:

  1. BizSnap chapter of Kylix Developer's Guide (особенно части 4-6)
  2. InterBase in a Multi-tier World
  3. Проектирование ISAPI приложений для работы с базами данных
  4. ... и конечно же - RTFM, правда с поиском в этих разделах почему-то большие проблемы, но информация в хелпах есть и достаточно подробная,
  5. а также - те демо-приложения, которые идут с Delphi

Пожелания по поводу развития данной статьи приветствуются на: KonstB@newmail.ru.



Опубликовал admin
9 Дек, Вторник 2003г.



Программирование для чайников.