Сообщений 4    Оценка 356        Оценить  
Система Orphus

Использование протокола SOAP в распределенных приложениях

Microsoft SOAP Toolkit 3.0

Автор: Иван Андреев
Aelita Software

Источник: RSDN Magazine #1-2003
Опубликовано: 13.06.2003
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Simple Object Access Protocol
SOAP Request
SOAP Response
SOAP Fault
Microsoft SOAP Toolkit 3.0
Состав
WSDL- и WSML-файлы
“Здравствуй, Мир” (SOAP Edition)
Обработка ошибок
Ошибки серверных компонентов
Элемент Detail
Обработка ошибок на клиенте
Серверные компоненты
Активация серверных компонентов
Отладка серверных компонентов
Настройка безопасности (security)
Авторизация
Архитектура прокси SOAPClient
Асинхронные SOAP-вызовы
Пишем SOAP-коннектор
Сложные типы данных и mapper-ы
UDT Mapper
Generic Custom Type Mapper
Custom Type Mapper
DIME и бинарные вложения
Custom Type Mapper 2
Переход от DCOM к SOAP
Генерируем WSDL и WSML
VB-клиент
Proxy для клиента
Изменяем клиента TView
Proxy с ранним связыванием
Все что вы хотели знать о SOAP, но боялись спросить
Заключение
Ссылки

Демонстрационные проекты

Введение

Simple Object Access Protocol (SOAP) – основанный на XML протокол, предназначенный для обмена информацией в распределенных системах. SOAP устанавливает стандарт взаимодействия клиент-сервер и регламентирует, как должен осуществляться вызов, передаваться параметры и возвращаемые значения. Для представления любой информации, передаваемой от клиента к серверу и наоборот, используется XML.

ПРИМЕЧАНИЕ

SOAP не накладывает ограничений на используемый транспорт. Для передачи сообщений могут использоваться любые протоколы и продукты, например, протоколы HTTP, HTTPS, SMTP. Данные могут передаваться через Microsoft Message Queueing, IBM MQ Series и т.д. Однако чаще всего используется протокол HTTP. Microsoft SOAP Toolkit включает в себя только поддержку HTTP.

В распределенных системах SOAP используется для обеспечения взаимодействия разных уровней. На сегодняшний день существует множество технологий и протоколов, позволяющих без труда соединять элементы распределенных систем между собой. Одна из наиболее известных технологий – DCOM, позволяющая эффективно осуществлять RPC-вызовы, передавать и принимать данные, распределять нагрузку между несколькими back-end серверами. Однако у систем, построенных на DCOM, есть очень важный недостаток, затрудняющий взаимодействие уровня представления и уровня бизнес-логики через Internet. Хотя DCOM-приложения могут использовать TCP/IP для передачи вызовов RPC, большинство современных сетевых экранов будут запрещать передачу таких пакетов из соображений безопасности. Конечно, с помощью утилиты DCOMCNFG можно настроить DCOM на использование любого порта в диапазоне от 1024 до 65535. Но при изменении настроек одного из промежуточных файрволлов DCOM может перестать работать. Можно сказать, что DCOM является доминирующей технологией для обмена информацией и передачи вызовов в пределах корпоративной локальной сети, но при выходе за ее пределы DCOM приносит большое количество хлопот, связанных с настройкой портов, файрволлов и т.д.

ПРИМЕЧАНИЕ

Чтобы расширить сферу применения DCOM, Microsoft выпустила COM Internet Services (CIS). CIS использует специальный “туннельный” TCP-протокол, позволяющий DCOM-приложениям осуществлять RPC-вызовы через порт 80. На стороне сервера находится ISAPI-расширение, перехватывающее запросы, идущие на порт 80, и перенаправляющее их дальше для обработки с помощью обычного RPC. Особенностью CIS является то, что только начальное “рукопожатие” клиента с сервером происходит в соответствии с протоколом HTTP, остальной трафик не является HTTP (сделано это, видимо, из соображений эффективности). Поэтому, несмотря на использование 80-го порта, CIS не устраняет проблему сетевых экранов, которые могут запретить передачу “подозрительных” пакетов.

Альтернативой DCOM при построении распределенных систем может служить использование Web-интерфейса на основе ASP. В таких системах от клиента ничего не требуется, кроме наличия браузера, способного соединиться с корпоративным Web-сервером. Для передачи запроса от клиента к серверу можно применить, например, метод POST для HTML-формы, а обработка запроса будет происходить уже на серверной стороне. Несмотря на популярность такого подхода, у него есть недостатки – состав передаваемой информации, а главное, способ ее передачи в методе POST специфичны для конкретного приложения. А если на уровне представления необходим полноценный пользовательский интерфейс Windows-приложения, не миновать проблем.

Как в этом случае может быть построено взаимодействие клиента с сервером? И, наконец, как быть, если необходимо обеспечить поддержку не только HTTP, но, например, SMTP или MSMQ (для асинхронного взаимодействия)? Эти задачи можно решить следующим образом:

Реализуя описанный сценарий, можно спроектировать свой собственный формат передачи параметров и информации о вызове, или использовать SOAP, тем самым снижая затраты на разработку, документирование и сопровождение системы.

Необходимо упомянуть о еще одном достоинстве протокола SOAP – он нейтрален к платформе, т.е. не накладывает ограничений на платформы, которые используются клиентом и сервером. Запрос клиента, работающего под управлением Windows 98, может быть обработан сервером под управлением Unix.

Simple Object Access Protocol

SOAP описывает преобразование в XML следующих элементов вызова:

SOAP Request

Вызов метода по протоколу SOAP преобразуется в XML следующим образом:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?> 
<SOAP-ENV:Envelope ...>
  <SOAP-ENV:Body ...>
<SOAPSDK4:Add ...>
    <x>1</x> 
    <y>2</y> 
  </SOAPSDK4:Add>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

В приведенном примере запроса вызывается метод Add, которому передаются два параметра: 1 и 2. Информация о том, какой метод и у какого объекта необходимо вызвать, передается в заголовке. В случае протокола HTTP заголовок может выглядеть так:

<HTTPHeaders>
  <soapaction>"http://tempuri.org/Sample1/action/Adder.Add"</soapaction>  <content-type>text/xml; charset="UTF-8"</content-type> 
  <user-agent>SOAP Toolkit 3.0</user-agent> 
  <host>ivan:8080</host> 
  <content-length>516</content-length> 
  <connection>Keep-Alive</connection> 
  <cache-control>no-cache</cache-control> 
  <pragma>no-cache</pragma> 
</HTTPHeaders>  

В теге SoapAction указывается, какое действие необходимо выполнить на сервере.

SOAP Response

Ответ сервера содержит значения возвращаемых параметров.

<?xml version="1.0" encoding="UTF-8" standalone="no" ?> 
<SOAP-ENV:Envelope ...>
  <SOAP-ENV:Body ...>
  <SOAPSDK4:AddResponse ...>
    <Result>3</Result> 
  </SOAPSDK4:AddResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

В данном случае сервер ответил на вызов метода Add с параметрами 1 и 2 значением 3.

SOAP Fault

В случае возникновения ошибок до или во время обработки запроса сервер возвращает информацию об ошибке. Ошибки могут быть двух типов:

Пример серверной ошибки:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?> 
<SOAP-ENV:Envelope ...>
  <SOAP-ENV:Body ...>
  <SOAP-ENV:Fault>
    <faultcode>SOAP-ENV:Server</faultcode> 
    <faultstring>Can add no more numbers</faultstring> 
    <faultactor>http://ivan:8080/Sample1/Sample1.ASP</faultactor> 
    <detail>
    <mserror:errorInfo ...>
      <mserror:returnCode>-2147467259 : Unspecified error
      </mserror:returnCode> 
      <mserror:serverErrorInfo>
      <mserror:description>Can add no more numbers</mserror:description> 
      <mserror:source>Sample1.Adder.1</mserror:source> 
      </mserror:serverErrorInfo>
      <mserror:callStack>
      <mserror:callElement>
        <mserror:component>WSDLOperation</mserror:component> 
        <mserror:description>Executing method Add failed
        </mserror:description> 
        <mserror:returnCode>-2147352567 : Exception occurred.
        </mserror:returnCode> 
      </mserror:callElement>
      ...
      </mserror:callStack>
    </mserror:errorInfo>
    </detail>
  </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

В теге faultcode указывается источник ошибки – клиент или сервер. Faultstring содержит строковое описание ошибки. Faultfactor – указывает URL, по которому обращался клиент. Тег detail содержит дополнительную информацию об ошибке (например, call stack).

ПРЕДУПРЕЖДЕНИЕ

Microsoft SOAP Toolkit 3.0, возможно, содержит ошибку, из-за которой теряется HRESULT, полученный после вызова метода, если компонент не установил IErrorInfo. В вышеприведенном примере IErrorInfo был установлен с описанием “Can add no more numbers”.

Microsoft SOAP Toolkit 3.0

Разработку распределенных приложений, использующих протокол SOAP, необязательно начинать с нуля, существует большое количество наборов компонентов, облегчающих процесс разработки и берущих на себя обработку различных этапов вызова и получения отклика от сервера. В этой статье мы будем рассматривать один из таких “наборов” – Microsoft SOAP Toolkit, который в значительной степени облегчает процесс создания серверных компонентов SOAP, обеспечивающих прием и обработку запросов, и предоставляет стандартный Proxy, реализующий динамический IDispatch –интерфейс, что позволяет легко преобразовывать вызовы COM в SOAP-запросы.

Состав

SOAP Toolkit включает в себя следующие части:

WSDL- и WSML-файлы

WEB Service Description Language (WSDL) основан на XML и предназначен для описания тех сервисов, которые поддерживает сервер. Для каждого сервиса в WSDL-файле перечисляется набор операций, поддерживаемых данным сервисом, а также определяется формат сообщения, которого должен придерживаться клиент, чтобы сформировать правильный запрос.

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

Основные разделы WSDL-файла:

<types>
  <schema ...>
  ...
  <complexType  name ='Recordset'>
    ...
  </complexType>
  </schema>
</types>
<message name='TView.GetModules'>
  <part name='processID' type='xsd:int'/>
</message>

<message name='TView.GetModulesResponse'>
  <part name='Result' type='typens:Recordset'/>
</message>
<portType name='TViewSoapPort'>
  ...
  <operation name='GetModules' parameterOrder='processID'>
    <input message='wsdlns:TView.GetModules'/>
    <output message='wsdlns:TView.GetModulesResponse'/>
  </operation>
</portType>
<binding name='TViewSoapBinding' type='wsdlns:TViewSoapPort' >
  ...
  <operation name='ShutdownMachine'>
    <soap:operation .../>
      <input>
        <soap:body ...parts='nFlags'/>
      </input>
      <output>
        <soap:body .../>
      </output>
    </sopa:operation>
  </operation>
</binding>
<service name='TView' >
  <port name='TViewSoapPort' binding='wsdlns:TViewSoapBinding' >
    <soap:address location='http://ivan/TView/TView.ASP'/>
  </port>
</service>

В WSDL-файле могут быть описаны один или несколько сервисов, для каждого из которых задан свой адрес – URL. Каждый сервис включает в себя один или несколько портов. Генератор WSDL-файлов создает один порт для каждого из указанных интерфейсов COM-объекта. Каждый порт включает в себя одну или несколько операций. Если сравнивать порт с интерфейсом, то операцию можно сравнить с методом интерфейса. Каждая операция включает несколько частей. Часть (part) можно сравнить с параметром метода.

