Поиск по этому блогу

среда, 22 июня 2011 г.

Delphi. Web службы SOAP

В этой статье.
Небольшая схема работы веб сервисов
Практическая реализация простого приложения
Передача файла с progress bar'ом

Сервисы - это модульные приложения, которые могут быть вызваны и опубликованы в инете.
Работа с ними ведется с помощью протокола SOAP - стандарт легковесного протокола обмена информацией (simple object protocol). Это протокол использует XML для кодирования удаленных вызовов процедур и использует коммуникационный протокол HTTP.

Приложения Web-служб публикуют информацию о том, через какой интерфейс они доступны и как осуществить их вызов с помощью документов WSDL.


Посмотреть на WSDL можно http://МойСайт/Приложение/wsdl
http://127.0.0.1/MyApp.dll/wsdl
или так
http://127.0.0.1:81/SCRIPT/soado.exe/wsdl


WSDL включает
  • объявления классов, прототипы функций
  • объявления вызывающего интерфейса

В разделе implementation содержится описание функций с именем GET<имя службы>, которая возвращает вызываемый интерфейс на клиенте, а при импорте WSDL на клиенте автоматически генерируется глобальная функция, которая возвращает интерфейс

Общий механизм работы можно описать так:
на сервере описан интерфейс с именем mSiteBSoap
    mSiteBSoap = inteface(IInvokable) // вызываемый интерфейс (Invokable)

при импорте wsdl генерируется прототип:
function GetmSiteBSoap(useWSDL: boolean; // если true, то местоположение сервера определяется с помощью WSDL
                Addr: string; // иначе клиент должен предоставить адрес URL
                HTTPRIO)
                    : mSiteBSoap; // возвращает ссылку на вызываемый интерфейс
Обращение клиента
var
    Port: mSiteBSoap
begin
    try
    Port := mSiteBSoap; // получаем ссылку на интерфейс класса
    Port.Login;  // вызываем метод этого класса
    finally
    Port := nil; // корректно удаляем ссылку
    end;
end;

При использовании вызываемого интерфейса автоматически поддерживаются скалярные типы данных: boolean, char, byte, integer, widestring...
При необходимости работать с другими типами данных необходимо выполнить их регистрацию с помощью реестра типов.
initialization
  { Invokable interfaces must be registered }
  InvRegistry.RegisterInterface(TypeInfo(IMulValues));
  InvRegistry.RegisterInterface(Мой класс);

Схема


Модули сервера
THTTPSoapDispatcher - диспетчер, получает SOAP пакеты и передает их компоненту, определенному в его свойстве Dispatcher
(обычно THTTPSoapPascalInvoker)
THTTPSoapPascalInvoker - получает SOAP запрос, находит в реестре (Invocation Registry) вызываемый метод, и выполняет его вызов (invokes)
формирует ответ и передает его обратно диспетчеру THTTPSoapDispatcher

Дополнительно
SOAPAttachment, позволяет обмениваться бинарными данными (binary data)
TSOAPAttachment = class(TRemotable) записывает attachment во временный файл (permanent file) или поток (stream)
Header. Заголовки
сами данные SOAP находятся в теге <SOAP:body>
SOAP:Header тэг позволяет посылать дополнительную информацию, например, куки