WSML-файл описывает связь операций с конкретными методами COM-объектов, которые будут обслуживать запросы. Основные разделы WSML-файла:

<service name='TView'>
  <using PROGID='TView.TView.1' cachable='0' ID='TViewObject' />
<types>
   <type name='Recordset' ... targetPROGID='ADODB.Recordset' iid='{00000535-0000-0010-8000-00aa006d2ea4}'/>
</types>
<port name='TViewSoapPort'>
   <operation name='ShutdownMachine'>
     <execute uses='TViewObject' method='ShutdownMachine' dispID='1'>
      <parameter callIndex='1' name='nFlags' elementName='nFlags' />
    </execute>
   </operation>
</port>

На основе информации, содержащейся в этих файлах, компоненты SOAP Toolkit осуществляют подготовку SOAP-запросов, преобразование и передачу параметров, анализ ответа сервера.

“Здравствуй, Мир” (SOAP Edition)

Рассмотрим создание простейшего приложения, использующего SOAP.

В этом и последующих примерах мы будем использовать следующие программные средства и инструменты:

Для начала нам понадобится небольшой компонент с единственным методом Hello, возвращающим строку “Hello, World”.

Реализация может быть такой:

STDMETHODIMP CHello::HelloWorld(BSTR *pString)
{
  *pString = CComBSTR(L"Hello, world").Detach();
  return S_OK;
}

Теперь необходимо сгенерировать WSDL- и WSML-файлы с помощью генератора, входящего в состав SOAP Toolkit. На первом шаге мастера необходимо выбрать имя для WSDL-файла и указать компонент, на основе библиотеки типов которого будет сгенерирован WSDL.

ПРИМЕЧАНИЕ

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

На следующем шаге необходимо выбрать из библиотеки типов интерфейсы и методы, которые войдут в WSDL-файл.

ПРЕДУПРЕЖДЕНИЕ

Если методы содержат параметры, типы которых не поддерживаются генератором (например, enum), генератор вставит “???????” вместо типа в WSDL-файл. Перед использованием сгенерированного файла придется заменить “????” на один из поддерживаемых типов.

Особенно WSDL-генератор “не любит” параметры с типом IUnknown и IDispatch.

На следующем шаге мастера необходимо указать URL, по которому будут обращаться клиенты, и выбрать тип серверного listener’а – ASP или ISAPI.

СОВЕТ

Указанный в мастере URL будет сохранен в WSDL-файле, поэтому клиент не обязан указывать URL. Ему будет нужно указать только месторасположение WSDL-файла. Поменять URL можно, отредактировав WSDL. Кроме того, клиент при подключении может указать другой URL, тем самым переопределяя значение из WSDL-файла.

Когда файлы сгенерированы, нужно создать виртуальный каталог для IIS, в котором будут размещаться все созданные файлы.

Среди сгенерированных файлов будет ASP-страница (если в мастере был выбран ASP listener). Ниже приведен фрагмент кода этой страницы, сгенерированный мастером:

      Option Explicit
OnErrorResumeNext
Response.ContentType = "text/xml"Dim SoapServer
IfNot Application("Sample1Initialized") Then
  Application.Lock
  IfNot Application("Sample1Initialized") ThenDim WSDLFilePath
  Dim WSMLFilePath
  WSDLFilePath = Server.MapPath("Sample1.wsdl")
  WSMLFilePath = Server.MapPath("Sample1.wsml")
  Set SoapServer = Server.CreateObject("MSSOAP.SoapServer30")
  If Err Then SendFault "Cannot create SoapServer object. " & Err.Description
  SoapServer.Init WSDLFilePath, WSMLFilePath
  If Err Then SendFault "SoapServer.Init failed. " & Err.Description
  Set Application("Sample1Server") = SoapServer
  Application("Sample1Initialized") = TrueEndIf
  Application.UnLock
EndIfSet SoapServer = Application("Sample1Server")
SoapServer.SoapInvoke Request, Response, ""

Как видно из кода, полученный сервером запрос передается специальному компоненту SoapServer30, который обрабатывает запрос и формирует ответ, возвращаемый клиенту.

СОВЕТ

SOAP Toolkit 3.0 устанавливается в режиме “Side By Side”. Чтобы обращаться к новым версиям компонентов Toolkit’а, необходимо использовать имена, оканчивающиеся на 30. Например, по Progid “MSSOAP.SoapServer” будет создана предыдущая версия (2.0) серверного компонента.

ПРИМЕЧАНИЕ

Если в качестве SOAP listener’а выбрать ISAPI, придется добавить ISAPI-расширение к виртуальному каталогу вручную, так как программа установки SOAP Toolkit не делает этого. Добавить ISAPI-расширение к виртуальному каталогу можно с помощью скрипта, поставляемого в составе SOAP Toolkit (Binaries/_svdir.vbs). Подробнее об этом можно прочитать в readme.txt к SOAP Toolkit.

Главное преимущество ASP Listener’а по сравнению с ISAPI заключается в том, что в ASP-страницу можно добавлять свой код, изменяющий XML-запрос (например, добавить информацию, доступную через объект Session) или, например, вести лог запросов. Но ISAPI-расширение работает быстрее, чем ASP Listener.

Чтобы пример заработал, нужно создать клиента, использующего клиентскую Proxy для доступа к серверу. Для этого напишем небольшой VB Script:

      Dim o AsObjectSet o = CreateObject("MSSOAP.SoapClient30")
o.MSSoapInit "http://ivan/Sample1/Sample01.WSDL "
o.helloworld s
MsgBox s

На первый взгляд непонятно, почему в приведенном примере мы вызываем метод серверного компонента “helloworld” у объекта с типом SoapClient30. Дело в том, что объект SOAPClient реализует динамический IDispatch, который позволяет вызывать через позднее связывание любые методы, описание которых будет обнаружено в WSDL-файлах.

СОВЕТ

WSDL-файл может находиться локально по отношению к клиенту или загружаться по протоколу HTTP (как в данном случае). Если WSDL-файл загружается по HTTP, и WEB-сервер требует аутентификации, имя пользователя и пароль необходимо передать через URL, например, http://user:pwd@ivan/Sample1/Sample01.wsdl

Чтобы увидеть “изнутри”, как работает тестовый пример, воспользуемся утилитой трассировки, входящей в состав Toolkit’а. Для того чтобы перехватывать запросы и ответы сервера, надо поменять URL в WSDL-файле, указав там порт 8080. После этого необходимо запустить утилиту трассировки. Вызывая методы по протоколу SOAP, можно будет видеть запросы и ответы сервера. Будьте внимательны – если утилита трассировки не запущена, обращение к порту 8080 приведет к ошибке.


Рисунок 1. Утилита трассировки SOAP вызовов.

Обработка ошибок

Во время вызова сервера по протоколу SOAP могут возникать самые разнообразные ошибки. Ошибки могут возникнуть во время подготовки вызова, во время обработки вызова на сервере или при передаче данных. Условно все ошибки можно классифицировать следующим образом:

ПРИМЕЧАНИЕ

Естественно, возникает вопрос, а что же происходит с кодами возврата серверного компонента, у которых не установлен бит ошибки (например, код S_FALSE)? Ситуация с такими кодами такая же, как и в VB-приложениях. SOAPServer игнорирует эти коды возврата, поэтому лучше избегать их использования или разработать свой механизм передачи таких кодов клиенту, например, через параметр метода.

В случае возникновения перечисленных выше ошибок (кроме клиентских ошибок, когда, например, осуществлялась попытка передать неправильное значение параметра) сервер передает клиенту ответ SOAPFault.

Клиент SOAPClient, получив от сервера SOAPFault, предпринимает следующие действия:

Если ошибка произошла на клиенте, то клиент также устанавливает значения свойств объекта SOAPClient, и формирует информацию об ошибке, доступную через IErrorInfo.

ПРИМЕЧАНИЕ

В описание ошибки, доступное через IErrorInfo, попадает не вся информация о результате вызова метода, а только значение <faultcode> + <faultstring>. Кодом ошибки будет либо код, который вернул метод компонента на сервере, либо стандартный код. Для получения подробной информации об ошибке необходимо проанализировать значения свойств объекта SOAPClient.

Элементам ответа сервера SOAPFault соответствуют одноименные свойства объекта SOAPClient:

Ошибки серверных компонентов

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

ПРИМЕЧАНИЕ

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

Элемент Detail

Как упоминалось выше, в этом элементе находится информация, специфичная для приложения. Спецификация SOAP не накладывает на нее каких бы то ни было ограничений. SOAP Toolkit помещает в <detail> элемент <mserror:errorInfo ...>, содержащий следующие дочерние элементы:

Ниже приведен пример элемента detail в отклике сервера SOAPFault.

<detail>
  <mserror:errorInfo ...>
  <mserror:returnCode>-2147467259 : Unspecified error </mserror:returnCode>
  <mserror:serverErrorInfo>
    <mserror:description>Can add no more numbers</mserror:description> 
    <mserror:source>Sample1.Adder.1</mserror:source> 
  </mserror:serverErrorInfo>
  
  <mserror:callStack>
    <mserror:callElement>
    <mserror:component>WSDLOperation</mserror:component> 
    <mserror:description>Executing method Add failed
    </mserror:description> 
    <mserror:returnCode>-2147352567 : Exception occurred.
    </mserror:returnCode> 
    </mserror:callElement>

    <mserror:callElement>
    <mserror:component>Server</mserror:component> 
    <mserror:description>An unanticipated error occurred during the processing of this request.</mserror:description> 
    <mserror:returnCode>-2147352567 : Exception occurred.
    </mserror:returnCode> 
    </mserror:callElement>
  </mserror:callStack>
  </mserror:errorInfo>
</detail>

Обработка ошибок на клиенте

В большинстве случае на клиенте достаточно IErrorInfo, который генерирует SOAPClient. Однако если запрос не может быть выполнен по не зависящим от серверного компонента причинам (он не может быть создан, запрещен доступ к ресурсу и т.п.), может оказаться полезным обработать дополнительную информацию, которая содержится в свойствах объектах SOAPClient – faultstring, detail.

Серверные компоненты

Не всякий COM-компонент может быть использован для обслуживания SOAP-запросов. В этом разделе будут рассмотрены требования к компонентам, способы их активации, возможности отладки.

Наиболее важное требование к компоненту, рассчитанному на работу с SOAP Toolkit – поддержка интерфейса IDispatch. SOAPServer должен сформировать вызов компонента на основе динамической информации, находящейся в WSML-файле. Чтобы это было возможным, компонент должен поддерживать позднее связывание на основе интерфейса IDispatch.

СОВЕТ

Если для создания серверного компонента используется ATL (как наиболее популярная библиотека для создания COM-компонентов), для поддержки интерфейса IDispatch может использоваться шаблон IDispatchImpl. Однако если существует необходимость вызова по SOAP двух различных дуальных интерфейсов компонента, средствами ATL этого будет сложно добиться – так как ATL позволяет компоненту иметь только один дуальный интерфейс, доступный через IDispatch::Invoke. Пожалуй, наиболее простой в данном случае способ заключается в создании “комбинированного” дуального-интерфейса, который перенаправляет вызовы нужной реализации, и который будет использоваться для SOAP-вызовов, или разделение компонента на два независимых, каждый из которых поддерживает свой дуальный интерфейс.

SOAP Toolkit накладывает ограничение на типы передаваемых параметров. Поскольку интерфейсы серверного компонента должны поддерживать IDispatch, мы ограничены только oleautomation-совместимым типами данных. Но и среди automation-типов есть типы, с которыми могут возникнуть проблемы. Это объектные ссылки (указатели на интерфейсы).

В мире COM/DCOM передача указателя на интерфейс сопровождалась неявным маршалингом, и клиент получал указатель на Proxy, который передавал вызовы серверному компоненту. В случае протокола SOAP передача указателя на интерфейс клиенту должна была бы означать, что клиент сможет делать SOAP-вызовы методов передаваемого объекта, но при этом для такого объекта также должны быть доступны WSDL- и WSML-файлы, и кто-то должен контролировать его время жизни (как это происходит в случае с DCOM).

Передача указателя на интерфейс от клиента серверу означает, что вызовы методов интерфейса будет делать сервер, а сам объект находится на клиенте. Применительно к SOAP это означало бы, что запросы по протоколу HTTP делал бы сервер, а клиент должен был бы стать полноценным SOAP-сервером.

Конечно, все эти проблемы могут быть решены, но в настоящее время SOAP Toolkit не поддерживает передачу объектных ссылок ни от сервера клиенту, ни в обратном направлении.

ПРИМЕЧАНИЕ

SOAP нельзя рассматривать как полную замену DCOM, так как DCOM поддерживает специфические технологии, которые сложно или практически невозможно реализовать для SOAP-вызовов – управление временем жизни объектов (DCOM ping), передача объектных ссылок, COM-события. Поэтому в общем случае для перехода с DCOM на SOAP может потребоваться частичное (или даже полное) изменение архитектуры приложения.

Активация серверных компонентов

Время жизни серверных объектов зависит от того, является компонент обычным COM-компонентом, или зарегистрированным в COM+, от типа сервера – in-process или out-of-process, а также от атрибута cacheable в WSML-файле.

Наиболее быстрыми будут in-process компоненты, не зарегистрированные в COM+, и реализующие FTM. Самыми медленными могут оказаться Apartment-компоненты из out-of-process сервера.

Будет ли объект создаваться для каждого нового запроса, определяется следующими условиями:

ПРИМЕЧАНИЕ

Конечно, SOAP-сервер мог бы заниматься маршалингом, чтобы кэшировать такую ссылку, но, видимо, этого не сделано.

ПРИМЕЧАНИЕ

Как показали эксперименты, SOAP кэширует COM+-компонент, имеющий тип потоковой модели Free, даже если компонент размещен во внепроцессном сервере. В такой ситуации лучше не устанавливать атрибут cacheable, так как Proxy чувствительна к потокам, а вызовы к объекту могут приходить из разных потоков.

ПРЕДУПРЕЖДЕНИЕ

Если для COM+-объекта используется кэширование (атрибут cacheable установлен в 1), а процесс компонента завершился по каким-либо причинам, последующие SOAP-вызовы будут заканчиваться с ошибкой “RPC server is unavailable”. Чтобы исправить ситуацию, необходимо выгрузить IIS-приложение из памяти (кнопка Unload в свойствах виртуального каталога).

Что использовать – COM или COM+, in-process или out-of-process сервер, зависит от решаемой задачи, но выбор должен определяться теми же факторами, которые используются при проектировании COM-объектов, используемых в ASP-страницах.

Отладка серверных компонентов

Способ отладки компонентов зависит от их типа и типа COM-сервера.

Тип компонента/сервера Способ отладки
COM+-компонент Запустить под отладку dllhost.exe, указав в качестве командной строки “/ProcessID:{ApplicationID}”.
In-process COM Необходимо подключить отладчик к процессу dllhost.exe, в который загружается in-process сервер. Чтобы найти нужную копию dllhost.exe, можно использовать утилиты, показывающие какие модули использует каждый процесс, например, TLIST, входящую в состав Support Tools W2K.
Out-of-process COM Запустить процесс на отладку.

Если на сервере выбран ASP как тип SOAP Listener’а, можно добавить в ASP-страницу ведение лога входящих запросов.

Настройка безопасности (security)

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

Настройка аутентификации на сервере осуществляется обычным для IIS-приложений образом – с помощью “Internet Information Services Snap-in”. Для виртуального каталога могут быть установлены следующие методы аутентификации:

В случае, когда клиент пытается осуществить доступ к защищенному ресурсу и не предоставляет информации для аутентификации, выдается ошибка – “Connection failure.:No matching authorization scheme enable on connector.”

Способ передачи информации о клиенте на сервер зависит от используемого “коннектора”. Коннектором называется компонент, используемый клиентом для передачи запросов на сервер и получения откликов. Коннектор по умолчанию требует указания необходимой информации через свойства.

Свойство ConnectorProperty Значение
AuthUser Имя пользователя
AuthPassword Пароль
ProxyServer Имя Proxy-сервера, допускаются следующие значения: пустая строка, имя сервера [:порт], <LOCAL_MACHINE> - настройки Proxy берутся из ветки реестра HKEY_LOCAL_MACHINE, <CURRENT_USER> - настройки Proxy берутся из Internet Explorer’а.
EnableAutoProxy Настройки Proxy определяются автоматически, если в свойстве ProxyServer указано <CURRENT_USER>.
ProxyUser Имя пользователя для Proxy.
ProxyPassword Пароль для Proxy.
SSLClientCertificateName Сертификат пользователя.
UseSSL True означает, что SSL будет использоваться независимо от того, указан в URL протокол http или https.
WinHTTPAuthScheme Можно указать приемлемые способы аутентификации. 0x1 – basic, 0x2 – integrated, 0x8 – digest, 0x10 – выбирается совместно с сервером. Значения допустимо объединять по or.

Рассмотрим, как модифицировать код клиента для доступа к защищенному ресурсу. Для этого необходимо запретить анонимный доступ к виртуальному каталогу, в котором находятся файлы серверной части, и установить basic-аутентификацию.

Вот измененный код клиента:

      Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "D:\Projects\SOAP\Sample1\IIS\Sample1.WSDL"
o.ConnectorProperty("AuthUser") = "Administrator"
o.ConnectorProperty("AuthPassword") = "admin"
o.ConnectorProperty("WinHTTPAuthScheme") = 1
Dim l AsLong
l = o.Add(2, 2)
ПРЕДУПРЕЖДЕНИЕ

В данном примере мы явно указали, что подходит только Basic-аутентификация. По умолчанию значение WinHTTPAuthScheme – 0x12, не позволяющее использовать Basic-аутентификацию. Если для виртуального каталога IIS одновременно указаны и Basic-, и Integrated-аутентификация, то при подключении с Basic-аутентификацией могут возникнуть проблемы, если значение WinHTTPAuthScheme допускает Integrated-аутентификацию, и при этом явно указаны имя пользователя и пароль. Суть проблемы будет заключаться в том, что клиент не сможет договориться с сервером об используемой схеме аутентификации и все закончится ошибкой “Access Denied”, несмотря на то, что указаны правильные имя и пароль. Скорее всего, это ошибка в реализации коннектора (эта ошибка наблюдалась и в SOAP Toolkit 2.0).

Если мы хотим использовать защищенное SSL-соединение, необходимо поменять URL, хранящийся в WSDL-файле. Протокол http должен быть заменен на https, иначе подключиться не получится. Кроме того, при подключении с использованием SSL необходимо обратить внимание на имя серверного сертификата для IIS. Этот сертификат задается с помощью средств администрирования IIS и предназначен для подтверждения подлинности Web-сервера (если не задать сертификат для Web-сервера, не получится “включить” поддержку защищенного HTTPS-протокола для виртуальных каталогов).

Если имя серверного сертификата не совпадает с именем Web-сервера, подключение также будет неудачным (Internet Explorer в такой ситуации показывает предупреждение).

СОВЕТ

Будьте внимательны при подключении с использованием SSL. Например, подключиться к https://localhost не выйдет, за исключением случая, когда серверный сертификат называется “localhost”

В некоторых случаях желательно было бы использовать стандартный пользовательский интерфейс (например, используемый Internet Explorer при подключении к защищенным ресурсам) для запроса информации об имени пользователя и выбора сертификата для аутентификации.


Рисунок 2. Пример стандартного UI для запроса имени пользователя и пароля

Однако стандартный коннектор, входящий в состав SOAP Toolkit 3.0, не позволяет сделать этого. Единственный способ указать информацию для аутентификации – передать ее явно через свойства коннектора. В следующих разделах мы рассмотрим альтернативную реализацию коннектора, которая сможет помочь в решении и этой проблемы.

Авторизация

В серверных COM-объектах будет производиться проверка прав доступа того пользователя, от имени которого поступил запрос. Если IIS настроен на анонимный доступ, вызов метода объекта будет осуществляться от имени анонимного пользователя IUSR_ИМЯ_КОМПЬЮТЕРА, если же на Web-сервере настроена аутентификация, вызов метода производится от имени аутентифицированного пользователя. Для COM+-объектов и out-of-process серверов, настроенных на работу под определенной учетной записью, придется использовать имперсонацию.

Архитектура прокси SOAPClient

SOAPClient, используемый для вызовов удаленного компонента по протоколу SOAP, является оберткой вокруг нескольких низкоуровневых компонентов, осуществляющих всю “грязную” работу. Поэтому при использовании SOAP Toolkit клиент может работать с высокоуровневым компонентом SOAPProxy, скрывающим все детали вызова, или (вручную) с низкоуровневыми компонентами, в случаях, когда необходима дополнительная функциональность, не предусмотренная в SOAPClient. Любой вызов SOAP включает в себя следующие этапы:

Преобразование параметров в текстовую форму осуществляется с помощью так называемых mapper-ов. В состав SOAP Toolkit’а mapper-ы для всех oleautomation-типов и некоторых других. Кроме того, есть Generic Custom Type Mapper, который подходит для большинства составных типов данных (структур). Для тех случаев, когда стандартные mapper-ы не подходят, возможно использование своих собственных mapper-ов.

Формирование XML-запроса и обратное преобразование выполняют SOAPSerializer30 и SOAPReader30.

Передача запроса на сервер и получение отклика осуществляется с помощью так называемого коннектора.

Чтобы расширить функциональные возможности стандартного Proxy, совсем необязательно программировать на низкоуровневом API, оперируя напрямую вспомогательными компонентами. SOAPClient позволяет заменять коннектор через свойства, а в WSDL-описании можно указывать свои mapper-ы.

ПРИМЕЧАНИЕ

Представляется возможным создание (с использованием низкоуровневого API) Proxy, осуществляющей вызовы с помощью раннего связывания и/или не зависящей от WSDL-файла, а использующей, например, библиотеку типов наподобие oleautomation Proxy. Однако, по моему мнению, большая часть времени при SOAP-вызове тратится совсем на другие вещи, и использование такой Proxy скорее упростит работу клиента, чем ощутимо повлияет на производительность.

Асинхронные SOAP-вызовы

Web-сервер, указанный в WSDL-файле, может оказаться временно недоступным или сильно загруженным, вызов метода может продолжаться длительное время. В этом разделе мы рассмотрим возможности настройки таймаутов соединения и реализуем поддержку асинхронных SOAP-вызовов.

За передачу запроса на сервер и получение отклика сервера отвечает коннектор, и настройки таймаутов соединения выполняются через его свойства. В стандартном коннекторе предусмотрено два свойства, помогающих настроить таймауты:

Свойство Значение
ConnectTimeout Таймаут на установление соединения с сервером
Timeout Таймаут на выполнение вызова метода, отсчет начинается с момента установления соединения
ПРИМЕЧАНИЕ

Если на сервере в качестве SOAP Listener’а используется ASP-страница, появляется еще один таймаут, задаваемый сервером. Если ASP-страница не может быть обработана в течение заданного в настройках IIS времени, запрос окончится неудачно. Эту настройку можно найти в свойствах виртуального каталога. Она называется “ASP Script timeout” и по умолчанию равна 90 секундам.

Зачастую в клиентских приложениях возникает необходимость отменить затянувшийся вызов. К сожалению, SOAP Toolkit не предоставляет поддержки асинхронных вызовов, и единственный способ повлиять на продолжительность вызова – изменить значения таймаутов.

ПРИМЕЧАНИЕ

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

В C++-приложениях можно обойти эту проблему, осуществляя SOAP-вызовы в отдельном потоке, однако за состоянием таких потоков надо следить, да и TerminateThread вызывать некорректно, так как это может привести к утечке ресурсов. В приложениях, написанных на VB6, о дополнительных потоках вообще придется забыть, если только не написать вспомогательный компонент на C++, который и будет создавать такой поток.

Благодаря открытой архитектуре SOAPClient’а существует возможность решить указанную проблему, не прибегая к помощи низкоуровневого API.

Пишем SOAP-коннектор

Стандартный коннектор не поддерживает отмену SOAP-вызова. Поэтому мы реализуем свой коннектор, который, помимо возможности отмены вызова, будет поддерживать стандартный UI для запроса имени пользователя и пароля (см. раздел “Настройка безопасности”) при доступе к защищенным ресурсам.

Чтобы стать “настоящим” коннектором, наш компонент должен реализовать интерфейс ISoapConnector, его объявление можно найти в заголовочных файлах, входящих в Platform SDK и SOAP Toolkit.

Название Описание
BeginMessage Подготовка SOAP-сообщения, к этому моменту свойства коннектора должны быть проинициализированы.
BeginMessageWSDL Подготовка SOAP-сообщения, свойства коннектора могут быть получены через параметр, имеющий тип IWSDLOperation*.
Connect Инициализация коннектора, все свойства должны быть установлены
ConnectWSDL Инициализация коннектора, свойства могут быть получены через параметр ISoapPort.
EndMessage Завершение формирования SOAP-сообщения, в этот момент должна начаться физическая отправка сообщения.
Reset Прекращает текущее соединение и выполняет повторную инициализацию коннектора.
Методы ISoapConnector
Название Описание
InputStream Входной поток. Используется для записи исходящего сообщения.
OutputStream Результат запроса.
Property Свойства коннектора.
Свойства ISoapConnector

Со времен SOAP Toolkit 2.0 интерфейс ISoapConnector несколько изменился. Теперь метод “reset” называется “Reset”, и интерфейс имеет другой IID, хотя имя осталось прежним. Будьте осторожны – при использовании заголовочных файлов от старых версий могут использоваться неправильные IID.

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

ПРИМЕЧАНИЕ

Можно использовать и серверный вариант ServerXMLHTTP, но в этом случае придется задавать информацию для доступа к защищенным ресурсам через “свойства” объекта, в то время как XMLHTTP использует стандартный UI для запроса имени пользователя/пароля и/или выбора сертификата.

ПРЕДУПРЕЖДЕНИЕ

При создании статьи использовалсять IXMLHTTPRequest, входящий в MS XML 2.6. Использование компонента, входящего в состав MS XML 3.0, не рекомендовано Microsoft по причине наличия ошибок в этой версии. Эти ошибки были исправлены в MS XML 3.0 SP1.

Еще один вопрос, который необходимо решить перед реализацией коннектора – выбор потоковой модели. Так как стандартный коннектор имеет потоковую модель ‘Both’, мы также будем использовать потоковую модель ‘Both’.

Если верить документации, вызовы к коннектору приходят в следующем порядке:

Именно таким образом и работал коннектор в SOAP Toolkit 2.0 . Однако в версии 3.0 это не так. Изменения в логике работы коннектора связаны с поддержкой бинарных вложений. Начиная с версии 3.0, появилась возможность передавать бинарные данные в теле запроса по HTTP в формате DIME, избегая накладных расходов на перекодировку в текстовый формат (base64). Поддержка вложений (attachment) осуществляется и на уровне коннектора. Реально от указателя на IStream, возвращаемого свойствами InputStream и OutputStream, можно запросить дополнительные интерфейсы, позволяющие передавать и принимать вложения:

К сожалению, эти интерфейсы не документированы (пока?). Но без них не удастся создать свой коннектор, так как SOAPClient использует их независимо от того, присутствуют вложения или нет.

Метод Описание
HRESULT _stdcall BeginSending( [out] ISequentialStream** par_stream, [out] ComposerDestinationFlags* par_flags); Получение потока для записи бинарных вложений.
HRESULT _stdcall EndSending(); Физическая отправка данных на сервер.
get_property, put_Property Позволяет задать заголовок запроса, например “content-type” = “application\dime”.
Интерфейс IComposeDestination
Метод Описание
HRESULT _stdcall BeginReceiving([out, retval] ISequentialStream** par_stream); Получение потока для чтения бинарных вложений
HRESULT _stdcall EndReceiving(); Чтение данных закончено.
get_property, put_Property Позволяет получить заголовок запроса, например “content-type”.
Интерфейс IParserSource

В SOAP Toolkit 3.0 коннектор работает в следующем режиме:

Графически работа коннектора без бинарных вложений выглядит так:


Рисунок 3. Последовательность вызовов в случае без бинарных вложений

С бинарными вложениями:


Рисунок 4. Последовательность вызовов при наличии бинарных вложений

Реализацию коннектора начнем со вспомогательного компонента, реализующего интерфейс IStream. Этот компонент будет использовать заранее распределенный блок памяти. Примерно тем же самым занимаются функции CreateStreamOnHGlobal и GetHGlobalFromStream, однако они работают с памятью, распределяемой функцией GlobalAlloc. В нашем случае это неприемлемо.

Этот компонент пригодится нам, чтобы избежать промежуточного копирования данных.

Реализация этого компонента тривиальна:

        void CMemStream::init(void* pb, ULONG cb)
{
  m_pb = reinterpret_cast<BYTE*>(pb);
  m_cb = cb;
  m_seekptr = 0;
}
STDMETHODIMP CMemStream::Read(void * pv, ULONG cb, ULONG * pcbRead)
{
  ULONG toread = min(cb , m_cb - m_seekptr);
  memcpy(pv, m_pb + m_seekptr, toread);
  m_seekptr += toread;
  if(pcbRead)
    *pcbRead = toread;
  return S_OK;
}

STDMETHODIMP CMemStream::Write(constvoid * pv, ULONG cb, ULONG * pcbWritten)
{
  ULONG towrite = min(cb , m_cb - m_seekptr);
  memcpy(m_pb + m_seekptr, pv, towrite);
  m_seekptr += towrite;
  if(pcbWritten)
    *pcbWritten = towrite;
  return S_OK;
}

Для реализации свойств InputStream и OutputStream коннектора мы используем два вспомогательных компонента CInputStream и COutputStream. COutputStream поддерживает интерфейсы IStream, IGetParserSource и IParserSource, а всю реальную работу перенаправляет коннектору.

STDMETHODIMP COutputStream::Read(void * pv, ULONG cb, ULONG * pcbRead)
{
  return m_pConn->receive(pv, cb, pcbRead);
}
// IGetParserSource
STDMETHODIMP COutputStream::get_ParserSource(IParserSource * * par_value)
{
  *par_value = IParserSourcePtr(GetUnknown()).Detach();
  return S_OK;
}
// IParserSource
STDMETHODIMP COutputStream::get_Property(BSTR par_name, VARIANT * par_value)
{
  _bstr_t val;
  
  HRESULT hr = m_pConn->get_property(par_name, val);
  if(SUCCEEDED(hr))
    *par_value = _variant_t(val).Detach();
  return hr;
}
STDMETHODIMP COutputStream::BeginReceiving(ISequentialStream * * par_stream)
{
  *par_stream = IStreamPtr(GetUnknown()).Detach();
  return S_OK;
}

Реализация CInputStream немного сложнее, так как вызов IStream::Write должен приводить к немедленной отправке сообщения на сервер, а вызовы через IComposerDestination должны буферизоваться, отправка на сервер должна быть начата только при вызове IComposerDestination::EndSending. Для хранения промежуточных данных между вызовами BeginSending и EndSending используется CMemStream.

        void CInputStream::init(CConnector* pConn)
{
  m_pConn = pConn;
}
STDMETHODIMP CInputStream::Write(constvoid * pv, ULONG cb, 
  ULONG * pcbWritten)
{
  HRESULT hr = m_pConn->send(pv, cb);
  if((SUCCEEDED(hr)) && pcbWritten)
    *pcbWritten = cb;
  return hr;
}
STDMETHODIMP CInputStream::put_TotalSize(LONG sz)
{
  m_cb = sz;
  return S_OK;
}
STDMETHODIMP CInputStream::put_Property(BSTR par_name, VARIANT par_value)
{
  CComVariant v = par_value;
  HRESULT hr = v.ChangeType(VT_BSTR);
  if(SUCCEEDED(hr))
    hr = m_pConn->set_property(par_name, v.bstrVal);
  return hr;
}
STDMETHODIMP CInputStream::BeginSending(ISequentialStream ** par_stream, 
  ComposerDestinationFlags * /*par_flags*/)
{
  if(m_pb)
  {
    delete[] m_pb;
    m_pb = 0;
  }
  m_pb = new BYTE[m_cb];
  m_pMemStream->init( m_pb, m_cb );
  *par_stream = IStreamPtr(m_pMemStream).Detach();
  return S_OK;
}
STDMETHODIMP CInputStream::EndSending()
{
  HRESULT hr = m_pConn->send(m_pb, m_cb);
  if(m_pb)
  {
    delete[] m_pb;
    m_pb = 0;
  }
  return hr;
}
// IGetComposerDestination
STDMETHODIMP CInputStream::get_ComposerDestination(
  IComposerDestination ** par_value)
{
  *par_value = IComposerDestinationPtr(GetUnknown()).Detach();
  return S_OK;
}

Чтобы поддерживать асинхронные вызовы, наш коннектор будет поддерживать три свойства (доступные через SOAPClient.ConnectorProperty), с помощью которых клиент будет контролировать асинхронные вызовы:

Свойство Описание
Async True, false – обычный или асинхронный вызов.
CallbackInterval Интервал в миллисекундах для получения клиентом уведомления о состоянии вызова.
Callback Указатель на интерфейс IAsyncSOAPCall, который должен реализовать клиент. С помощью этого интерфейса можно отменить текущий вызов.

Метод Описание
OnProgress([out] VARIANT_BOOL* bCancel) Коннектор будет передавать управлению этому методу через заданный интервал времени в процессе SOAP-вызова. Чтобы прервать SOAP-вызов, нужно вернуть bCancel = true.
Интерфейс IAsyncSOAPCall

Вот реализация основных методов коннектора:

STDMETHODIMP CConnector::put_Property(BSTR pPropertyName,VARIANT pPropertyValue )
{
    ObjectLock lock_(this);
    try
    {
        _variant_t v = pPropertyValue;
        if(!wcsicmp(pPropertyName, L"Async"))
        {
            // сохраняем новое значение в переменной m_bAsync// и при необходимости снова вызываем XMLHTTP->open с новым значением
            v.ChangeType(VT_BOOL);
            m_bAsync = (v.boolVal == VARIANT_TRUE);
            // open
            m_spRequest->open(L"POST", (WCHAR*)m_bstrURL, _variant_t(m_bAsync ?      
                 VARIANT_TRUE : VARIANT_FALSE ), vtMissing, vtMissing);
        }
        elseif(!wcsicmp(pPropertyName, L"CallbackInterval"))
        {
            v.ChangeType(VT_I4);
            m_nCallInterval = v.lVal;
        }
        elseif(!wcsicmp(pPropertyName, L"Callback"))
        {
            // сохраняем указатель на интерфейс в переменной m_spClientif((v.vt != VT_UNKNOWN) && (v.vt != VT_DISPATCH))
                _com_issue_error(E_INVALIDARG);
            CComPtr<IAsyncSOAPCall> spClient;

            HRESULT hr = v.punkVal->QueryInterface(IID_IAsyncSOAPCall,
                (void**)&spClient);
            if(SUCCEEDED(hr))
            {
                m_spClient.Release();
                m_spClient = spClient;
            }
            else
                _com_issue_error(hr);
        }
    
    }
    catch(_com_error& e)
    {
         // обрабатываем ошибкиreturn e.Error();
    }
    return S_OK;
}