Создаем сервер. New - SOAP Server Application - Service Name даем ему имя MulValues
Доп.
свойства WSDLHTMLPublish
PublishOption PoDefault = true AdminEnable = true
В этом случае при запросе wsdl документа (http://127.0.0.1:81/SCRIPT/mulval.exe/wsdl) будет доступна кнопка "Administrator". С помощью встроенного в web модуль администрирования можно создавать несколько серверов физических (указать адрес - порт), которые будут предоставлять услугу. Т.о. можно распределить нагрузку между несколькими серверами.
I. Описание интерфейса. обычно файлы именуются * intf.pas
1. Пишем unit с интерфейсом IMulValues, который должен быть наследником от базового интерфейса IInvokable.
{ Invokable interfaces must derive from IInvokable }
  IMulValues = interface(IInvokable)
  ['{53ADBA92-C6BA-40E7-A075-D5981FCB92C1}'] // это уникальный GUID в D его можно сгенерировать ctrl+Shift+G
    { Methods of Invokable interface must not use the default }
    { calling convention; stdcall is recommended }
// эти методы нам любезно сгенерировала Дельфи
    function echoEnum(const Value: TEnumTest): TEnumTest; stdcall;
    function echoDoubleArray(const Value: TDoubleArray): TDoubleArray; stdcall;
    function echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall;
    function echoDouble(const Value: Double): Double; stdcall;
    // а эти методы мы добавим ручками
    function echoString(const Value: WideString): WideString; stdcall; // получить-вернуть строчку
    function GetAttachmentFile(const Fname: WideString): TSoapAttachment; stdcall; // передать файл
  end;
implementation
initialization
  { Invokable interfaces must be registered }
// а вот эта строка регистрирует наш интерфейс в реестре (Invocation Registry)
// это позволит нам идентифицировать его и обращаться к его методам
// т.е. теперь клиент может обращаться к интерфейсу IMulValues

  InvRegistry.RegisterInterface(TypeInfo(IMulValues));

Доп. Директива компилятора {$M+} - указать в начале модуля, необходима для резервирования памяти для стека, требуемого для обращения к удаленным процедурам. (Delphi 7 Бобровский стр.582). Не пробовала.
II. Теперь приступаем к реализации (Implementation), модули именуются * impl.pas
{ TMulValues }
  TMulValues = class(TInvokableClass, IMulValues)
  public
    function echoEnum(const Value: TEnumTest): TEnumTest; stdcall;
    function echoDoubleArray(const Value: TDoubleArray): TDoubleArray; stdcall;
    function echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall;
    function echoDouble(const Value: Double): Double; stdcall;
    function echoString(const Value: WideString): WideString; stdcall; // вернуть строчку
    function GetAttachmentFile(const Fname: WideString): TSoapAttachment; stdcall;
  end;
...
function TMulValues.echoString(const Value: WideString): WideString;
begin
  Result := Value; // возвращаем строчку
end; 

// возвращаем файл
function TMulValues.GetAttachmentFile(const Fname: WideString): TSoapAttachment;
var
 memStr: TMemoryStream;
begin
 { Загрузить файл}
 Result := TSoapAttachment.Create;
 memStr := TMemoryStream.Create;
 if FileExists(Fname) then
 begin
   memStr.LoadFromFile(Fname);
   Result.SetSourceStream(memStr, soReference);
 end
 else
   Result := nil;
end;

Добавить модуль данных. File - New - SoapServerDataModule
!!! Обратить внимание на
1. stateless - т.е. не хранит информацию, связанную с пользователем
2. при добавлении TDataModule из dpr убрать авто создание модуля, создавать в коде.
Клиентская часть. Обычное приложение.
1. В uses добавляем InvokeRegistry, Rio, SOAPHTTPClient.
2. Добавляем RIO: THTTPRIO; - создает сообщение формата http для вызова удаленного сервиса, для связи с удаленным сервером используется свойство url или WSDLLocation.
прим. RIO - Remote Invokable Object, т.е удаленный вызываемый объект
3. На клиенте импортируем WSDL, при этом будет сгенерирован pas файл с интерфейсами
    File - New - Other вкладка WebServices WSDLImporter
Дальше надо указать url (http://127.0.0.1:81/SCRIPT/mulval.exe/wsdl/IMulValues) или WSDL файл.
Механизм доступа к интерфейсу  port := RIO as Указываем интерфейс, где port наш интерфейс
procedure TForm2.EchoStringClick(Sender: TObject);
var
 port: IMulValues;
 SendStr, ResStr: WideString;
begin
   nRead := 0; // это для Progress Bar'а счетчик
   // указываем url
   RIO.URL := 'http://127.0.0.1:81/SCRIPT/MulVal.exe/soap/IMulValues';
   port := RIO as IMulValues; // доступ к интерфейсу
   SendStr := Memo1.Lines.Text; // строка, которую собираемся передать
   ResStr := port.echoString(SendStr); // передаем строчку, получаем ответ
   Memo2.Lines.Text := ResStr; // пишем результат в memo
end;
// можно файл, можно поток
procedure TForm2.Button2Click(Sender: TObject);
var
 port: IMulValues;
 sAtt: TSoapAttachment;
 memStr: TMemoryStream;
 AttFileName: String;
 AttFileNameWithServerPath: WideString;
begin
 nRead := 0;
 RIO.URL := 'http://127.0.0.1:81/SCRIPT/MulVal.exe/soap/IMulValues';

 port := RIO as IMulValues;
 AttFileName := trim(edFileName.Text); // простой Edit в к-м задаем имя файла
 AttFileNameWithServerPath := 'C:\Inetpub\TEST81\SCRIPT\' + AttFileName;  //
 sAtt := port.GetAttachmentFile(AttFileNameWithServerPath);
 // пишем что получили в поток
 try
  memStr := TMemoryStream.Create;
    try
     sAtt.SaveToStream(memStr);
...
     if memStr.Size > 0 then
     begin
     memStr.Position := 0;
     memStr.SaveToFile(AttFileName); // пишем на диск
     end
     else
     ShowMessage('Файл не найден');
    finally
     memStr.Free;
    end;
 finally
  DeleteFile(sAtt.CacheFile);
  sAtt.Free;
 end;
end;
...
//****************************************************************************
//******** события  RIOHTTP **************************************************
procedure TForm2.RIOAfterExecute(const MethodName: string;
  SOAPResponse: TStream);
begin
 ShowMessage('Готово '); // после загрузки сообщаем, что все готово )
end;

procedure TForm2.RIOHTTPWebNode1ReceivingData(Read, Total: Integer);
var
 CurrPos: integer;
begin
// показываем процесс загрузки
 inc(nRead, Read);
 StatusBar1.Panels[0].Text := Format('Total %d', [Total]);
 ProgressBar1.Position := Trunc((Read/Total)*100);// прогресс загрузки
 Application.ProcessMessages;
end;

Дополнительно
класс TlinkedRIO используется для отладки, методы сервера он вызывает напрямую