HRESULT CConnector::send(constvoid* pb, ULONG cb)
{
    ObjectLock lock_(this);
    try
    {
        // устанавливаем заголовок запроса content-type = text/xml
        WCHAR buf[20];
        _ultow(cb, buf, 10);
        if(!m_bContentTypeSet)
            m_spRequest->setRequestHeader(L"content-type", L"text/xml");
        m_spRequest->setRequestHeader(L"content-length", buf);
        
        m_pMemStream->init(const_cast<void*>(pb), cb);
        IUnknownPtr spStm = m_pMemStream;
        m_spRequest->send(_variant_t((IUnknown*)spStm));
        
        while(m_spRequest->readyState != 4)
        {
            VARIANT_BOOL bCancel = VARIANT_FALSE;
            if(m_spClient)
            {
                // вызываем событие у клиента, если bCancel = true - выходим
                m_spClient->OnProgress(&bCancel);
                if(bCancel == VARIANT_TRUE)
                {
                    m_spRequest->abort();
                    _com_issue_error(E_ABORT);
                }
            }
            // обрабатываем очередь сообщений
            MSG msg;
            const DWORD dwSleepTime = 100;
            DWORD dwTotal = 0;
            while(dwTotal < m_nCallInterval)
            {
                while(::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
                {
                    ::TranslateMessage(&msg);
                    ::DispatchMessage(&msg);
                }
                Sleep(dwSleepTime);
                dwTotal += dwSleepTime;
            }
        }

        // сохраняем в m_spStm значение responseStream
        _variant_t v = m_spRequest->responseStream;
        if((v.vt != VT_UNKNOWN) && (v.vt != VT_DISPATCH))
            _com_issue_error(E_UNEXPECTED);
            
        m_spStm = v.punkVal;
    }
    catch(_com_error& e)
    {
        return e.Error();
    }
    return S_OK;
}

Основная особенность метода send заключается в том, что во время ожидания завершения вызова обрабатывается очередь сообщений. Чтобы клиент мог осуществлять асинхронный вызов, ему достаточно реализовать интерфейс IAsyncSOAPCall (в VB, например, это можно сделать с помощью ключевого слова implements).

Итак, наша реализация коннектора обладает следующими достоинствами:

Сложные типы данных и mapper-ы

SOAP Toolkit поддерживает возможность передачи сложных типов данных. Чтобы передавать такие данные, используются специальные COM-объекты – mapper-ы, определяющие способ прямого и обратного преобразования сложных типов в XML. В данном случае под сложными типами имеются в виду COM-объекты и UDT (User Defined Type, или, по-простому, структуры). Для их преобразования в SOAP Toolkit входит два стандартных mapper-а:

Если функциональности стандартных mapper-ов недостаточно, можно создать собственный Custom Type Mapper.

UDT Mapper

UDT представляет собой структуру, состоящую из oleautomation-совместимых типов. В Visual Basic 6 UDT определяются следующим образом:

Type Employee
  EmpNumber AsInteger
  EmpOfficePhone AsString  
  EmpHomePhone AsStringEnd Type
UDT могут быть вложенными:
Type Man
  emp As Employee
End Type

На UDT распространяются следующие ограничения:

Рассмотрим использование UDT Mapper-а на следующем примере:

        typedef [uuid(C1D3A8C0-A4AA-11D0-819C-00A0C90FFFC3)] struct UDT 
{
   unsignedlong a1;
   BSTR pbstr;
} UDT;

...
interface IUDTSample : IDispatch
{
  [id(1)] HRESULT GetData([in]UDT* pIn, [in,out]UDT* pOut);
};

Наш COM-объект поддерживает метод GetData, имеющий входной и выходной параметры типа UDT. Более подробно о работе с UDT в COM можно прочитать по адресу http://rsdn.ru/article/?com/varsafearr.xml.

СОВЕТ

В сгенерированном мастером WSML-файле появится ссылка на UDT Mapper:

<using PROGID='MSSOAP.UDTMapper30' cachable='0' ID='UDTM' />
<types>
  <type name='UDT' targetNamespace=... uses='UDTM' targetClassId='XX' 
    libGUID='YY' libVersion='1.0'/>
</types>

Клиент осуществляет вызов так:

        Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "D:\Projects\SOAP\MapperSampleSrv\IIS\MapperSampleSrv.WSDL",
  , ,   "D:\Projects\SOAP\MapperSampleSrv\IIS\MapperSampleSrv.WSML"Dim q As UDT
q.a1 = 1
Dim r As UDT
o.GetData q, r
MsgBox CStr(r.a1) & r.pbstr
СОВЕТ

Реализация клиента на VC++ будет немного сложнее, так как для UDT придется получать интерфейс IRecordInfo*, описывающий UDT – сделать это можно с помощью функции GetRecordInfoFromGuids. Подробнее см. в вышеупомянутой статье.

В коде клиента есть две особенности, связанных с использованием mapper-а:

Generic Custom Type Mapper

COM-объекты – это еще одна разновидность сложных типов данных. Для передачи таких данных SOAP Toolkit включает в себя специальный Generic Custom Type Mapper (GCTM). С ним связана неприятная особенность генератора WSDL-файлов – когда он встречает параметр метода, имеющий тип "указатель на интерфейс", генератор вставляет в WSML-файл ссылку на GCTM. Но это совсем не означает, что SOAP Toolkit поддерживает передачу клиенту объектных ссылок, напротив, GCTM использует только свойства (properties) COM-объекта, сохраняя их в XML и восстанавливая на приемной стороне. При использовании GCTM происходит следующее:

Из этого следует:

Работу с GCTM иллюстрирует следующий пример:

...
interface IGCTMDataObject : IDispatch
{
  [propget, id(1)] HRESULT Data([out, retval] BSTR *pVal);
  [propput, id(1)] HRESULT Data([in] BSTR newVal);
};
...
interface IGCTMSample : IDispatch
{
  [id(1)] HRESULT Get([out]IGCTMDataObject** pData);
};

Интерфейс IGCTMSample возвращает указатель на интерфейс IGCTMDataObject, имеющий одно read-write свойство Data.

Код клиента получает объект DataObject и читает его свойство:

        Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "D:\Projects\SOAP\MapperSampleSrv\IIS\MapperSampleSrv.WSDL", , _
  "GCTMSampleSoapPort", _
  "D:\Projects\SOAP\MapperSampleSrv\IIS\MapperSampleSrvClient.WSML"Dim p AsObject
o.Get p
MsgBox p.Data

Как и для UDT Mapper-а, при инициализации клиента необходимо указать местоположение WSML-файла и предоставить клиенту библиотеку типов с описанием передаваемого интерфейса. Специфика использования GCTM заключается в том, что клиент предпримет попытку создать передаваемый COM-объект, поэтому помимо библиотеки типов клиенту потребуется модуль, содержащий код компонента.

Custom Type Mapper

Мы расширим категорию COM-объектов, которые могут быть переданы клиенту по протоколу SOAP, реализовав свой собственный Custom Type Mapper. Наш mapper будет использовать возможность объектов сохранять и восстанавливать свое состояние с помощью интерфейса IPersistStream, который реализуют очень многие компоненты.

Mapper должен поддерживать интерфейс ISoapTypeMapper:

Метод/Свойство Описание
IID IID интерфейса объекта, для которого предназначен mapper.
Init Инициализация mapper-а.
Read Преобразование из XML в сложный тип.
SchemaNode Возвращает фрагмент схемы, описывающий сложный тип.
VarType Тип значения, которое ожидает mapper.
Write Преобразует сложный тип в XML.
XSDType Тип данных XML, который поддерживает mapper.

Так как mapper будет использовать интерфейс IPersistStream объекта, метод VarType будет возвращать VT_UNKNOWN, а XSDType - enXSDbase64binary.

Нам потребуется преобразование из бинарного формата в base64. Это преобразование может быть выполнено с помощью компонента, входящего в состав SOAP Toolkit – DataEncoder:

CComPtr<IDataEncoderFactory> spFactory;
CheckError(spFactory.CoCreateInstance(CLSID_DataEncoderFactory30));
CComPtr<IDataEncoder> spEncoder;
CheckError(spFactory->GetDataEncoder(CComBSTR(L"base64"), &spEncoder));

Ниже приведена реализация методов Read и Write:

STDMETHODIMP CPersistMapper::Write(ISoapSerializer * par_ISoapSerializer, 
           BSTR par_encoding, enEncodingStyle par_encodingMode, LONG par_flags,
           VARIANT * par_var)
{
    
    try
    {
        usingnamespace _com_util;

        // создаем фабрику кодировщиков и получаем нужный    
        CComPtr<IDataEncoderFactory> spFactory;
        CheckError(spFactory.CoCreateInstance(CLSID_DataEncoderFactory30));

        CComPtr<IDataEncoder> spEncoder;
        CheckError(spFactory->GetDataEncoder(CComBSTR(L"base64"), &spEncoder));
        if((par_var->vt != VT_UNKNOWN) && (par_var->vt != VT_DISPATCH))
            _com_issue_error(E_INVALIDARG);
    
        // сохраняем объект в IStream
        CComPtr<IPersistStream> spPersist;
        CheckError(par_var->punkVal->QueryInterface(IID_IPersistStream,
            (void**)&spPersist));
        CComPtr<IStream> spStm;
        CheckError(CreateStreamOnHGlobal(0, TRUE, &spStm));
        CheckError(spPersist->Save(spStm, FALSE));
        LARGE_INTEGER off = {0};
        CheckError(spStm->Seek(off, STREAM_SEEK_SET, NULL));
        STATSTG st = {0};
        CheckError(spStm->Stat(&st, 0));

        // перекодируем в base64
        CComPtr<IStream> spEncoded;
        CheckError(CreateStreamOnHGlobal(0, TRUE, &spEncoded));
        CheckError(spEncoder->EncodeStream(spStm, spEncoded));
        CheckError(spEncoded->Seek(off, STREAM_SEEK_SET, NULL));
        LPVOID hGlobal = NULL;
        CheckError(GetHGlobalFromStream(spEncoded, &hGlobal));
        CheckError(spEncoded->Stat(&st, 0));

        // записываем результат в serializer
        CheckError(par_ISoapSerializer->WriteBuffer(st.cbSize.LowPart,
              *((unsignedchar**) hGlobal)));
    }
    catch(_com_error& e)
    {
        return e.Error();
    }
    return S_OK;
}

STDMETHODIMP CPersistMapper::Read(ISoapReader * par_soapreader, 
                IXMLDOMNode * par_Node, BSTR par_encoding, enEncodingStyle  
                par_encodingMode, LONG par_flags, VARIANT * par_var)
{
    try
    {
        usingnamespace _com_util;

        CComPtr<IDataEncoderFactory> spFactory;

        // получаем атрибут targetPROGID из WSML и создаем нужный объект
        CComPtr<IXMLDOMElement> spElem;
        CheckError(m_spWSML.QueryInterface(&spElem));
        CComVariant vValue;
        CComPtr<IPersistStream> spPersist;
        CheckError(spElem->getAttribute(L"targetPROGID", &vValue));
        CheckError(vValue.ChangeType(VT_BSTR));
        CheckError(spPersist.CoCreateInstance(vValue.bstrVal));

        // создаем фабрику кодировщиков и получаем нужный// код аналогичный приведенному выше для Write
        ...

        // получаем данные в base64, преобразуем их в IStream и перекодируем // в бинарный формат
        CComBSTR bstrText;
        CheckError(par_Node->get_text(&bstrText));
        
        CComPtr<IStream> spStm;
        USES_CONVERSION;
        CheckError(CreateStreamOnHGlobal(0 , TRUE, &spStm));
        CheckError(spStm->Write(W2A(bstrText), bstrText.Length(), 0));
        LARGE_INTEGER off = {0};
        CheckError(spStm->Seek(off, STREAM_SEEK_SET, 0));
        CComPtr<IStream> spDecoded;
        CheckError(CreateStreamOnHGlobal(0 , TRUE, &spDecoded));
        CheckError(spEncoder->DecodeStream(spStm, spDecoded));
        CheckError(spDecoded->Seek(off, STREAM_SEEK_SET, 0));

        // загружаем данные объекта
        CheckError(spPersist->Load(spDecoded));
        CComPtr<IUnknown> spUnk;
        CheckError(spPersist.QueryInterface(&spUnk));
        CComVariant vResult = spUnk;
        
	    // возвращаем созданный объект
        CheckError(vResult.Detach(par_var));
   }
   catch(_com_error& e)
   {
        return e.Error();
   }
   return S_OK;
}

Метод Read создает экземпляр передаваемого компонента, используя атрибут targetPROGID, указанный в WSML-файле.

Чтобы проверить mapper в действии, нам потребуется серверный компонент, возвращающий объектную ссылку, и компонент, поддерживающий IPersistStream. В качестве последнего нам подойдет Recordset, входящий в состав ADO. Метод серверного компонента мы объявим следующим образом:

        interface IPersistObj : IDispatch
{
  [id(1)] HRESULT Get([out]_Recordset** pData);
};

А реализация этого метода будет создавать Recordset с двумя полями, заполняя их некоторыми значениями:

STDMETHODIMP CPersistObj::Get(_Recordset **pData)
{
  _RecordsetPtr pRecordset(__uuidof(Recordset));
  pRecordset->CursorLocation = adUseClient;
  FieldsPtr pFields = pRecordset->Fields;
  pFields->Append(L"id", adInteger, 0, adFldUnspecified);
  pFields->Append(L"name", adBSTR, 0, adFldUnspecified);
  pRecordset->Open(vtMissing, vtMissing, adOpenStatic,
    adLockBatchOptimistic, adOptionUnspecified);
  pRecordset->AddNew();
  pFields->Item[L"id"]->Value = (long) 1;
  pFields->Item[L"name"]->Value = L"123";

  *pData = pRecordset.Detach();
  return S_OK;
}

Теперь нам необходимо создать WSDL- и WSML-файлы, описывающие серверный компонент и использующие разработанный нами mapper. Часть работы придется проделать вручную, так как генератор, обнаружив в методе Get параметр с типом _Recordset, создаст описания большого количества компонентов из ADO со ссылкой на GCTM. Например, у интерфейса _Recordset есть свойство Fields, для которого генератор также вставит описание в WSDL и WSML. Нам придется поступить так: убрать из IDL ссылку на _Recordset (временно заменив на long, например), сгенерировать WSDL и WSML мастером, а затем добавить в WSDL и WSML описание для типа Recordset вручную.

В WSML-файл мы добавим:

<using PROGID='CustomMapper.PersistMapper' cachable='0' ID='CTM' />
<types>
  <type name='Recordset' targetNamespace=... uses='CTM' 
        targetPROGID='ADODB.Recordset' 
        iid='{00000535-0000-0010-8000-00aa006d2ea4}'/>
</types>
СОВЕТ

Изменения нужно внести в два WSML-файла, сгенерированных мастером, один из них используется на сервере, а другой на клиенте, ссылка на Custom Mapper и описание типа Recordset должно присутствовать в обоих.

ПРЕДУПРЕЖДЕНИЕ

При описании типа в WSML-файле очень важно указать правильный IID интерфейса, иначе SOAPServer не будет использовать указанный mapper. В данном случае мы указали IID интерфейса _Recordset.

В WSDL-файл необходимо добавить:

<complexType  name ='Recordset'>
  <sequence>
  </sequence>
</complexType>
<message name='PersistObj.GetResponse'>
  <part name='pData' type='typens:Recordset'/>
</message>

Код клиента, как обычно, очень прост:

        Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "D:\Projects\SOAP\MapperSampleSrv\IIS\MapperSampleSrv.WSDL", , _
  "PersistObjSoapPort", _
  "D:\Projects\SOAP\MapperSampleSrv\IIS\MapperSampleSrvClient.WSML"Dim p AsObject
o.Get p

Работу mapper-а можно “увидеть”, используя утилиту трассировки SOAP-вызовов. Для этого в WSDL-файле изменим URL, добавив порт 8080, который использует трассировщик:

<soap:address location='http://ivan:8080/MapperSampleSrv/MapperSampleSrv.ASP'/>
ПРИМЕЧАНИЕ

Утилита трассировки должна быть запущена во время SOAP-вызова, иначе обращение к порту 8080 будет неуспешным.

Ниже приведен пример отклика сервера, использующего mapper, “увиденный” с помощью утилиты трассировки:

<SOAP-ENV:Body ...>
 <SOAPSDK4:GetResponse ...>
  <pData>
  tpLyPwSyzxGNIwCqAF/+WAEHVEchAAAAAAIZALaS8j8Ess8RjSMAqgBf/lgBAAAAAAAAAAADZwDS
  rWP2AuvPEbDjAKoAPwAPAAAAAgACAAAAAAAAAAEAAAABAME8jrbrbdARjfYAqgBf/lgHAAsAAAAE
  AAEAAAATAAAABAABAAAADQAAAAAADgAAAAAADwAAAAAAEAAAAAAAEgAAAAAAEHwAAgC+IrXI81zO
  Ea3lAKoARHc9BAB/AAAAAgD//4YAAAACAP//IgAAAAQAAAAAAEkAAAAEAAAAAADBPI62623QEY32
  AKoAX/5YBQAEAAAABAAPAAAABQAAAAQAAgAAAAMAAAAEAA8AAAAHAAAABAAyAAAACAAAAAQAAwAA
  AAYhAIABAAEAAgBpAGQAAwAEAAAAAAAAAAAAAAAUAAAAAAD//wYlAIABAAIABABuAGEAbQBlAIIA
  /////wAAAAAAAAAAhAAAAAAA//8NwAABAAAABgAAADEAMgAzAA8=
  </pData> 
 </SOAPSDK4:GetResponse>
</SOAP-ENV:Body>

DIME и бинарные вложения

Поскольку XML представляет собой текстовый формат, для передачи бинарных данных требуется их перекодировка, например, в кодировку base64. Это означает, что в случае передачи бинарных данных, во-первых, увеличивается время вызова, так как перекодировка в base64 занимает некоторое время, а, во-вторых, увеличивается объем данных, которые необходимо передать по HTTP.

SOAP Toolkit, начиная с версии 3.0, поддерживает новый формат HTTP-запросов, называемый DIME – Direct Internet Message Encapsulation. DIME является по сути бинарным форматом передачи данных и не требует их перекодировки. Сообщение в формате DIME представляет собой совокупность DIME-записей. Каждая запись состоит из заголовка и бинарных данных. Заголовок записи включает в себя длину записи, тип данных и идентификатор записи. Такая организация позволяет быстро найти нужную запись и прочитать данные из нее.

SOAP Toolkit использует формат DIME для передачи бинарных вложений. Преимущество передачи бинарных данных в виде вложений заключается в том, что данные будут передаваться более эффективно и на их обработку потребуется меньше времени и ресурсов.

SOAP Toolkit поддерживает 4 вида бинарных вложений:

Название объекта для работы с бинарными вложениями Описание
FileAttachment30 Позволяет передать файл.
StringAttachment30 Предназначен для передачи строк.
ByteArrayAttachment30 Передает массив байтов.
StreamAttachment30 Передает данные в IStream.

Когда генератор WSDL-файлов встречает в tlb описание метода, имеющего параметром один из перечисленных выше типов, он автоматически добавляет в WSDL информацию, необходимую для передачи/приема вложений. Эта информация включает в себя описание типа:

<complexType name='UnknownBinaryContent'>
  <simpleContent>
    <restriction base='typens:ReferencedBinary'>
      <annotation>
        <appInfo>
          <!-- 
            You may use one or more of the following elements
            to describe the binary content:
            <content:type value='(URI Identifing Type)' />
            <content:mediaType value='(MIME Media Type)' />
            <content:documentType 
              value='(Name of XML Document Element)' />
          -->
        </appInfo>
      </annotation>
    </restriction>
  </simpleContent>
</complexType>
и указание использовать формат DIME в описании операции:
<operation name='PutPicture' >
  <soap:operation 
  soapAction=  'http://tempuri.org/AttachSvr/action/AttachSvr.PutPicture' />
    <input>
      <dime:message 
        layout='http://schemas.xmlsoap.org/ws/2002/04/dime/closed-layout' 
        wsdl:required='true' />

SOAP-запрос, включающий бинарные вложения, может выглядеть так:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?> 
<SOAP-ENV:Envelope ...>
  <SOAP-ENV:Body ...>
    <SOAPSDK4:PutPicture ...>
      <Picture href="uuid:AC3437F8-8746-4250-A204-9F8DAAA5E2D7"/> 
    </SOAPSDK4:PutPicture>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Работа с бинарными вложениями через высокоуровневый API сводится к следующему:

STDMETHODIMP CAttachHandler::GetFile(IFileAttachment **pAttach)
{
  IFileAttachmentPtr spAttach(CLSID_FileAttachment30);
  spAttach->put_FileName(L"D:\\as331003.zip");
  *pAttach = IFileAttachmentPtr(spAttach).Detach();
  return S_OK;
}

Клиент получает бинарное вложение, используя интерфейс IReceivedAttachment:

      Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "D:\Projects\SOAP\Sample2\IIS\Sample2.WSDL"Dim f As IReceivedAttachment
o.GetFile f
f.SaveToFile "d:\recvd"
STDMETHODIMP CAttachHandler::PutFile(IReceivedAttachment *pFile)
{
  pFile->SaveToFile(CComBSTR(OLESTR("d:\\someattach.zip")), VARIANT_FALSE);
  return S_OK;
}

Клиент использует один из поддерживаемых в SOAP Toolkit видов вложений, чтобы передать данные серверу:

      Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "D:\Projects\SOAP\Sample2\IIS\Sample2.WSDL"Dim f As FileAttachment30
Set f = New FileAttachment30
f.FileName = "D:\as331003.zip"
o.PutFile f

При работе с высокоуровневым API описание всех бинарных вложений появляется в WSDL-файле, и SOAP-запрос включает в себя ссылку на каждое бинарное вложение:

<SOAPSDK4:PutPicture ...>
  <Picture href="uuid:AC3437F8-8746-4250-A204-9F8DAAA5E2D7"/> 
</SOAPSDK4:PutPicture>

Однако существует возможность добавлять анонимные вложения (unreferenced), описания которых нет в WSDL-файле, и ссылка на которые не появляется в SOAP-запросе. Работа с такими вложениями возможна только через низкоуровневый API.

Custom Type Mapper 2

Мы можем использовать поддержку бинарных вложений для более эффективной передачи сериализованных COM-объектов клиенту. Разработанный нами Custom Type Mapper использовал кодировку base64, чтобы сохранить сериализованный объект непосредственно в теле XML-ответа сервера. Мы можем избежать необходимости перекодирования бинарного потока в base64, используя бинарные вложения.

Чтобы добавить в mapper поддержку DIME, придется немного изменить методы Read и Write. Кроме того, метод XSDType будет теперь возвращать enXSDbinary. Новые реализации Read и Write будут использовать возможности объектов SOAPReader30 и SOAPSerializer30 для работы с бинарными вложениями:

STDMETHODIMP CDIMEMapper::Write(ISoapSerializer * par_ISoapSerializer, 
              BSTR par_encoding, enEncodingStyle par_encodingMode, 
              LONG par_flags, VARIANT * par_var)
{
    
    try
    {
        usingnamespace _com_util;
        if((par_var->vt != VT_UNKNOWN) && (par_var->vt != VT_DISPATCH))
            _com_issue_error(E_INVALIDARG);
        // сохраняем состоаяние объекта с помощью IPersistStream::Save
        CComPtr<IPersistStream> spPersist;
        CheckError(par_var->punkVal->QueryInterface(IID_IPersistStream,
            (void**)&spPersist));
        CComPtr<IStream> spStm;
        CheckError(CreateStreamOnHGlobal(0, TRUE, &spStm));
        CheckError(spPersist->Save(spStm, FALSE));
        
        // добавляем данные в виде бинарного вложения
        CComPtr<IStreamAttachment> spAttach;
        CheckError(spAttach.CoCreateInstance(CLSID_StreamAttachment30));
        CheckError(spAttach->putref_Stream(spStm));
        CheckError(par_ISoapSerializer->AddAttachmentAndReference(spAttach));
    }
    catch(_com_error& e)
    {
        return e.Error();
    }
    return S_OK;
}

В реализации метода Write мы добавляем данные с помощью метода AddAttachmentAndReference.

STDMETHODIMP CDIMEMapper::Read(ISoapReader * par_soapreader, 
               IXMLDOMNode * par_Node, BSTR par_encoding, 
               enEncodingStyle par_encodingMode, LONG par_flags, 
               VARIANT * par_var)
{
    if (par_var == NULL)
        return E_POINTER;
   try
   {
        usingnamespace _com_util;

        CComPtr<IPersistStream> spPersist;
        // создаем объект spPersist по targetProgID аналогично реализации// Write предыдущего mapper-а
        ...

        // получаем бинарное вложение// конвертируем его в IStream
        CComPtr<IReceivedAttachment> spAttach;
        CheckError(par_soapreader->GetReferencedAttachment(par_Node, &spAttach));
        CComVariant vArray;
        CheckError(spAttach->GetAsByteArray(&vArray));
        if(vArray.vt != (VT_ARRAY | VT_UI1) ) _com_issue_error(E_UNEXPECTED);
        VOID* pData = NULL;
        CheckError(SafeArrayAccessData(vArray.parray, &pData));
        CComPtr<IStream> spStm;
        CheckError(CreateStreamOnHGlobal(0, TRUE, &spStm));
        long lBound, uBound;
        CheckError(SafeArrayGetLBound(vArray.parray, 1, &lBound));
        CheckError(SafeArrayGetUBound(vArray.parray, 1, &uBound));
        CheckError(spStm->Write(pData, uBound - lBound + 1, 0));
        LARGE_INTEGER off = { 0 };
        CheckError(spStm->Seek(off, STREAM_SEEK_SET, 0));

        // ... и загружаем состояние объекта
        CheckError(spPersist->Load(spStm));
        CComPtr<IUnknown> spUnk;
        CheckError(spPersist.QueryInterface(&spUnk));
        CComVariant vResult = spUnk;
        
        // возвращаем ссылку на созданный объект
        CheckError(vResult.Detach(par_var));
   }
   catch(_com_error& e)
   {
        return e.Error();
   }
   return S_OK;
}

Read использует метод GetReferencedAttachment, чтобы добраться до бинарного вложения, переданного в составе сообщения.

Чтобы новый mapper мог правильно работать, придется изменить описание типа Recordset в WSDL-файле и добавить ссылку на DIME в описание SOAP-операции:

<complexType  name ='RecordsetEx'>
  <simpleContent>
  <restriction base='typens:ReferencedBinary'>
    <annotation>
    <appInfo>
    </appInfo>
    </annotation>
  </restriction>
  </simpleContent>
</complexType>

<binding name='PersistObjSoapBinding' type='wsdlns:PersistObjSoapPort' >
  ...
  <operation name='GetEx'>
    <soap:operation .../>
      <input>
        <soap:body use='encoded' .../>
      </input>
      <output>
       <dime:message layout=
          'http://schemas.xmlsoap.org/ws/2002/04/dime/closed-layout' 
          wsdl:required='true' />      <soap:body
        ... />
      </output>
    </soap:operation>
</operation>
</binding>

Тег dime-message заставит SOAPServer использовать формат DIME при передаче данных клиенту. Это можно увидеть, используя утилиту трассировки:

<DimePayload>
  <DimeRecord traceOffset="0x00000000">
    <Recordinfo Version="1" MB="1" ME="0" CF="0" IDLength="41" /> 
    <Typefield TNF="2" TypeLength="41" /> 
    <Options O="0" OptionLength="0" /> 
    <Datalength length="585" /> 
    <ID value="uuid:714C6C40-4531-442E-A498-3AC614200295" /> 
    <Type value="http://schemas.xmlsoap.org/soap/envelope/" /> 
  </DimeRecord>
  <DimeRecord traceOffset="0x000002B0">
    <Recordinfo Version="1" MB="0" ME="1" CF="0" IDLength="41" /> 
    <Typefield TNF="3" TypeLength="0" /> 
    <Options O="0" OptionLength="0" /> 
    <Datalength length="380" /> 
    <ID value="uuid:13849D5F-BA10-4B4B-AFD1-5C3838D0524D" /> 
  </DimeRecord>
 </DimePayload>

Как видим, DIME-сообщение включает в себя 2 записи, первая является обычным SOAP XML-откликом сервера, а вторая – сериализованным COM-объектом. Длина данных указывает на то, что преобразования в base64 не было.

Надо заметить, что код клиента и код сервера при замене одного mapper-а на другой не изменились, метод сервера использует тип Recordset, поэтому такой метод можно использовать и в DCOM-приложениях. Таким образом, использование mapper-ов придает гибкость SOAP-приложениям, позволяя “на лету” изменять способ и формат передаваемых данных, никак не затрагивая при этом ни сервер, ни клиента.

Переход от DCOM к SOAP

SOAP не поддерживает COM-событий через интерфейс IConnectionPoint, обратных вызовов от сервера к клиенту, передачи объектных ссылок, управления временем жизни серверных компонентов. Поэтому в общем случае DCOM-приложение не может быть легко преобразовано для использования протокола SOAP. Однако существует категория DCOM-приложений, архитектура которых организована таким образом, что клиенту передаются только данные в виде массивов или наборов записей ADO. Такие приложения могут легко переключиться на использование SOAP.

Проблемы, возникающие при переходе от DCOM к SOAP, можно рассмотреть на примере DCOM-приложения TView. Это приложение предназначено для просмотра информации о запущенных процессах на удаленном компьютере. Подробное описание TView можно найти в MSDN Magazine (декабрь 2000 г.) - http://msdn.microsoft.com/msdnmag/issues/1200/tview/default.aspx

TView использует Recordset из ADO для передачи данных клиенту, сервер представляет собой COM+-компонент. Интерфейс, реализуемый сервером:

Метод Описание
HRESULT ShutdownMachine([in] long nFlags) Выключить удаленный компьютер.
HRESULT GetProcesses([out, retval] _Recordset** ppRecordset) Возвращает список процессов.
HRESULT KillProcess([in] long processID) Завершает заданный процесс.
HRESULT GetModules( [in] long processID, [out, retval] _Recordset** ppRecordset); Возвращает список модулей процесса.
HRESULT DebugProcess([in] long processID) Запускает процесс на отладку.
HRESULT GetMemory([in] long processID, [out, retval] _Recordset** ppRecordset); Возвращает информацию об используемой процессом памяти.
HRESULT GetHandles([in] long processID, [out, retval] _Recordset** ppRecordset) Список хэндлов процесса.
HRESULT GetEnvironment([in] long processID, [out, retval] _Recordset** ppRecordset) Список переменных окружения процесса.

Интерфейс сервера предполагает только передачу данных клиенту. Главная проблема, связанная с этим интерфейсом, заключается в том, что для передачи данных используется ADO Recordset, и нам потребуется mapper, рассмотренный в предыдущих разделе.

Генерируем WSDL и WSML

В первую очередь нам понадобятся WSDL- и WSML-файлы, описывающие методы серверного компонента. Воспользоваться генератором не удастся, так как он, встретив тип Recordset в библиотеке типов, делает попытку добавить описание для всех свойств объекта Recordset в WSDL и завершается с ошибкой “Class not registered”. Чтобы не создавать эти файлы вручную, мы “обманем” генератор. Для этого:

        typedef
        short _Recordset;

Теперь в библиотеке типов параметр Recordset заменен на short, и мы можем использовать генератор. Кода файлы сгенерированы, нам придется отредактировать их, чтобы заменить short на Recordset вручную и добавить ссылку на наш mapper:

ПРЕДУПРЕЖДЕНИЕ

При замене short на Recordset надо изменять и namespace с xsd на typens. Таким образом, строка “xsd:short” заменяется на “typens:Recordset”

<complexType  name ='Recordset'>
  <simpleContent>  
    <restriction base='typens:ReferencedBinary'>
      <annotation>
        <appInfo/>
      </annotation>
    </restriction>
  </simpleContent> 
</complexType>
<dime:message 
      layout='http://schemas.xmlsoap.org/ws/2002/04/dime/closed-layout'
      wsdl:required='true' />
<using PROGID='CustomMapper.DIMEMapper' cachable='0' ID='DIMECTM' />
<types>
  <type name='RecordsetEx' 
        targetNamespace='http://tempuri.org/TView/type/' 
uses='DIMECTM' targetPROGID='ADODB.Recordset' 
        iid='{00000535-0000-0010-8000-00aa006d2ea4}'/>
</types>
ПРЕДУПРЕЖДЕНИЕ

Атрибут тега type targetNamespace должен совпадать со значением, указанным в разделе <schema> WSDL-файла.

Когда файлы сгенерированы, надо не забыть создать виртуальный каталог IIS, к которому будут обращаться клиенты. Если в качестве listener’а был выбран ISAPI, то необходимо также зарегистрировать SOAP ISAPI-расширение для виртуального каталога. Для проверки правильности настроек виртуального каталога можно обратиться с помощью Internet Explorer’а по URL, указанному в WSDL-файле.

VB-клиент

Перед тем, как вносить изменения в клиента TView, убедимся, что SOAP-сервер функционирует и способен передавать данные клиенту. Для этого напишем небольшой VB-клиент. Он будет получать список процессов и отображать их, используя DataGrid:

        Dim p As ADODB.Recordset
Dim o As MSSOAPLib30.SoapClient30
Set o = New MSSOAPLib30.SoapClient30
o.MSSoapInit "http://ivan/TView/TView.wsdl", , , _
  "http://ivan/TView/TViewClient.wsml"Set p = o.GetProcesses
Set grd.DataSource = p

Proxy для клиента

В идеале мы не должны вносить никаких изменений в код уже существующего клиента, а он использует раннее связывание для вызовов серверного объекта. Чтобы избежать серьезной модификации кода клиента, нам придется реализовать небольшой Proxy-объект, реализующий интерфейс сервера и передающий запросы через SOAP. Для разработки Proxy мы будем использовать VB.

Реализация Proxy тривиальна. В методе Class_Initialize мы подготавливаем соединение с SOAP-сервером, а вызовы всех методов перенаправляем к SOAP-клиенту, используя позднее связывание:

        Implements TVIEWMTSLib.TView
Private o As MSSOAPLib30.SoapClient30

PrivateSub Class_Initialize()
  Set o = New MSSOAPLib30.SoapClient30
  o.MSSoapInit "http://ivan/TView/TView.wsdl", , , _
    "http://ivan/TView/TView.wsml"EndSubPrivateFunction TView_GetEnvironment(ByVal processID AsLong) _
    As ADODB.Recordset
  Set TView_GetEnvironment = o.GetEnvironment(processID)
EndFunction
...

Изменяем клиента TView

Единственное изменение, которое потребуется сделать в клиенте, заключается в замене CLSID компонента, передающего данные. Вместо непосредственного обращения к серверу запрос будет передаваться Proxy, которая будет передавать его дальше, используя SOAP. Для этого в клиенте надо изменить всего одну строчку, где создается экземпляр класса TView:

HRESULT hr = CoCreateInstanceEx(__uuidof(TView), NULL,
    CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, &csi, 1, &mqi);

на

HRESULT hr = CoCreateInstanceEx(__uuidof(Proxy), NULL,
    CLSCTX_ALL, &csi, 1, &mqi);

В результате мы получили следующее:

Во многом прозрачность перехода на SOAP для сервера и клиента обеспечивается использованием mapper-а, который преобразует COM-объект Recordset в некоторое бинарное представление, пригодное для передачи по протоколу SOAP.

Proxy с ранним связыванием

Объект SOAPClient30, входящий в SOAP Toolkit, удобен в использовании, но вызовы методов происходят с помощью динамического интерфейса IDispatch. .

Чтобы упростить жизнь клиенту и заодно избежать накладных расходов IDispatch-интерфейса, мы реализуем Proxy, которая будет принимать вызовы через vtable и использовать низкоуровневый API для передачи этих вызовов на сервер. Применение такой Proxy в коде клиента имеет два существенных преимущества по сравнению с SOAPClient30:

Чтобы спроектировать Proxy, нам надо разобраться с последовательностью вызовов низкоуровневых объектов:

      Dim r As WSDLReader30
Dim es As IEnumWSDLService
Dim s As IWSDLService
Dim ep As IEnumWSDLPorts
Dim p As IWSDLPort
Dim eo As IEnumWSDLOperations
Dim o As IWSDLOperation
Dim em As IEnumSoapMappers
   
Set r = New WSDLReader30
r.Load "http://ivan/TView/TView.wsdl", "http://ivan/TView/TView.wsml"
r.GetSoapServices es
es.Find "TView", s
s.GetSoapPorts ep
ep.Find "TViewSoapPort", p
p.GetSoapOperations eo
eo.Find "GetProcesses", o
o.GetOperationParts em
      Set ocnf = New SoapConnectorFactory30
Set ocn = ocnf.CreatePortConnector(p)
ocn.Property("SoapAction") = o.SoapAction
ocn.ConnectWSDL p
      Set ss = New SoapSerializer30
ss.InitWithComposer ocn.InputStream, composer
ss.StartEnvelope
ss.StartBody "STANDARD"
o.Save ss, True
ss.EndBody
ss.EndEnvelope
ss.Finished
ocn.EndMessage
      Set sr = New SoapReader30
sr.LoadWithParser ocn.OutputStream, Parser
o.Load sr, False

Код будет одинаков для вызова любых методов серверного компонента. Изменяться будут только имя операции и работа с входными и выходными параметрами с помощью интерфейса ISoapMapper. Кроме того, код можно разбить на две части – первая часть (разбор WSDL и WSML, создание компонентов) выполняется однократно, другая часть (получение операции, заполнение параметров, сериализация в XML) выполняется при каждом вызове.

Интерфейс нашего Proxy-объекта будет таким:

Метод Описание
Initialize(WSDL,WSML,Service,Port) Инициализация. Разбор WSDL и WSML, создание вспомогательных компонентов.
ConnectorProperty Позволяет изменять свойства коннектора.
ProxyProperty Позволяет изменять свойства Proxy. Мы будем поддерживать одно свойство – ConnectorProgID, позволяющее заменять коннектор.
GetOperation(Name) Возвращает операцию из WSDL по имени.
ExecuteOperation(op) Выполняет указанную операцию. В случае ошибки генерирует IErrorInfo.

В лучших традициях компонентов SOAP Toolkit потоковая модель Proxy будет ‘Both’. Компонент будет агрегировать FTM (Free Threaded Marshaler).

Реализация методов Proxy полностью повторяет приведенный выше код на VB. Интерес представляет только обработка ошибок, которые может возвратить сервер – если после вызова ISoapReader::Load свойство Fault ненулевое, значит, сервер вернул ошибку. Ее код и описание нужно получить, используя объектную модель MS XML.

XML::IXMLDOMElementPtr spFault;
CheckError(spReader->get_FaultDetail(&spFault));
if(spFault)
{
  // сервер вернул ошибку
  HRESULT hCode = E_FAIL;
  XML::IXMLDOMNodeListPtr spList = 
      spFault->getElementsByTagName(OLESTR("mserror:returnCode"));
  if(spList->length)
  {
    hCode = _wtol(spList->item[0]->text);
  }
  _bstr_t description;
  spList = spFault->getElementsByTagName(OLESTR("mserror:description"));
  if(spList->length)
  {
    description = spList->item[0]->text;
  }
  _bstr_t source;
  spList = spFault->getElementsByTagName(OLESTR("mserror:source"));
  if(spList->length)
  {
    source = spList->item[0]->text;
  }
  ... // создаем IErrorInfo
  _com_error e(hCode, spInfo, true);
  throw e;
}

Чтобы использовать раннее связывание, т.е. вызов через vtable, мы создадим новый компонент и при помощи ATL-мастера “Implement Interface” добавим реализацию нужного интерфейса. Теперь в каждый метод интерфейса нужно добавить код, перенаправляющий вызов к нашей Proxy. Например, реализация метода GetProcesses, возвращающего один выходной параметр, может быть такой:

STDMETHOD(GetProcesses)(_Recordset * * ppRecordset)
{
  try
  {
    IWSDLOperationPtr spOp = 
     m_spSoapProxy->GetOperation(L"GetProcesses");
    m_spSoapProxy->Execute(spOp);
    IEnumSoapMappersPtr spEnum;
    spOp->GetOperationParts(&spEnum);
    while(true)
    {
     long l = 0;
     ISoapMapperPtr spMap;
     spEnum->Next(1, &spMap, &l);
     if(l == 1)
     {
      if(spMap->PartName == _bstr_t(L"Result"))
      {
        CheckError(spMap->ComValue.punkVal->QueryInterface(
           IID__Recordset, (void**)ppRecordset));
      }
     }
     elsebreak;
     }
  }
  catch(_com_error & e)
  {
    return e.Error();
  }      
  return S_OK;
}

В состав Visual Studio.NET входит генератор Proxy с ранним связыванием для протокола SOAP - sproxy.exe. С его помощью можно сгенерирован компонент, использующий поддержку SOAP в ATL 7.0 для передачи вызовов серверу. Генератор работает на основе WSDL-файлов. Главный недостаток этого генератора связан с тем, что полученный код будет компилироваться только VC++ 7.0 и ATL 7.0, поэтому для приложений, использующих SOAP Toolkit и/или написанных в VC++ 6.0 этот генератор непригоден.

Все что вы хотели знать о SOAP, но боялись спросить

В этом разделе приводятся наиболее часто встречаемые в конференциях вопросы о протоколе SOAP и SOAP Toolkit’е.

ПРИМЕЧАНИЕ

Не все DCOM-приложения запрашивают данные большими порциями. На сервере могут работать stateless COM+-компоненты, настроенные для обработки частых/коротких вызовов от клиентов. Для таких приложений переход с DCOM на SOAP может значительно ухудшить производительность.

Заключение

SOAP представляет собой нейтральный к платформе Firewall-friendly протокол, с помощью которого можно создавать как Web-, так и desktop-приложения, получающие данные с удаленного сервера через Internet. SOAP-приложения или их отдельные части могут разрабатываться с использованием:

ПРИМЕЧАНИЕ

Благодаря нейтральности протокола SOAP клиент и сервер могут быть реализованы с помощью разных программных средств. Например, сервер на ASP.NET может общаться с клиентом, разработанным с помощью SOAP Toolkit 3.0. Но взаимодействие с компонентами .NET по протоколу SOAP – тема отдельной статьи.

Несомненно, что протокол SOAP будет приобретать все большую популярность, благодаря его нейтральности к платформе, поддержке в Microsoft.NET (.NET Remoting), способности использовать любой транспорт, в том числе и HTTP в формате DIME.

Ссылки

SOAP Toolkit 3.0 может быть найден здесь:

http://msdn.microsoft.com/downloads/default.asp?URL=/downloads/sample.asp?url=/msdn-files/027/001/948/msdncompositedoc.xml

Раздел в MSDN, посвященный протоколу SOAP и всему, что с ним связано:

http://msdn.microsoft.com/library/default.asp?url=/nhp/Default.asp?contentid=28000523

Пример TView, использовавшийся для демонстрации перехода от DCOM к SOAP:

http://msdn.microsoft.com/msdnmag/issues/1200/tview/default.aspx

Информация об объектах и интерфейсах SOAP Toolkit’а может быть найдена в документации, которая поставляется в составе SOAP Toolkit 3.0.


Эта статья опубликована в журнале RSDN Magazine #1-2003. Информацию о журнале можно найти здесь
    Сообщений 4    Оценка 356        Оценить