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

DIME и XML Web-сервисы ATL Server

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

Источник: RSDN Magazine #3-2003
Опубликовано: 27.09.2003
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Структура DIME-сообщения
DIME в WSDL
SOAP-сообщения в формате DIME
Поддержка DIME в SOAP Toolkit 3.0
DIME в приложениях ATL Server
Автоматическая генерация WSDL
Расширяем WSDL
Классы для работы с DIME-записями
Классы для управления набором DIME-записей
Обработчик DIME-запросов
Серверный класс-обработчик запросов
Клиентский класс – Proxy
Создаем клиента с помощью SOAP Toolkit 3.0
Тестируем производительность
Заключение
Ссылки

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

Введение

Относительно недавно в мире SOAP-приложений и XML Web-сервисов появился новый термин – DIME (Direct Internet Message Encapsulation). DIME является спецификацией упаковки разнородных данных, таких, как XML, бинарные потоки, изображения JPEG и даже видео, в составе одного сообщения. Хотя DIME – формат общего назначения, разрабатывался он главным образом, чтобы обеспечить эффективную передачу данных по протоколу SOAP.

Главное преимущество (с точки зрения нейтральности к платформам, гибкости и расширяемости) и одновременно главный недостаток (с точки зрения эффективности) протокола SOAP – использование XML для описания запросов. XML обеспечивает структурированность и аккуратную работу с типами для SOAP-запросов. Корректность запроса с точки зрения структуры задается схемой. Но XML, являясь текстовым форматом описания, не способен эффективно описывать потоки бинарных данных. Такие данные для пересылки в составе XML-документа должны быть преобразованы в base64. Такое преобразование имеет несколько недостатков:

С передачей бинарных данных связано использование шифрования и цифровой подписи, так как зашифрованный XML уже не является текстом, а цифровая подпись представляет собой последовательность байтов. К тому же, после преобразования в base64 клиенту придется выполнять обратное преобразование как самих данных, так и цифровой подписи, чтобы проверить ее.

XML малопригоден не только для передачи бинарных данных, но и для передачи фрагментов других XML-документов, так как разные документы могут иметь совершенно разные структуры и использовать различные кодировки.

Из-за невозможности эффективной организации передачи разнородных данных в составе единого XML SOAP-сообщения был разработан новый формат DIME. Этот формат описывает способ объединения совокупности таких данных в составе одного сообщения. Сам по себе DIME является спецификацией общего назначения и не делает никаких предположений ни об используемом транспорте (это может быть HTTP или TCP/IP), ни о составе и структуре передаваемых данных.

DIME является далеко не первым форматом для упаковки набора данных в единое сообщение, всем хорошо известен MIME, широко используемый для отправки e-mail сообщений с прикрепленными файлами. Почему же в XML Web-сервисах вместо MIME, используется совершенно новый формат? MIME оперирует метаданными и является очень гибким и расширяемым форматом, но ради гибкости пришлось пожертвовать эффективностью – например, чтобы сосчитать количество вложений в формате MIME, нужно просмотреть весь поток данных. В некоторых статьях встречается определение DIME как “улучшенного MIME для XML Web-сервисов”. И действительно, формат DIME был спроектирован так, чтобы сделать разбор (и создание) сообщений максимально простым и эффективным. При обработке DIME-сообщения можно легко получить пятое по счету вложение (attachment), никак не анализируя и не обрабатывая остальные части сообщения.

ПРИМЕЧАНИЕ

Может быть, через несколько лет DIME заменит MIME и для электронной почты, став единым форматом передачи разнородных данных в составе одного сообщения.

DIME может использоваться в любых системах при необходимости передачи совокупности данных, представляющих собой одно логическое целое. В XML Web-сервисах DIME служит для “прикрепления” к XML-запросу любого количества таких дополнительных данных, как цифровая подпись, файлы и т.д. DIME-сообщение является бинарным, и тип такого сообщения при передаче по HTTP устанавливается как “application/dime”.

Структура DIME-сообщения

Любое DIME-сообщение представляет собой совокупность записей. Каждая запись содержит как характеристики данных в заголовке – тип, длина данных, идентификатор, так и сами данные. В первой записи DIME-сообщения устанавливается в 1 флаг-признак первой записи “MB” (Message Begin), а в последней – “ME” (Message End). Сообщение может состоять всего из одной записи, тогда будут установлены оба флага – MB и ME. DIME-сообщения могут вкладываться друг в друга, при этом одна из записей содержит в себе другое DIME-сообщение целиком.


Рисунок 1. DIME-запись.

ПРИМЕЧАНИЕ

В DIME используется big-endian представление чисел. Бит с номером 0 является самым значимым.

Заголовок записи состоит из следующих полей:

Поле Описание
VERSION Задает версию формата DIME. Все записи в одном DIME-сообщении должны содержать одинаковое значение версии. Если DIME-парсер не поддерживает указанную в записи версию, он должен отвергнуть сообщение целиком.
MB Флаг первой записи сообщения (1 бит). Первая запись сообщения должна обязательно устанавливать этот флаг в 1.
ME Флаг последней записи сообщения (1 бит). В сообщении, состоящем из одной записи, оба флага (MB и ME) должны быть установлены в одной записи.
CF Флаг, указывающий на то, что запись передает фрагмент данных. Данные целиком описываются последовательностью записей с установленным битом CF, последняя запись в последовательности сбрасывает бит CF в 0. Полная длина блока данных вычисляется как сумма длин фрагментов в каждой записи из последовательности.
TYPE_T 3-битное поле, описывающее содержимое поля TYPE. Наиболее распространенные значения поля TYPE_T – 1 (тогда TYPE содержит тип MIME) и 2 (TYPE содержит URI), значение 0 используется для данных, разбитых на несколько записей, и означает Unchanged, т.е. такое же, как и для первой записи последовательности. Значение 3 означает “Unknown” и используется в случаях, когда поле TYPE не содержит никакой информации.
OPTIONS_LENGTH 16 бит. Задает длину поля OPTIONS (без учета выравнивания по границ4 4 байт).
ID_LENGTH 16 бит. Длина поля ID (без учета выравнивания на границу 4 байт).
TYPE_LENGTH 16 бит. Длина поля TYPE(без учета выравнивания на границу 4 байт).
DATA_LENGTH 32 бита. Длина блока данных. Так как длина описывается 32-битным числом, запись может иметь длину до 4 Гб.
OPTIONS Предназначено для будущих расширений DIME. В настоящее время в этом поле могут находиться элементы, каждый из которых описывается типом ELEMENT_T (16 бит), ELEMENT_LENGTH (16 бит), ELEMENT_DATA
ID Задает идентификатор записи. Этот идентификатор задается в форме URI и позволяет ссылаться на запись другим записям в сообщении.
TYPE Тип данных в записи. Может быть MIME-типом (например, “application/dime” для вложенного DIME-сообщения) или URI. Интерпретация значения в этом поле зависит от значения в поле TYPE_T.
DATA Непосредственно блок данных, длина которого может достигать 4 Гб

Поля с переменной длиной (ID, TYPE) должны дополняться до длины, кратной 4 байтам.

ПРИМЕЧАНИЕ

DIME все еще не прошел стандартизации, и имеет статус “draft”. Поэтому после окончательного утверждения формат заголовка может измениться. Формат уже изменялся – так, в первой версии заголовок не включал в себя поля VERSION и OPTIONS.

DIME в WSDL

Применительно к протоколу SOAP DIME используется для передачи вместе с SOAP-сообщениями прикрепленной информации – файлов, вложенных SOAP-сообщений, цифровых подписей и т.п. Такие вложения получают уникальные идентификаторы, а в теле XML-сообщения могут содержаться ссылки на эти вложения. Главное, SOAP-сообщение всегда является первой DIME-записью, а вложения передаются как дополнительные записи. Главное преимущество использования DIME для SOAP – возможность избежать “раздувания” XML-сообщения и преобразования в base64, передавая данные в нетронутом виде (и тем самым избегая 30% увеличения размера).

Поскольку при общении по протоколу SOAP контракт между клиентом и сервером задается в WSDL-файле (Web Service Description Language), при использовании формата DIME в состав этого документа должны быть внесены специфические изменения, описывающие вложения.

В раздел <operation><input> или <operation><output> добавляется новый элемент <dime:message>, который описывает тип вложения. Тип вложения задается атрибутом “layout” элемента <dime:message> и может принимать два значения:

<binding name='PersistObjSoapBinding' type='wsdlns:PersistObjSoapPort' >
 <stk:binding preferredEncoding='UTF-8'/>
 <soap:binding style='rpc' transport='http://schemas.xmlsoap.org/soap/http'/>

 <operation name='GetEx'>
   <soap:operation soapAction=
        'http://tempuri.org/MapperSampleSrv/action/PersistObj.Get'/>
     <input>
       <soap:body use='encoded' namespace=   
         'http://tempuri.org/MapperSampleSrv/message/'
          encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'/>
     </input>
     <output>
       <dime:message layout= 
         'http://schemas.xmlsoap.org/ws/2002/04/dime/closed-layout' 
         wsdl:required='true' />
       <soap:body use='encoded' namespace=
         'http://tempuri.org/MapperSampleSrv/message/' encodingStyle=
         'http://schemas.xmlsoap.org/soap/encoding/' parts='pData'/>
     </output>
 </operation>
</binding>

Параметр метода, который будет передаваться в виде вложения, прикрепленного к основному сообщению, должен быть описан в WSDL-файле как сложный тип <complexType> с базовым типом base64binary, hexBinary или ReferencedBinary. Фрагмент WSDL-файла для описания соответствующего типа может выглядеть так:\

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

Такое описание ничего не говорит о том, какие данные должны передаваться с помощью такого параметра. Поэтому для описания типа добавляются три новых элемента:

<content:type value='urn:schemas-microsoft-com:rowset'/>
<content:mediaType value='image/jpeg'/>
<content:documentType value='tns:Report'/>

Значения из этих элементов будут использоваться и при установке полей TYPE и TYPE_T в DIME-записи. Если не указан ни один из элементов-описателей, поле TYPE_T записи будет установлено в значение 3 – “Unknown”.

ПРИМЕЧАНИЕ

Описания типа как URI, MIME или XML-документа являются взаимоисключающими, так как соответствующее поле TYPE_T в DIME-записи допускает лишь одно значение.

SOAP-сообщения в формате DIME

Использование DIME для создания составных SOAP-сообщений ограничивается набором правил, гарантирующих, что такое сообщение будет правильно интерпретироваться на любых системах и платформах.

Любое SOAP-сообщение в формате DIME должно обязательно включать в себя основное SOAP-сообщение в XML в качестве первой записи DIME и одну или несколько записей, описывающих прикрепленные вложения. Дополнительные записи используют в качестве идентификатора URI, с помощью которого можно ссылаться на них из первой записи. Главная (первая) запись должна использовать тип “http://schemas.xmlsoap.org/soap/envelope/”. Ниже приведен пример составного SOAP-сообщения, передающего изображение JPEG в качестве прикрепленного файла:

DIME Record 1: Primary SOAP message part/Главная часть SOAP сообщения
TNF:           2 
Type:          http://schemas.xmlsoap.org/soap/envelope/ 
Id:               


DIME Record 2: Secondary part/Прикрепленное изображение JPEG
TNF:           1 
Type:          image/jpeg 
Id:            uuid:09233523-345b-4351-b623-5dsf35sgs5d6

В виде дополнительных записей могут передаваться как вложения, ссылки на которые есть в основном сообщении (они называются “referenced attachments”), так и вложения, на которые основной документ не ссылается (они называются “unreferenced attachments”), но такие вложения должны идти после “referenced”.

Поддержка DIME в SOAP Toolkit 3.0

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

На сегодняшний день поддержку формата DIME включают в себя 2 продукта:

Обсуждение WSE, его возможностей и недостатков – тема отдельной статьи, а в этой речь пойдет о поддержке DIME в SOAP Toolkit, который можно свободно загрузить здесь: http://msdn.microsoft.com/downloads/default.asp?URL=/downloads/sample.asp?url=/msdn-files/027/001/948/msdncompositedoc.xml

Для передачи прикрепленных вложений в SOAP Toolkit включено несколько COM-объектов, каждый из которых предназначен для своего конкретного типа данных.

Название Описание
FileAttachment30 Позволяет передать файл (IFileAttachment)
StringAttachment30 Предназначен для передачи строк (IStringAttachment)
ByteArrayAttachment30 Передает массив байтов (IByteArrayAttachment)
StreamAttachment30 Передает данные в IStream (IStreamAttachment)

Чтобы автоматически включить поддержку DIME, достаточно описать параметр метода объекта, передающего вложение как один из перечисленных выше интерфейсов. Когда генератор WSDL встретит в библиотеке типов серверного компонента такой параметр, он добавит в WSDL-файл тег <dime:message> и описание типа. На приемной стороне параметр будет иметь уже другой тип – IReceivedAttachment, с помощью этого интерфейса полученное вложение можно сохранить в файл или произвести какую-нибудь обработку.

Добавляемые генератором WSDL описания используют “referenced” вложения, т.е. ссылка на эти вложения присутствует в главном сообщении, а в качестве ID используется GUID.

Чтобы “увидеть” DIME в действии, мы используем небольшой пример, который на сервере создает FilеAttachment:

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

А на клиенте сохраняет его в файл:

      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"

Для изучения передаваемого от сервера клиенту DIME-сообщения мы используем утилиту SOAPTrace, которая входит в состав SOAP Toolkit 3.0 и “понимает” формат 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="583" /> 
    <ID value="uuid:714C6C40-4531-442E-A498-3AC614200295" /> 
    <Type value="http://schemas.xmlsoap.org/soap/envelope/" /> 
  </DimeRecord>

  <DimeRecord traceOffset="0x000002AC">
    <Recordinfo Version="1" MB="0" ME="1" CF="0" IDLength="41" /> 
    <Typefield TNF="3" TypeLength="0" /> 
    <Options O="0" OptionLength="0" /> 
    <Datalength length="362130" /> 
    <ID value="uuid:80C4EE42-17FB-4291-998E-76818F606157" /> 
    </DimeRecord>
</DimePayload>

Наше DIME-сообщение состояло из двух записей, первая из которых являлась обычным SOAP-сообщением, а вторая использовалась для передачи файла (длина данных соответствует размеру файла).

Работать с форматом DIME можно и на низкоуровневом API SOAP Toolkit 3.0, используя напрямую коннекторы – вспомогательные компоненты DimeComposer30.

Можно использовать поддержку DIME и для передачи своих собственных типов данных, не ограничиваясь перечисленными выше четырьмя. Для этого придется реализовать свой mapper, преобразующий данные в бинарный поток и обратно, и добавить нужные элементы в WSDL-файл вручную, так как в этом случае WSDL-генератор нам уже не поможет. Законченный пример создания своего mapper-а для передачи данных в формате DIME можно найти в статье [1].

DIME в приложениях ATL Server

В XML Web-сервисах, разрабатываемых с помощью библиотеки ATL 7.0 и её части, известной под названием ATL Server, поддержки DIME нет.

ПРИМЕЧАНИЕ

Поддержки DIME нет ни в ATL 7.0, которая входит в Visual Studio .NET, ни в ATL 7.1 из Visual Studio .NET 2003

Но, с другой стороны, есть подробное описание формата DIME (см. предыдущие разделы), утилита для трассировки анализа DIME-сообщений SOAPTrace, возможность отладки с помощью клиента SOAP Toolkit 3.0 (в котором поддержка DIME есть) и, самое главное, у нас есть доступ к исходным текстам ATL и возможность создавать свои собственные классы, расширяющие функциональность тех, которые уже входят в состав ATL 7.0. Этим мы и займемся в этом разделе статьи.

ПРИМЕЧАНИЕ

Описание механизма работы XML Web-сервисов, написанных с помощью ATL Server, можно найти в статье [2].

Приведенный в статье код компилируется (и работает) с ATL 7.0 и ATL 7.1.

Прежде всего, нам необходимо выбрать типы данных, которые будут передаваться как индивидуальные DIME-записи. SOAP Toolkit предоставляет с этой целью несколько COM-объектов, с помощью которых можно передавать в виде вложений файлы, строки, потоки бинарных данных. В ATL Server мы ограничены типами, поддержка которых уже есть, так как аналога mapper-ов (преобразователей типов данных) для ATL Server не существует. Кроме того, у нас нет доступа к провайдеру атрибутов ATL, который отвечает за генерацию структур данных, описывающих методы и их параметры.

Поэтому в качестве “DIME-типа данных” мы выберем ATLSOAP_BLOB и будем использовать следующий критерий – если в методе присутствует хотя бы один параметр с типом ATLSOAP_BLOB, будет использоваться DIME, в противном случае – обычное XML-сообщение.

ПРИМЕЧАНИЕ

В нашей простой реализации мы можем ограничиться типом ATLSOAP_BLOB. В более сложных случаях придется ждать, когда в ATL появится поддержка DIME, или писать код, который изменяет структуры, генерируемые провайдером атрибутов ATL (рискуя потерять совместимость с будущими версиями ATL), и изменять код маршалинга для параметров, добавляя свои типы для DIME-вложений, например, IFileAttachment* и т.п.

Автоматическая генерация WSDL

XML Web-сервисы ATL Server генерируют WSDL автоматически на основе информации о методах и их параметрах, которая создается провайдером атрибутов ATL после обработки атрибутов “soap_method” и “soap_header” в исходном коде.

В первую очередь нам потребуется изменить механизм генерации WSDL так, чтобы добавить необходимые элементы – тег <dime:message> для описания операций и описание типа данных IStream как сложного типа, передаваемого в виде DIME-вложения.

Механизм автоматической генерации WSDL и примеры структур, описывающих методы и их параметры, можно найти в статье [2].

WSDL-файл генерируется классом CSDLGenerator. Ссылка на него добавляется в главный класс макросом HANDLER_ENTRY_SDL или атрибутом sdl в объявлении класса:

HANDLER_ENTRY_SDL("Default", CHelloWorldService,
                      ::HelloWorldService::CHelloWorldService, GenHelloWorldWSDL)

Класс CSDLGenerator создает WSDL-файл на основе строкового шаблона, хранящегося в глобальной переменной s_szAtlsWSDLSrf. Дляформирования динамической части шаблонаиспользуетсяклассCStencil.КогдаCStencilвстречаетвшаблонесоответствующуюинструкцию, онвызываетметодклассаCSDLGenerator, которыйиподставляетдинамическуючастьшаблона. Методы, генерирующиединамическуючасть, задаютсямакросами:

BEGIN_REPLACEMENT_METHOD_MAP(_CSDLGenerator)
    REPLACEMENT_METHOD_ENTRY("GetNamespace", OnGetNamespace)
END_REPLACEMENT_METHOD_MAP()

В SRF шаблоне имена этих методов называются тегами. А для обработки SRF используется специальный интерфейс ITagReplacer. Если в SRF встречается соответствующий тег, с помощью этого интерфейса вызываются нужные методы-обработчики (ATL-реализация этого интерфейса называется ITagReplacerImpl).

CSDLGenerator реализует интерфейс ITageReplacer, используя вспомогательный класс _CSDLGenerator, который и определяет карту методов-обработчиков тегов SRF. Чтобы расширить генерируемый WSDL дополнительными элементами, нужно, во-первых, изменить сам шаблон SRF, который задается как глобальная переменная, хранящая константную строчку, а во-вторых, расширить карту обработчиков тегов SRF дополнительными обработчиками для типа ATLSOAP_BLOB.

Первая сложность, с котором нам предстоит столкнуться, состоит в том, что для карты методов-обработчиков SRF (в отличие от карты интерфейсов ATL) нет макроса REPLACEMENT_METHOD_CHAIN. Поэтому нельзя просто создать производный от _CSDLGenerator класс и связать его карту обработчиков с картой класса _CSDLGenerator. Но все же существует способ распределить обработку SRF между несколькими классами. Для этой цели служит метод FindReplacementOffset интерфейса ITagReplacer, который используется во время генерации WSDL по шаблону SRF:

HTTP_CODE FindReplacementOffset(
  LPCSTR szMethodName,     // имя метода-обработчика в SRF
  DWORD * pdwMethodOffset, // числовой идентификатор метода
  LPCSTR szObjectName,     // имя объекта, предоставляющего метод-обработчик
  DWORD * pdwObjOffset,    // числовой идентификатор объекта
  DWORD * pdwMap,          // карта обработчиков, содержащая реализацию методаvoid ** ppvParam,        // аргумент обработчика
  IAtlMemMgr * pMemMgr     // распределитель памяти ATL
);

Перед обработкой SRF-файла CStencil вызывает FindReplacementOffset для каждого найденного в SRF метода-обработчика. ATL-реализация интерфейса ITagReplacer, ITagReplacerImpl, в зависимости от значения pdwObjOffset либо вернет карту обработчиков текущего класса, либо, если FindReplacementObject вернула ненулевое значение в pdwObjOffser, у класса, унаследованного от ITagReplacerImpl будет вызван метод GetReplacementObject. Этот метод должен предоставить альтернативный интерфейс ITagReplacer для заданного значения pdwObjOffset.

HTTP_CODE GetReplacementObject(
   DWORD dwObjOffset    // числовой идентификатор объекта
   ITagReplacer ** ppReplacer  // альтернативный ITagReplacer
);

Чтобы распределить обработку SRF по нескольким классам (используя _CSDLGenerator для генерации основной части WSDL и дополнительный класс для генерации DIME-элементов в WSDL), мы поступим так:

        template<class THandler, constchar* szHandlerName>
  class _CSDLGeneratorDIME : public ATL::ITagReplacerImpl<_CSDLGeneratorDIME>
{
};

Параметр шаблона szHandlerName задает имя обработчика SRF, а параметр THandler задает класс-обработчик входящих запросов на генерацию WSDL, который будет использовать _CSDLGeneratorDIME:

BEGIN_REPLACEMENT_METHOD_MAP(_CSDLGeneratorDIME)
  REPLACEMENT_METHOD_ENTRY("GetNextFunction", OnGetNextFunction)
...
END_REPLACEMENT_METHOD_MAP()
CComObjectStack<_CSDLGeneratorImpl> m_StdGen;

Здесь _CSDLGeneratorImpl – унаследованный от _CSDLGenerator класс, который определяет чисто виртуальные функции класса _CSDLGenerator.

HTTP_CODE FindReplacementOffset(
        LPCSTR szMethodName,
        DWORD *pdwMethodOffset,
        LPCSTR szHandlerName,
        DWORD *pdwHandlerOffset,
        DWORD *pdwMap, void **ppvParam, IAtlMemMgr *pMemMgr)
{
   HTTP_CODE code = replacerImpl::FindReplacementOffset(szMethodName,
    pdwMethodOffset, szHandlerName, pdwHandlerOffset, pdwMap, ppvParam, pMemMgr);

  if(code != HTTP_SUCCESS)
  {
      code = m_StdGen.FindReplacementOffset(szMethodName, pdwMethodOffset, 
         szHandlerName, pdwHandlerOffset, pdwMap, ppvParam, pMemMgr);
      if(code == HTTP_SUCCESS)
      {
         *pdwHandlerOffset = GetStdGenCookie();
      }
  }
  return code;
}

Если нужный обработчик находится в классе _CSDLGenerator, возвращается специальное значение pdwObjOffset, которое затем используется в методе GetReplacementObject так:

HTTP_CODE GetReplacementObject(DWORD dwObjOffset, ITagReplacer **ppReplacer)
{
  if(dwObjOffset == GetStdGenCookie())
  {
    *ppReplacer = &m_StdGen;
    return HTTP_SUCCESS;
  }
  return HTTP_FAIL;
}

Теперь SRF генерируется с помощью двух классов – главного, _CSDLGeneratorDIME, который добавляет в WSDL элементы, специфичные для DIME, и классом _CSDLGenerator из библиотеки ATL, который обрабатывает остальную часть шаблона SRF.

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

Как уже упоминалось, за генерацию WSDL отвечает класс CSDLGenerator (без символа подчеркивания, не путать с _CSDLGenerator). Этот класс является частью библиотеки ATL. Именно он обслуживает входящие HTTP-запросы на генерацию WSDL.

Класс реализации Web-сервиса использует CSDLGenerator либо объявлением атрибута sdl:

[
  request_handler(name="Default" , sdl="GenDIMESenderWSDL"),
  soap_handler(
    name="DIMESenderService", 
    namespace="urn:DIMESenderService",
    protocol="soap"
  )
]
class CDIMESenderService

либо явным указанием макроса:

HANDLER_ENTRY_SDL("Default", CHelloWorldService,
                  ::HelloWorldService::CHelloWorldService, GenHelloWorldWSDL)

При получении запроса с заданным именем (например, “GenDIMESenderWSDL”) управление получит экземпляр CSDLGenerator. Он сформирует WSDL (в методе InitializeHandler), обрабатывая SRF-шаблон с помощью класса CStencil и реализации ITagReplacer – _CSDLGenerator.

Чтобы использовать альтернативную обработку SRF, нам придется создать свой класс-аналог CSDLGenerator, который будет генерировать WSDL с помощью нашего вспомогательного класса _CSDLGeneratorDIME и измененного шаблона SRF.

Наш класс-генератор WSDL будет называться CSDLGeneratorDIME, а его реализация будет очень похожей на реализацию ATL:

        template <class THandler, constchar *szHandlerName>
class CSDLGeneratorDIME :
  public _CSDLGeneratorDIME<THandler, szHandlerName>,
  public IRequestHandlerImpl<CSDLGeneratorDIME>,
  public CComObjectRootEx<CComSingleThreadModel>
{
private:

public:
  typedef CSDLGeneratorDIME<THandler, szHandlerName> _sdlGenerator;

  BEGIN_COM_MAP(_sdlGenerator)
    COM_INTERFACE_ENTRY(IRequestHandler)
    COM_INTERFACE_ENTRY2(ITagReplacer, replacerImpl)
  END_COM_MAP()

  HTTP_CODE InitializeHandler(AtlServerRequest *pRequestInfo, 
    IServiceProvider *pServiceProvider)
  {
    IRequestHandlerImpl<CSDLGeneratorDIME>::
      InitializeHandler(pRequestInfo, pServiceProvider);

    CComObjectStack<THandler> handler;
    if (FAILED(InitializeSDL(&handler)))
      return HTTP_FAIL;

    CStencil s;
    HTTP_CODE hcErr = s.LoadFromString(s_szAtlsWSDLSrfDIME, 
      (DWORD) strlen(s_szAtlsWSDLSrfDIME));
    if (hcErr != HTTP_SUCCESS)
      return hcErr;

    CHttpResponse HttpResponse(pRequestInfo->pServerContext);
    HttpResponse.SetContentType("text/xml");
    if (s.ParseReplacements(this) == false)
      return HTTP_FAIL;

    s.FinishParseReplacements();
    SetStream(&HttpResponse);
    SetWriteStream(&HttpResponse);
    SetHttpServerContext(m_spServerContext);
    hcErr = s.Render(this, &HttpResponse);

    return hcErr;
  }

  constchar * GetHandlerName()
  {
    return szHandlerName;
  }
};

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

ПРИМЕЧАНИЕ

Приведенная выше реализация метода InitializeHandler имеет важное отличие от стандартной ATL-реализации – в метод InitializeSDL передается указатель на тип THandler, тогда как в ATL-реализации передается указатель на базовый класс CSoapRootHandler. Дело в том, что для генерации WSDL необходима информация о SOAP-методах и типах их параметров, а в классе CSoapRootHandler соответствующие методы для доступа к этой информации объявляются с модификатором доступа “protected”. Код ATL имеет к ним доступ, так как класс _CSDLGenerator объявляется другом класса CSoapRootHandler, но нашу реализацию _CSDLGeneratorDIME другом никто не объявлял. К счастью, в классе реализации WEB-сервиса те же самые виртуальные методы объявляются с модификатором доступа “public”, поэтому если получать доступ к информации о SOAP-методах и типах через указатель на этот класс (THandler), код будет скомпилирован успешно.

Для упрощения использования класса CSDLGeneratorDIME клиентом определим макрос HANDLER_ENTRY_SDL_DIME по аналогии с макросом HANDLER_ENTRY_SDL.

Пользовательский класс Web-сервиса будет использовать генератор так:

        namespace DIMESEnderService
{
[
  request_handler(name="Default"/*, sdl="GenDIMESenderWSDL" */ ),
  soap_handler(
    name="DIMESenderService", 
    namespace="urn:DIMESenderService",
    protocol="soap"
  )
]
class CDIMESenderService :
  public IDIMESenderService
{
public:
...
}; // класс CDIMESenderService

} // пространство имен DIMESenderService

HANDLER_ENTRY_SDL_DIME("Default", CDIMESenderService,
           ::DIMESenderService::CDIMESenderService, GenDIMESenderWSDL)

Мы закомментировали атрибут sdl, чтобы не использовался стандартный ATL-генератор CSDLGenerator, и добавили макрос HANDLER_ENTRY_SDL_DIME, который делает наш класс CSDLGeneratorDIME обработчиком запроса “GenDIMESenderWSDL”.

Теперь у нас появилась возможность изменять шаблон SRF (который задается глобальной переменной s_szAtlsWSDLSrfDIME) и добавлять в WSDL новые элементы, задавая обработчики тегов в классе _CSDLGeneratorDIME.

Ниже приведена таблица с описаниями классов ATL и наших классов, участвующих в генерации WSDL:

Класс/Интерфейс Описание
CSoapRootHandler (ATL) Базовый класс для класса реализации Web-сервиса. Объявляет виртуальные функции для доступа к информации о SOAP-методах и их параметрах, которая используется для генерации WSDL.
CStencil (ATL) Заполняет шаблон SRF, используя передаваемый интерфейс ITagReplacer. Используется классом CSDLGenerator и CSDLGeneratorDIME.
ITagReplacer (ATL) Интерфейс, используемый классом CStencil для вызова обработчиков тегов SRF шаблона.
ITagReplacerImpl (ATL) ATL-реализация интерфейса ITagReplacer. Использует карту обработчиков и позволяет использовать несколько карт в различных классах с помощью метода GetReplacementObject.
_CSDLGenerator (ATL) Реализует интерфейс ITagReplacer, унаследован от ITagReplacerImpl, задает карту обработчиков тегов для стандартного SRF. Является базовым классом для CSDLGenerator.
CSDLGenerator (ATL) Обработчик HTTP-запроса на генерацию WSDL. Использует SRF-шаблон, CStencil и _CSDLGenerator для создания WSDL “на лету”. Использует информацию о SOAP-методах и их параметрах, вызывая виртуальные функции экземпляра THandler, унаследованного от CSoapRootHandler
_CSDLGeneratorDIME (наша реализация) Унаследован от ITagReplacerImpl. Задает дополнительную карту обработчиков тегов SRF для внесения в WSDL дополнительных, специфичных для DIME, элементов. Использует экземпляр класса _CSDLGenerator для обработки стандартной части SRF шаблона.
CSDLGeneratorDIME (наша реализация) Обработчик HTTP-запроса на генерацию WSDL. Использует SRF-шаблон, CStencil и _CSDLGeneratorDIME для создания WSDL “на лету”. Использует информацию о SOAP-методах и их параметрах, вызывая виртуальные функции экземпляра THandler, унаследованного от CSoapRootHandler.

Расширяем WSDL

Теперь у нас есть способ добавлять нужные элементы в автоматически генерируемый WSDL. Чтобы использовать DIME-вложения, нам нужно включить в WSDL-описание типа ATLSOAP_BLOB:

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

а также указание использовать DIME в описании операций, параметры которых имеют тип IStream:

<operation name='GetEx'>
  <soap:operation soapAction=
      'http://tempuri.org/MapperSampleSrv/action/PersistObj.Get'/>
    <input>
...
    </input>
    <output>
      <dime:message layout= 
        'http://schemas.xmlsoap.org/ws/2002/04/dime/closed-layout' 
        wsdl:required='true' />
    ...
    </output>
</operation>

В первую очередь нам необходимо добавить в шаблон SRF пространство имен DIME:

        "  xmlns:dime=\"http://schemas.xmlsoap.org/ws/2002/04/dime/wsdl/\"\r\n" \

В раздел “schema” шаблона мы добавим фрагмент, описывающий тип Blob:

        "{{if 
        HasBlobParams
        }}\r\n" \
"  <s:complexType  name =\"Blob\">\r\n" \
"    <s:simpleContent>\r\n" \
"      <s:restriction base=\"typens:ReferencedBinary\">\r\n" \
"        <s:annotation>\r\n" \
"          <s:appinfo/>\r\n" \
"          </s:annotation>\r\n" \
"        </s:restriction>\r\n" \
"      </s:simpleContent>\r\n" \
"    </s:complexType>\r\n" \
"{{endif}}\r\n" \

Описание типа “Blob” будет добавлено в схему, только если обработчик тега “HasBlobParams” вернет HTTP_SUCCESS.

В раздел операций WSDL мы добавим указание использовать DIME, если у соответствующего SOAP-метода есть параметр типа ATLSOAP_BLOB:

        "{{if 
        IsInDimeMessage
        }}\r\n" \
"  <dime:message layout=\"http://schemas.xmlsoap.org/ws/2002/04/dime/closed-layout\" wsdl:required=\"true\"/>\r\n" \
"{{endif}}\r\n" \

В класс _CSDLGeneratorDIME мы добавим реализацию соответствующих обработчиков тегов SRF. Карта обработчиков этого класса выглядит так:

BEGIN_REPLACEMENT_METHOD_MAP(_CSDLGeneratorDIME)
    REPLACEMENT_METHOD_ENTRY("IsParameterUDT", OnIsParameterUDT)
    REPLACEMENT_METHOD_ENTRY("GetNextParameter", OnGetNextParameter)
    REPLACEMENT_METHOD_ENTRY("GetParameterSoapType", OnGetParameterSoapType)
    REPLACEMENT_METHOD_ENTRY("IsInDimeMessage", OnIsInDIMEMessage)
    REPLACEMENT_METHOD_ENTRY("IsOutDimeMessage", OnIsOutDIMEMessage)
    REPLACEMENT_METHOD_ENTRY("HasBlobParams", OnHasBlobParams)
    REPLACEMENT_METHOD_ENTRY("GetNextFunction", OnGetNextFunction)
END_REPLACEMENT_METHOD_MAP()

В методе InitializeSDL этого класса мы сохраним указатели на структуры с описанием SOAP-методов и их параметров:

HRESULT InitializeSDL(THandler *pHdlr)
{
  m_bIncludeBlobDef = false;
  m_pFuncs = pHdlr->GetFunctionMap();

  if (m_pFuncs == NULL)
  {
    return E_FAIL;
  }

  size_t i;
  for (i=0; m_pFuncs[i] != NULL; i++)
  {
    if(HasBlobParam(m_pFuncs[i], SOAPFLAG_IN) ||
      HasBlobParam(m_pFuncs[i], SOAPFLAG_OUT))
    {
      m_bIncludeBlobDef = true;
      break;
    }
  }
  return m_StdGen.InitializeSDL(pHdlr);
}

Помимо всего прочего, метод InitializeSDL просматривает описания всех SOAP-методов и устанавливает переменную-член m_bIncludeBlobDef, если хотя бы один из параметров имеет тип ATLSOAP_BLOB.

Реализация обработчики тегов SRF приведена ниже:

HTTP_CODE OnGetNextFunction()
{
  HTTP_CODE code = m_StdGen.OnGetNextFunction();
  if(code == HTTP_SUCCESS)
  {
    m_nFunc++;
  }
  elseif(code == HTTP_S_FALSE)
  {
    m_nFunc = -1;
  }
  return code;
}

HTTP_CODE OnIsInDIMEMessage()
{
  return HasBlobParam(m_pFuncs[m_nFunc], SOAPFLAG_IN) ?
    HTTP_SUCCESS : HTTP_S_FALSE;
}

HTTP_CODE OnIsOutDIMEMessage()
{
  return HasBlobParam(m_pFuncs[m_nFunc], SOAPFLAG_OUT) ? 
    HTTP_SUCCESS : HTTP_S_FALSE;
}

HTTP_CODE OnHasBlobParams()
{
  return m_bIncludeBlobDef ? HTTP_SUCCESS : HTTP_S_FALSE;
}

HTTP_CODE OnGetNextParameter()
{
  HTTP_CODE code = m_StdGen.OnGetNextParameter();
  if(code == HTTP_SUCCESS)
  {
    m_nParam++;
  }
  elseif(code == HTTP_S_FALSE)
  {
    m_nParam = -1;
  }
  return code;
}

HTTP_CODE OnGetParameterSoapType()
{
  if ((m_pFuncs[m_nFunc]->pEntries[m_nParam].nVal ==
    SOAPTYPE_BASE64BINARY) &&  m_bIncludeBlobDef)
  {
    constchar* szBlob = "Blob";
    HRESULT hr = m_pWriteStream->WriteStream(szBlob,
     (DWORD)strlen(szBlob), 0);
    return SUCCEEDED(hr) ? HTTP_SUCCESS : HTTP_FAIL;
  }
  elsereturn m_StdGen.OnGetParameterSoapType();
}

HTTP_CODE OnIsParameterUDT()
{
  if((m_pFuncs[m_nFunc]->pEntries[m_nParam].nVal ==
    SOAPTYPE_BASE64BINARY) &&  m_bIncludeBlobDef)
  {
    return HTTP_SUCCESS;
  }
  elsereturn m_StdGen.OnIsParameterUDT();
}

void SetWriteStream(IWriteStream *pStream)
{
  m_pWriteStream = pStream;
  m_StdGen.SetWriteStream(pStream);
}

void SetHttpServerContext(IHttpServerContext *pServerContext)
{
  m_StdGen.SetHttpServerContext(pServerContext);
}
    
bool HasBlobParam(const _soapmap* pMap, SOAPFLAGS flags)
{
  size_t j = 0;
  bool bResult = false;
  while(pMap->pEntries[j].nHash)
  {
    const _soapmapentry& entry = pMap->pEntries[j];
    if((entry.nVal == SOAPTYPE_BASE64BINARY ) && 
      (entry.dwFlags & flags ))
    {
      bResult = true;
      break;
    }
    ++ j;
  }
  return bResult;
}

Вспомогательный метод HasBlobParam возвращает true, если один из параметров метода (входной или выходной – задается флагами) имеет тип ATLSOAP_BLOB.

Описание текущего SOAP-метода в карте задается переменной-членом m_nFunc, значение которой увеличивается после каждого вызова OnGetNextFunction, а текущий параметр метода – переменной m_nParam. OnGetParameterSoapType записывает в выходной поток имя нашего типа – “Blob”, методы OnIsInDIMEMessage и OnIsOutDIMEMessage определяют необходимость включения в описание SOAP-операции указание использовать DIME, а метод OnIsParameterUDT необходим для того, чтобы добавить описание типа “Blob” в правильном пространстве имен XML.

ПРИМЕЧАНИЕ

Обработчики тегов SRF, реализованные в классе CSDLGeneratorDIME, выполняют необходимую обработку и, если нужно, передают управление стандартному классу _CSDLGenerator.

Классы для работы с DIME-записями

У нас есть генератор “правильного” WSDL. Пришла пора реализовать пару классов для работы с DIME-записями и чтения/сохранения их в поток. Класс DIMEHdr управляет заголовком DIME-записи:

        typedef CFixedStringT<CStringA, ID_LENGTH> id_string_t;

enum DIMETypeT
{
  ettUnchanged = 0,
  ettMime = 1,
  ettURI = 2,
  ettUnknown = 3
};
inline HRESULT PadBytes(IWriteStream* pStm, long len)
{
  // дополняет нулями до длины, кратной 4 байтам.
}
inline HRESULT SkipPadBytes(IStream* pStm, long len)
{
  // пропускает байты для выравнивания на границу, кратную 4
}
inline HRESULT PutString(IWriteStream* pStm, constchar* str, long nLen)
{
  // сохраняет строку с учетом выравнивания
}
inline HRESULT LoadString(IStream* pStm, LPSTR data, long len)
{
  // считывает строку с учетом выравнивания
}
inline id_string_t CreateRecordID()
{
  // создает GUID и преобразует его в строку
}

class DIMEHdr
{
public:
  DIMEHdr(const id_string_t& rec_id, 
        bool mb, bool me, DIMETypeT type_t = ettUnknown, 
        constchar* type = NULL)
  {
    ZeroMemory(&hdr, sizeof(hdr));
    hdr.version = DIME_VERSION;
    hdr.type_t = type_t;
    hdr.mb = mb;
    hdr.me = me;
    SetType(type);
    SetID(rec_id);
  }
  constchar* GetID();
  void SetME(bool me);
  bool GetME();
  void SetID(const id_string_t& rec_id);
  void SetType(constchar* type);
  void SetDataLength(unsigned__int32 len);
  unsigned__int32 GetDataLength();

  HRESULT SaveToStream(IWriteStream* pStm)
  {
    HRESULT hr = pStm->WriteStream((LPCSTR)&hdr, sizeof(hdr), 0);
    if(SUCCEEDED(hr))
    {
      hr = PutString(pStm, m_id, m_id.GetLength());
      if(SUCCEEDED(hr))
      {
        hr = PutString(pStm, m_type, m_type.GetLength());
      }
    }
    return hr;
  }

  HRESULT LoadFromStream(IStream* pStm)
  {
    HRESULT hr = pStm->Read(&hdr, sizeof(hdr), 0);
    if(SUCCEEDED(hr))
    {
      if(hdr.version != DIME_VERSION)
        return E_FAIL;
      long id_len = ntohs(hdr.id_length);
      long type_len = ntohs(hdr.type_length);
      hr = LoadString(pStm, m_id.GetBuffer(), id_len);
      if(SUCCEEDED(hr))
        hr = LoadString(pStm, m_type.GetBuffer(), type_len);
    }
    return hr;
  }
private:
  struct
  {
    bool cf:1;
    bool me:1;
    bool mb:1;
    byte version:5;
    byte opt_t:4;
    byte type_t:4;
    unsigned__int16 options_length;
    unsigned__int16 id_length;
    unsigned__int16 type_length;
    unsigned__int32 data_length; 
  } hdr;
  CFixedStringT<CStringA, ID_LENGTH> m_id;
  CFixedStringT<CStringA, MAX_PATH> m_type;
};

Структура заголовка задается анонимной структурой, члены которой имеют соответствующие длины в битах.

Управление DIME-записью осуществляется с помощью класса DIMERecord:

        class DIMERecord 
{
public:
  DIMERecord(IAtlMemMgr* pMgr, bool mb, bool me,
    const id_string_t& rec_id, DIMETypeT type_t = ettUnknown, 
    constchar* type = NULL) :
  hdr(rec_id, mb, me, type_t, type), m_pMgr(pMgr) {}

  constchar* GetID();

  HRESULT LoadFromStream(IStream* pStm, ATLSOAP_BLOB& data)
  {
    HRESULT hr = hdr.LoadFromStream(pStm);
    if(SUCCEEDED(hr))
    {
      unsigned__int32 len = hdr.GetDataLength();
      data.data = (byte*)m_pMgr->Allocate(len);
      data.size = len;
      hr = pStm->Read(data.data, len, 0);
      if(SUCCEEDED(hr))
        hr = SkipPadBytes(pStm, len);
    }
    if(hdr.GetME())
      hr = S_FALSE;
    return hr;
  }

  HRESULT SaveToStream(IWriteStream* pStm, ATLSOAP_BLOB data)
  {
    hdr.SetDataLength(data.size);
    HRESULT hr = hdr.SaveToStream(pStm);
    if(SUCCEEDED(hr))
    {
      hr = pStm->WriteStream((LPCSTR)data.data, data.size, 0);
      if(SUCCEEDED(hr))
        hr = PadBytes(pStm, data.size);
    }
    return hr;
  }

private:
  DIMEHdr hdr;
  IAtlMemMgr* m_pMgr;
};

Нужно отметить, что вся работа с памятью в этом классе (как и во всех остальных) осуществляется через интерфейс IAtlMemMgr. Это гарантирует, что способы распределения и освобождения блока памяти будут одинаковыми. Например, в приведенном выше коде данные из потока загружаются в структуру ATLSOAP_BLOB, поля которой будут затем освобождаться кодом ATL с помощью того же интерфейса IAtlMemMgr.

Классы для управления набором DIME-записей

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

Первый класс, SendAttachmentsHandler, выполняет такие функции:

        class SendAttachmentsHandler : public IWriteStream
{
public:  
  typedef std::vector<std::pair<id_string_t, ATLSOAP_BLOB> > attachments_t;

  // реализация интерфейса IWriteStream
  HRESULT WriteStream(LPCSTR szOut,int nLen, DWORD * pdwWritten)
  {
    return m_MainStm.WriteStream(szOut, nLen, pdwWritten);
  }
  HRESULT FlushStream( )
  {
    return m_MainStm.FlushStream();
  }

  void CleanUp()
  {
    m_Attachments.clear();
    m_MainStm.Cleanup();
  }
  HRESULT Init(IAtlMemMgr* pMgr)
  {
    m_pMgr = pMgr;
    return S_OK;
  }
  id_string_t AddAttachment(ATLSOAP_BLOB data)
  {
    id_string_t href = CreateRecordID();
    m_Attachments.push_back(std::make_pair(href,data));
    return href;
  }
  size_t GetAttachCount()
  {
    return m_Attachments.size();
  }

  CStringA& GetXMLString()
  {
    return m_MainStm.m_str;
  }

  // сохраняет DIME-записи в поток
  HRESULT SaveToStream(IWriteStream* pStm)
  {
    HRESULT hr = S_OK;
    if(m_Attachments.size())
    {
      // если есть бинарные данные, записать в формате DIME
      ATLSOAP_BLOB data;
      data.data = (byte*)m_MainStm.m_str.GetBuffer();
      data.size = m_MainStm.m_str.GetLength();
      DIMERecord main(m_pMgr, true, m_Attachments.size() == 0,
        CreateRecordID(), ettURI,
        "http://schemas.xmlsoap.org/soap/envelope/");
      hr = main.SaveToStream(pStm, data);
      if(SUCCEEDED(hr))
      {
        attachments_t::iterator it = m_Attachments.begin();
        for( ; it != m_Attachments.end(); ++it)
        {
          bool bLast = (it + 1 == m_Attachments.end());
          DIMERecord rec(m_pMgr, false, bLast, it->first);
          hr = rec.SaveToStream(pStm, it->second);
          if(FAILED(hr)) break;
        }
      }
    }
    else// если бинарных данных нет, записать в виде текста
      hr = pStm->WriteStream((LPCSTR)m_MainStm.m_str.GetBuffer(),
        m_MainStm.m_str.GetLength(), 0);
    return hr;
  }

  CWriteStreamOnCString m_MainStm;
  attachments_t m_Attachments;
  IAtlMemMgr* m_pMgr;
};

Для записи в главное XML-сообщение используется вспомогательный класс CWriteStreamOnCString, который входит в состав библиотеки ATL Server и реализует интерфейс IWriteStream на строке CString.

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

        class RecvAttachmentsHandler
{
public:
  typedef std::map<id_string_t, ATLSOAP_BLOB> attachments_t;
  ~RecvAttachmentsHandler()
  {
    CleanUp();
  }
  void CleanUp()
  {
    ClearAttachments(m_Attachments, m_pMgr);
  }
  // возвращает главное XML-сообщение
  IStream* GetXMLStream()
  {
    return m_bDime ? &m_MainStm : m_spStm;
  }
  HRESULT Init(IAtlMemMgr* pMgr, const CStringA& ctx, IStream* pStm)
  {
    m_pMgr = pMgr;
    m_spStm = pStm;
    m_bDime = (stricmp(ctx, "application/dime") == 0);
    if(m_bDime)
      return ParseRequest();
    return S_OK;
  }
  // возвращает данные из DIME-записи по ее ID
  ATLSOAP_BLOB GetAttachment(const CStringA& href)
  {
    ATLSOAP_BLOB res = {0};
    attachments_t::iterator it = m_Attachments.find(href);
    if(it != m_Attachments.end())
    {
      res = it->second;
      m_Attachments.erase(it);
    }
    return res;
  }
  bool AttachmentExists(const CStringA& href)
  {
    return (m_Attachments.find(href) != m_Attachments.end());
  }
protected:
  // загружает набор DIME-записей и их данные из потока
  HRESULT ParseRequest()
  {
    CleanUp();

    HRESULT hr = S_OK;
    while(true)
    {
      ATLSOAP_BLOB blob = {0};
      DIMERecord rec(m_pMgr, false, false, "");
      hr = rec.LoadFromStream(m_spStm, blob);
      if(SUCCEEDED(hr))
      {
        if(m_Attachments.size() == 0)
        {
          m_MainStm.init(blob.data, blob.size);
        }
        m_Attachments[rec.GetID()] = blob;
        if(hr == S_FALSE)
          break;
      }
      elsebreak;
    }
    return hr;
  }

  IAtlMemMgr* m_pMgr;
  CComPtr<IStream> m_spStm;
  CStringA m_MainRecordID;
  CComObjectStackEx<CMemStream> m_MainStm;
  attachments_t m_Attachments;
  bool m_bDime;
};

Для получения главного XML-сообщения служит метод GetXMLStream, а за разбор входных данных в формате DIME отвечает метод ParseRequest.

Реализацию интерфейса IStream для блока данных ATLSOAP_BLOB мы поручим классу CMemStream:

CComObjectStackEx<CMemStream> m_MainStm;
...
m_MainStm.init(blob.data, blob.size);

Реализация этого класса приводится в [1].

Обработчик DIME-запросов

У нас уже есть все необходимые вспомогательные классы, с помощью которых мы можем сохранять и загружать из потока наборы DIME-записей вместе с их данными. Сейчас нам нужно найти подходящий способ изменить логику работы обычного обработчика запросов ATL Server так, чтобы передавать и принимать параметры с типом ATLSOAP_BLOB в формате DIME.

Основной класс ATL Server, организующий обработку SOAP-запросов – CSoapHandler<THandler>, здесь THandler – пользовательский класс, который будет унаследован от CSoapHandler<> явно или неявно (при использовании атрибута “soap_handler”). Класс CSoapHandler<> унаследован от базового класса CSoapRootHandler. Этот базовый класс реализует всю логику генерации и разбора SOAP-сообщений, а также вызова методов пользовательского класса, и используется в качестве базового класса как для серверного кода, так и для клиентского. В том числе CSoapRootHandler отвечает и за преобразования различных типов данных в XML-представление и обратно.

Так как мы хотим изменить представление типа ATLSOAP_BLOB в SOAP-сообщении, нам нужно переопределить соответствующие методы CSoapRootHandler. Детальное изучение этого класса показывает, что:

        inline HRESULT AtlSoapGetElementValue(void *pVal, IWriteStream *pStream, SOAPTYPES type, IAtlMemMgr *pMemMgr);
inline HRESULT AtlSoapGetElementValue(const wchar_t *wsz, int cch, 
  void *pVal, SOAPTYPES type, IAtlMemMgr *pMemMgr)
ПРИМЕЧАНИЕ

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

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

Итак, мы не можем поучаствовать в процессе генерации SOAP-сообщения и преобразовании типов данных в XML-сообщение классом CSoapRootHandler, так как все соответствующие методы находятся в закрытой (private) части класса. Но можно применить другой подход – генерировать основную часть SOAP-сообщения с помощью класса CSoapRootHandler, а затем выполнять преобразование в формат DIME, аналогично для входящего сообщения – сначала преобразовать его из формата DIME в XML, а затем поручить дальнейшую обработку классу CSoapRootHandler. Чтобы этот подход заработал, нужно исключить значения с типом ATLSOAP_BLOB из набора параметров SOAP-метода так, чтобы CSoapRootHandler не выполнил для них преобразование в base64 . Обратимся к механизму вызова SOAP-метода:

        struct ___HelloWorldService_CHelloWorldService_HelloWorld_struct
{
  BSTR bstrInput;
  BSTR bstrOutput;
};
        virtual HRESULT CallFunction(
  void *pvParam, 
  const wchar_t *wszLocalName, int cchLocalName,
  size_t nItem);

Тело этой виртуальной функции генерируется провайдером атрибутов ATL, для каждого метода, объявленного с атрибутом [soap_method] добавляется код, вызывающий этот метод (по порядковому номеру nItem). В нашем примере тело этой функции будет выглядеть так:

ATL_NOINLINE inline HRESULT CHelloWorldService::CallFunction(
  void *pvParam,const wchar_t *wszLocalName,int cchLocalName,
  size_t nItem)
{
  HRESULT hr = S_OK;
  switch(nItem)
  {
    case 0:
    {
      ___HelloWorldService_CHelloWorldService_HelloWorld_struct *p =
        (___HelloWorldService_CHelloWorldService_HelloWorld_struct *) pvParam;
      hr = HelloWorld(p->bstrInput, &p->bstrOutput);
break;
    }
    default:
      hr = E_FAIL;
  }
  return hr;
}

Когда CSoapRootHandler вызывает эту функцию, в параметре void* pvParam передается блок памяти, содержащий структуру с параметрами метода. После вызова метода в этой структуре будут заполнены выходные (out) параметры.

Теперь понятно, как можно предотвратить преобразование в base64 параметров с типом ATLSOAP_BLOB – достаточно переопределить виртуальную функцию CallFunction, в которой после вызова “настоящей” функции CallFunction сохранить значения всех бинарных параметров, а затем обнулить соответствующие элементы структуры – CSoapRootHandler не будет преобразовать их в base64. Для входных параметров наша версия CallFunction будет размещать в структуре параметров данные из DIME-записей. Таким образом, CSoapRootHandler будет заниматься своей обычной работой, а мы будем обрабатывать только параметры с типом ATLSOAP_BLOB.

Разбор входящего сообщения и генерацию исходящего выполняют (нам опять повезло) виртуальные функции:

        virtual HRESULT BeginParse(IStream *pStream);
virtual HRESULT GenerateResponse(IWriteStream* pStm);

Переопределив их в производном классе, мы сможем преобразовать входящее сообщение из DIME в XML, и наоборот.

Кроме того, в случае исходящих сообщений, помимо создания DIME-записей с данными, нужно добавить в главное SOAP-сообщение ссылки на ID этих записей.

Как уже говорилось, ссылки на DIME-записи не обязаттельно присутствуют в главном XML-сообщении. Записи, на которые есть ссылки, называются referenced, а те, на которые ссылок нет – unreferenced attachment. Во избежание проблем с совместимостью с SOAP Toolkit мы будем использовать первый тип записей. При этом в главное XML-сообщение нужно добавить атрибут href для соответствующего параметра метода, значение которого будет совпадать с id одной из DIME-записей. Так будет выглядеть, например, фрагмент главного XML-сообщения со ссылкой на DIME-запись вместо обычного значения элемента:

<snp:Send xmlns:snp="urn:DIMESenderService">
  <pData href="uuid:70F05190-9C09-46A6-BCFF-8C80854F0">
  </pData>
</snp:Send>

Чтобы DIME-записи соответствовали параметрам SOAP-методов, нужно получить доступ к описанию этих методов, которое генерируется в виде структур провайдером атрибутов ATL, когда он встречает атрибут “soap_method”. Получить доступ к этим структурам можно с помощью виртуальной функции GetFunctionMap(). В приведенном фрагменте кода мы перебираем описания всех методов и находим параметры, имеющие тип ATLSOAP_BLOB. На клиенте типом будет “Blob”, а на сервере – SOAPTYPE_BASE64BINARY.

        const _soapmap * pMap = GetFunctionMap()[m_nFunc];
size_t i = 0;
while(pMap->pEntries[i].nHash)
{
    const _soapmapentry& entry = pMap->pEntries[i];
    if((entry.nVal == SOAPTYPE_BASE64BINARY ) ||
       ((entry.pChain != 0) && (!strcmp(entry.pChain->szName, "Blob"))) )
    {

Здесь тип параметра SOAPTYPE_BASE64BINARY означает как раз, что это ATLSOAP_BLOB, а флаг принимает значения SOAPFLAG_OUT или SOAPFLAG_IN в зависимости от того, где вызывается этот метод – на сервере (SOAPFLAG_OUT) или на клиенте (SOAPFLAG_IN).

Для добавления нужных атрибутов мы работаем с XML-сообщением как со строкой – CSoapRootHandler надежно прячет методы генерации XML в private-секции, поэтому нам не остается ничего, кроме модификации строки с XML, когда все сообщение уже сгенерировано.

Для входящих сообщений нам нужно обнаруживать наличие атрибута href у параметра (в котором будет находиться ID DIME записи с данными) и сохранять значение этого атрибута в соответствующем поле структуры с параметрами. Класс CSoapRootHandler разбирает входящий XML с помощью парсера SAX. Это означает, что сам класс реализует интерфейс ISAXContentHandler, а парсер, встречая в XML-документе элементы и их атрибуты, вызывает через этот интерфейс нужные методы.

Нас будет интересовать метод ISAXContextHandler::startElelement, который вызывается всякий раз, когда в XML-документе встречается новый элемент. Переопределив эту виртуальную функцию, мы сможем находить элементы, имеющие атрибут href, и DIME-запись с нужным ID для соответствующего параметра.

ПРИМЕЧАНИЕ

CSoapRootHandler не очень любит атрибут href, и если он найдет такой атрибут, разбор XML закончится с очень уместной в данном случае ошибкой E_FAIL. Чтобы этого не произошло – нам придется “прятать” атрибут href от кода CSoapRootHandler в методе startElelent.

Соберем все эти методы вместе в классе CSoapHandlerDIMEBase. Этот класс параметризован двумя классами – тем, от которого он должен быть унаследован (например, CSoapRootHandler – мы же переопределяем виртуальные функции) и тем, который будет унаследован от него (это нужно, чтобы вызывать методы, которые могут быть реализованы только в производных классах специфично для клиента и сервера ).

        template<class THandler, class TDerived>
class CSoapHandlerDIMEBase : public THandler

{
  typedef THandler baseClass;
public:
  CSoapHandlerDIMEBase() : m_bClient(false) {}
  // выполняет пост-обработку после CSoapRootHandler, генерируя сообщение // в формате DIME, если есть бинарные данныеvirtual HRESULT GenerateResponse(IWriteStream* pStm)
  {
    HRESULT hr = baseClass::GenerateResponse(&send_handler);
    if(SUCCEEDED(hr))
    {
      TDerived* pT = static_cast<TDerived*>(this);
      if(send_handler.GetAttachCount())
        pT->SetContentType("application/dime", pStm);
      else
        pT->SetContentType("text/xml", pStm);

      AddReferences();
      hr = send_handler.SaveToStream(pStm);
    }
    return hr;
  }

  // по имени запрашиваемого SOAP-метода находит индекс метода m_nFuncMap// в структурах, генерируемых провайдером ATLvirtual HRESULT DispatchSoapCall(const wchar_t *wszNamespaceUri,
    int cchNamespaceUri, const wchar_t *wszLocalName,
    int cchLocalName)
  {
    const _soapmap ** pMap = GetFunctionMap();
    ULONG nFuncHash = AtlSoapHashStr(wszLocalName, cchLocalName);
    ULONG nNamespaceHash = AtlSoapHashStr(wszNamespaceUri, cchNamespaceUri);
    for(size_t i = 0; pMap[i] !=0; ++ i)
    {
      if(IsEqualStringHash(wszLocalName, cchLocalName, nFuncHash, 
        pMap[i]->wszName, pMap[i]->cchWName, pMap[i]->nHash) && 
        IsEqualStringHash(wszNamespaceUri, cchNamespaceUri,
          nNamespaceHash,
          pMap[i]->wszNamespace, pMap[i]->cchNamespace, 
          pMap[i]->nNamespaceHash))
        break;
    }
    m_nFunc = i;
    CreateParamsInfo();

    return baseClass::DispatchSoapCall(wszNamespaceUri, cchNamespaceUri,
      wszLocalName, cchLocalName);
  }

  // инициализирует класс RecvAttachmentHandler входящим потоком// и после преобразования DIME -> XML вызывает CSoapRootHandler::BeginParsevirtual HRESULT BeginParse(IStream *pStream)
  {
    HRESULT hr = recv_handler.Init(GetMemMgr(), 
      static_cast<TDerived*>(this)->GetContentType(), pStream);
    if(SUCCEEDED(hr))
    {
      hr = baseClass::BeginParse(recv_handler.GetXMLStream());
    }
    return hr;
  }

  // вызывается парсером SAX для каждого XML-элемента // при наличии атрибута href - мы ищем DIME-запись с нужным ID,// и если такая запись есть, запоминаем данные в списке полученных// бинарных вложенийvirtual HRESULT __stdcall startElement(const wchar_t *wszNamespaceUri,
    int cchNamespaceUri, const wchar_t *wszLocalName, int cchLocalName,
    const wchar_t * wszQName, int cchQName, ISAXAttributes *pAttributes)
  {
    if(pAttributes)
    {
      CStringW str;
      if(SUCCEEDED(GetAttribute(pAttributes, L"href", 4, str)))
      {
        LPSTR href = CW2A(str);
        if(recv_handler.AttachmentExists(href) )
        {
          id_string_t strLocalName = CW2A(wszLocalName);
          params_map_t::iterator it = m_Params.find(strLocalName);
          if(it != m_Params.end())
          {
            m_RecvdAttachments[href] = it->second;
            CComObjectStack<CSAXAttributesWrapper> wrapper;
            wrapper.init(pAttributes, L"href");  
            return baseClass::startElement(wszNamespaceUri,
              cchNamespaceUri, wszLocalName, cchLocalName, 
              wszQName, cchQName, &wrapper);
          }
        }
      }
    }
    return baseClass::startElement(wszNamespaceUri, cchNamespaceUri, 
      wszLocalName, cchLocalName, wszQName, cchQName, pAttributes);
  }

  // сравнивает две строки более эффективно за счет сравнения хэш-значений
  BOOL IsEqualStringHash(const wchar_t *wszStr1, int cchStr1, ULONG nHash1, 
    const wchar_t *wszStr2, int cchStr2, ULONG nHash2)
  {
    if(nHash1 == nHash2)
    {
      if(cchStr1 == cchStr2)
        return wcsncmp(wszStr1, wszStr2, cchStr1) == 0;
    }
    return FALSE;
  }

  // для всех исходящих параметров с типом ALTSOAP_BLOB// удаляет их значения из структуры параметров LPVOID pvParam// (по смещению, задаваемому _soapmapentry->nOffset)// и сохраняет данные в списке вложений для отправкиvoid AddAttachments(LPVOID pvParam)
  {
    send_handler.Init(GetMemMgr());
    DWORD dwFlag = m_bClient ? SOAPFLAG_IN : SOAPFLAG_OUT;
    params_map_t::iterator it = m_Params.begin();
    for( ; it != m_Params.end(); ++it)
    {
      if(it->second->dwFlags & dwFlag)
      {
        ATLSOAP_BLOB* p = 
          (ATLSOAP_BLOB*)((byte*)pvParam + it->second->nOffset);
        m_SentAttachments[send_handler.AddAttachment(*p)] = it->second;
        p->size = 0;
      }
    }
  }
  // добавляет атрибут href с ID DIME-записи для каждого// исходящего (out) параметра, имеющего тип ATLSOAP_BLOB в// главном XML-сообщенииvoid AddReferences()
  {
    params_map_t::iterator it = m_SentAttachments.begin();
    for( ; it != m_SentAttachments.end(); ++ it)
    {
      CFixedStringT<CStringA, MAX_PATH> szRef;
      szRef.Format("<%s href=\"%s\">", it->second->szField,
        (LPCSTR)it->first);

      CFixedStringT<CStringA, MAX_PATH> szField;
      szField.Format("<%s>", it->second->szField);
      send_handler.GetXMLString().Replace( szField, szRef );  
    }
  }
  // устанавливает в структуре параметров значения ATLSOAP_BLOB,// получаемые из списка полученных DIME-записейvoid SetBlobParams(LPVOID pvParam)
  {
    params_map_t::iterator it = m_RecvdAttachments.begin();
    for( ; it != m_RecvdAttachments.end(); ++it)
    {
      ATLSOAP_BLOB* p = 
        (ATLSOAP_BLOB*)((byte*)pvParam + it->second->nOffset);
      *p = recv_handler.GetAttachment(it->first);
    }
  }
  // создает карту параметров метода, имеющих тип// ATLSOAP_BLOB. Используется остальными методами для// быстрого поиска таких параметровvoid CreateParamsInfo()
  {
    const _soapmap * pMap = GetFunctionMap()[m_nFunc];
    size_t i = 0;
    while(pMap->pEntries[i].nHash)
    {
      const _soapmapentry& entry = pMap->pEntries[i];
      if((entry.nVal == SOAPTYPE_BASE64BINARY ) ||
        ((entry.pChain != 0) && (!strcmp(entry.pChain->szName, "Blob"))) )
      {
        m_Params[entry.szField] = &entry;
      }
      ++ i;
    }
  }
protected:
  SendAttachmentsHandler send_handler;
  RecvAttachmentsHandler recv_handler;
  bool m_bClient;
  params_map_t m_Params, m_RecvdAttachments, m_SentAttachments;
  size_t m_nFunc;
};

Серверный класс-обработчик запросов

Настало время разобраться во взаимоотношениях классов ATL Server, чтобы понять, как использовать класс CSoapHandlerDIMEBase. Когда класс XML Web-сервиса объявляет атрибут “soap_handler”, провайдер атрибутов ATL добавляет для этого класса наследование от CSoapHandler. Класс CSoapHandler, в свою очередь, унаследован от CSoapHandlerRoot (который и выполняет всю “грязную работу”).

CSoapHandler параметризован типом, который будет использоваться в качестве обработчика HTTP-запросов из ISAPI фильтра. Ниже приведен фрагмент кода, который добавляется провайдером атрибутов:

        class CHelloWorldService :
  public IHelloWorldService,
    /*+++ Added Baseclass */public CSoapHandler<CHelloWorldService>
{

Параметр шаблона CSoapHandler задает тип, который будет использоваться в качестве обработчика HTTP-запросов. В данном случае в качестве этого параметра передается пользовательский класс CHelloWorldService, который унаследован от CSoapHandler, а вся логика обработки HTTP-запроса и вызова нужных методов класса CSoapHandlerBase находится в классе CSoapHandler.

В приведенном выше коде класса CSoapHandlerDIMEBase не было реализации виртуальной функции CallFunction – дело в том, что мы собираемся использовать один и тот же класс как для клиента, так и для сервера, а на клиенте реализации этой функции нет. Поэтому мы создадим два класса, производных от CSoapHandlerDIMEBase. Один из них будет использоваться на сервере, а другой – на клиенте.

Серверный вариант выглядит так:

        template<class THandler> 
class CSoapHandlerDIMESrv : public CSoapHandlerDIMEBase<THandler, CSoapHandlerDIMESrv>
{
public:
  CStringA GetContentType()
  {
    return m_spServerContext->GetContentType();
  }
  void SetContentType(const CStringA& ct, IWriteStream* pStm)
  {
    CHttpResponse* pResponse = (CHttpResponse*)pStm;
    pResponse->SetContentType(ct);
  }
  virtual
  HRESULT CallFunction(void *pvParam, const wchar_t *wszLocalName, 
    int cchLocalName, size_t nItem)
  {
    SetBlobParams(pvParam);
    HRESULT hr = THandler::CallFunction(pvParam, wszLocalName, 
      cchLocalName, nItem);
    if(SUCCEEDED(hr))
    {
      AddAttachments(pvParam);
    }
    return hr;
  }
};

Этот класс реализует функции GetContentType и SetContentType, которые нужны базовому классу CSoapHandlerDIMEBase. Так как мы переопределили виртуальную функцию CallFunction, наш класс CSoapHandlerDIMESrv должен быть последним в иерархии наследования, а в качестве параметра THandler подойдет пользовательский класс Web-сервиса. Использовать наш класс нужно так:

[
  /*request_handler(name="Default" , sdl="GenDIMESenderWSDL"  ),*/
  soap_handler(
    name="DIMESenderService", 
    namespace="urn:DIMESenderService",
    protocol="soap"
  )
]
class CDIMESenderService :
  public IDIMESenderService,
    public CSoapHandler<CSoapHandlerDIMESrv<CDIMESenderService> >
{
};

typedef CSoapHandlerDIMESrv<DIMESenderService::CDIMESenderService> ReqHandler;
DECLARE_REQUEST_HANDLER("Default", ReqHandler, ReqHandler)

В этом примере CDIMESenderService – пользовательский класс XML Web-сервиса, который наследуется от обычного ATL-класса CSoapHandler, но в качестве параметра шаблона передает не себя, а наш класс – CSoapHandlerDIMESrv, поэтому обработчиком HTTP запросов будет именно он.

Иерархия классов продемонстирована на рисунке:


Рисунок 2. Иерархия классов на сервере.

Клиентский класс – Proxy

На клиенте нет обработчиков HTTP-запросов, не используется класс CSoapHandler, и нет реализации виртуальной функции CallFunction. Поэтому нам нужен еще один, клиентский, класс.

        template<class THandler, class TDerived> 
class CSoapHandlerDIMECli : public CSoapHandlerDIMEBase<THandler, TDerived>
{
public:
  typedef CSoapHandlerDIMEBase<THandler, TDerived> baseClass;
  CSoapHandlerDIMECli() : m_pvParam(0)
  {
    m_bClient = true;
  }
  HRESULT SetClientStruct(void *pvParam, int nMapIndex)
  {
    m_pvParam = pvParam;
    m_nFunc = nMapIndex;
    CreateParamsInfo();
    return THandler::SetClientStruct(pvParam, nMapIndex);
  }    
  virtualvoid Cleanup()
  {
    send_handler.CleanUp();
    recv_handler.CleanUp();
    m_Params.clear();
    m_SentAttachments.clear();
    m_RecvdAttachments.clear();
    baseClass::Cleanup();
  } 
  virtual HRESULT GenerateResponse(IWriteStream* pStm)
  {                          
    AddAttachments(m_pvParam);
    return baseClass::GenerateResponse(pStm);
  }
  virtual HRESULT BeginParse(IStream *pStream)
  {
    HRESULT hr = baseClass::BeginParse(pStream);
    if(SUCCEEDED(hr))
    {
      SetBlobParams(m_pvParam);
    }
    return hr;
  }
  LPVOID m_pvParam;
};

Клиентский Proxy, который генерируется утилитой sproxy.exe по WSDL-файлу, обычно унаследован от CSoapHandlerRoot и вызывает у него те же методы, что и серверный класс CSoapHandler. Отличия работы клиента заключаются в следующем:

Наш класс CSoapHandlerDIMECli в иерархии наследования будет находиться между классом CSoapHandlerRoot и Proxy-классом, поэтому вызовы невиртуальных функций SetClientStruct и Cleanup от Proxy-класса будут попадать к нему, а он передаст их базовому классу CSoapHandlerRoot.

Чтобы подключить нашего клиента к коду, сгенерированному sproxy.exe, нам придется (как ни жаль) модифицировать этот код, поэтому при повторной генерации эти изменения будут потеряны и их придется вносить повторно.

Рассмотрим по шагам, что нужно сделать, чтобы подключить наш клиентский класс CSoapHandlerDIMECli:

        struct Blob
{
};

Но мы-то знаем, что это вовсе не Blob, а тип ATLSOAP_BLOB – поэтому объявление пустой структуры нужно заменить на:

        typedef ATLSOAP_BLOB Blob;
        inline HRESULT AtlCleanupValue<DIMESenderService::Blob>(DIMESenderService::Blob *pVal)
{
  pVal;
  return S_OK;
}
inline HRESULT AtlCleanupValueEx<DIMESenderService::Blob>(DIMESenderService::Blob *pVal, IAtlMemMgr *pMemMgr)
{
  pVal;
  pMemMgr;
  return S_OK;
}

Так как мы сделали Blob синонимом типа ATLSOAP_BLOB – эти специализации больше не нужны, и их нужно удалить из заголовочного файла.

        template <typename TClient = CSoapSocketClientT<> >
class CDIMESenderServiceT : 
    public TClient, 
    public CSoapHandlerDIMECli<CSoapRootHandler, CDIMESenderServiceT>
{

Но это еще не все. Proxy-класс параметризован типом TClient, который задает класс, передающий и принимающий данные по протоколу HTTP. В библиотеке ATL Server имеется 3 готовых класса, которые можно использовать в роли TClient (подробнее об этих классах написано в [2]):

Ни один из этих классов не позволяет передавать ничего, кроме текста – все они устанавливают заголовок “content-type” в значение “text/xml” и используют строку CString для хранения данных. Кроме того, ни один из них не позволяет получить значение заголовка “content-type”. Поэтому нам придется разработать собственный транспортный класс, например, на основе CSoapSocketClientT. Этот класс будет реализовать методы GetContentType и SetContentType, и правильно устанавливать заголовок “content-type” при передаче. Реализацию транспортного класса CSoapSocketClient2 можно найти в примерах к статье, здесь она не приводится.

Иерархия клиентских классов проиллюстрирована на рисунке:


Рисунок 3. Иерархия классов на клиенте.

Создаем клиента с помощью SOAP Toolkit 3.0

Так как наш сервер передает данные в формате DIME, а SOAP Toolkit 3.0 поддерживает этот формат, мы можем написать на VBScript небольшого клиента, использующего SOAP Toolkit. Например, пусть сервер поддерживает два метода:

        __interface IDIMESenderService
{
  [id(1)] HRESULT Send([in] long nSize, [out]ATLSOAP_BLOB  *pData);
  [id(2)] HRESULT Recv([in]ATLSOAP_BLOB Data);
};

Метод Send передает бинарные данные от сервера клиенту, а метод Recv получает бинарные данные от клиента.

Клиент на VBScript будет выглядеть так:

        ' создаем Proxy-объект SOAPClient
        Set o = CreateObject("MSSOAP.SoapClient30")
' загружаем с сервера WSDL
o.MSSoapInit "http://localhost/DIMESender/DIMESender.dll?Handler=GenDIMESenderWSDL"' получаем от сервера бинарное вложениеset vAttach = o.Send( 1024 )
' сохраняем его в файл
vAttach.SaveToFile "c:\attachment", true' создаем новое вложение из файлаset vSend = CreateObject("MSSOAP.FileAttachment30")
vSend.FileName = "c:\attachment"' отправляем данные серверу
o.Recv vSend

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

С помощью утилиты SoapTrace, которая входит в состав SOAP Toolkit, можно увидеть передаваемое сервером сообщение:

<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="544" /> 
    <ID value="uuid:714C6C40-4531-442E-A498-3AC614200295" /> 
    <Type value="http://schemas.xmlsoap.org/soap/envelope/" /> 
  </DimeRecord>
  <DimeRecord traceOffset="0x00000284">
    <Recordinfo Version="1" MB="0" ME="1" CF="0" IDLength="41" /> 
    <Typefield TNF="3" TypeLength="0" /> 
    <Options O="0" OptionLength="0" /> 
    <Datalength length="1024" /> 
    <ID value="uuid:931B2A26-C5AA-48A7-A3FC-4621EE19651B" /> 
  </DimeRecord>
</DimePayload>

Тестируем производительность

У нас есть клиент и сервер ATL 7.0, поддерживающие формат DIME. Мы можем создавать клиента с помощью SOAP Toolkit 3.0. Значит, теперь мы можем сравнить скорость передачи и получения бинарных данных сервером в различных конфигурациях, чтобы оценить выигрыш от использования формата DIME.

На сервере мы создадим компонент с двумя методами:

[ soap_method ]
HRESULT Send(/*[in]*/long nSize, /*[out]*/ ATLSOAP_BLOB *pData)
{
  staticint fill = 0;
  pData->size = nSize;
  if(nSize == 524288)
  {
    ++fill;
  }
  pData->data = (byte*)GetMemMgr()->Allocate(pData->size);
  memset(pData->data, ++fill, pData->size);
  return S_OK;
}

[ soap_method ]
HRESULT Recv(/*[in]*/ ATLSOAP_BLOB  Data)
{
  byte* b = Data.data;   
  return S_OK;
}

Send передает блок бинарных данных с размером, задаваемым первым параметром, а Recv получает блок данных от клиента.

Клиент вызывает в цикле эти методы, увеличивая размер блока от 512 байт до 512 КБ (каждый раз в два раза) и замеряет время, требующееся на передачу данных:

        using
        namespace DIMESenderService;
usingnamespace DIMESupport;
int _tmain(int argc, _TCHAR* argv[])
{
  usingnamespace std;
  ::CoInitialize(0);
  {
    CDIMESenderServiceT<CSoapSocketClient2 > svc;
    constint N = 200;
    constint n = 12;
    constint nInitial = 256;
    int nCurrent = nInitial;
    for(int i = 0; i < n; ++ i)
    {
      cout <<nCurrent << " " ;
      DWORD dw1 = GetTickCount();
      ATLSOAP_BLOB data = {0};
      for(int j = 0; j < N; ++ j)
      {
        HRESULT hr = svc.Send(nCurrent, &data);
        AtlCleanupValueEx(&data, svc.GetMemMgr());
      }
      DWORD dw2 = GetTickCount();
      cout << (dw2 - dw1) * 1.0/ N << endl;
      nCurrent *= 2;
    }
    nCurrent = nInitial;
    for(int i = 0; i < n; ++ i)
    {
      cout << nCurrent << " ";
      ATLSOAP_BLOB data = {0};
      data.data = (byte*)svc.GetMemMgr()->Allocate(nCurrent);
      data.size = nCurrent;
      DWORD dw1 = GetTickCount();
      for(int j = 0; j < N; ++ j)
      {
        HRESULT hr = svc.Recv(data);
        if(FAILED(hr))
          DebugBreak();
      }
      DWORD dw2 = GetTickCount();
      svc.GetMemMgr()->Free(data.data);
      cout << (dw2 - dw1) * 1.0/ N << endl;
      nCurrent *= 2;
    }
  }
  ::CoUninitialize();
  return 0;
}

Измерять время работы мы будем для следующих конфигураций:

  1. Сервер и клиент ATL 7.0 без поддержки DIME (т.е. созданные с помощью мастера ATL).
  2. Клиент/Сервер SOAP Toolkit 3.0 с поддержкой DIME.
  3. Сервер и клиент ATL 7.0 с нашей поддержкой DIME.

Серверный компонент для SOAP Toolkit, так же, как и серверный компонент ATL Server, передает и получает от клиента блок данных заданного размера.

STDMETHODIMP CSender::Send(long nSize, IStreamAttachment **pData)
{

   staticint fill = 0;
   CComPtr<IStream> spStm;
   byte* p = (byte*)GlobalAlloc(0, nSize);
   memset(p, ++fill, nSize);
   CreateStreamOnHGlobal(p, TRUE, &spStm);
   CComPtr<IStreamAttachment> spStmAttach;
   spStmAttach.CoCreateInstance(CLSID_StreamAttachment30);
   spStmAttach->putref_Stream(spStm);
   *pData = spStmAttach.Detach();
   return S_OK;
}

STDMETHODIMP CSender::Receive(IReceivedAttachment *pData)
{

	return S_OK;
}

Клиент на VB 6.0 выполняет те же действия, что и рассмотренный выше клиент C++, и позволяет измерить скорость передачи и приема данных от сервера:

        Option Explicit
PrivateDeclareFunction GetTickCount Lib"kernel32" () AsLongPrivateSub Form_Load()
Dim o AsObjectDim v
Set o = CreateObject("MSSOAP.SoapClient30")
o.ClientProperty("ConnectorProgID") = "SockConnector.Connector"
o.MSSoapInit "http://localhost/DSTKServer/DSTKServer.WSDL"Dim a As MSSOAPLib30.StringAttachment30
Set a = New MSSOAPLib30.StringAttachment30
Dim n AsLong
n = 256
Dim s AsStringDim i AsLongFor i = 1 To 14
    Set a = New MSSOAPLib30.StringAttachment30
    a.String = String(n / 2, "A")
    Dim d1 AsLong
    d1 = GetTickCount
    Dim j AsLongFor j = 1 To 100
'        Set v = o.send(n)
        o.receive a
    Next j
    Dim d2 AsLong
    d2 = GetTickCount
    Dim f AsDouble
    f = (d2 - d1) / 100
    s = s + CStr(n) + " " + CStr(f) + vbCrLf
    n = n * 2
Next i
MsgBox s
EndSub
ПРЕДУПРЕЖДЕНИЕ

ISAPI-расширение SOAP Toolkit 3.0 по умолчанию не разрешает клиенту передавать данные размером больше 100 кБ. Это сделано для того, чтобы злоумышленник не смог повлиять на работу сервера, передавая ему очень большие блоки данных. В нашем тесте это ограничение будет препятствовать успешной передаче серверу больших блоков, поэтому нам придется воспользоваться утилитой regedit и изменить значение MaxPostSize в ветке HKLM\Software\Microsoft\MSSOAP\30\SOAPISAP – например, на 0x1900000.

Чтобы исключить влияние различных способов передачи данных по HTTP (WinInet, WinHTTP и т.п.), мы будем использовать коннектор, который использует тот же механизм передачи (сокеты), что и ATL Server.

ПРИМЕЧАНИЕ

В приведенном выше коде коннектор устанавливается инструкцией o.ClientProperty("ConnectorProgID") = "SockConnector.Connector". Рассмотрение реализации этого коннектора выходит за рамки статьи, примеры реализации коннекторов для STK 3.0 можно найти в [1].

Клиент и сервер для первой конфигурации (обычное приложение ATL Server) идентичны клиенту и серверу для третьей конфигурации, за исключением того, что не используется разработанная нами поддержка DIME. Однако в коде ATL 7.0 есть некоторые проблемы, которые могут помешать нам получить достоверную информацию о скорости приема/передачи данных без DIME. Дело в том, что для разбора XML парсером SAX ATL использует строковый класс CStringW (который появился в ATL 7.0). Этот класс динамически увеличивает размер буфера для строки, когда строка увеличивается, однако делает это весьма специфическим способом. Ниже приведен фрагмент из кода ATL класс CSimleStringT (atlsimplestr.h):

        else
        if( pOldData->nAllocLength < nLength )
{
	// Grow exponentially, until we hit 1K.int nNewLength = pOldData->nAllocLength;

        if( nNewLength > 1024 )
	{
	nNewLength += 1024;
	}
	else
	{
		nNewLength *= 2;
	}
	if( nNewLength < nLength )
	{
		nNewLength = nLength;
	}
	Reallocate( nNewLength );
}

Традиционный способ увеличения размера буфера заключается в том, что новый размер рассчитывается как nNewSize = K*nPrevSize, где коэффициент K обычно равен 2 или 1,5. Однако в ATL 7.0 для строк, размер которых превышает 1 КБ, размер начинает увеличиваться не экспоненциально, а линейно. Очевидно, что если мы передадим строку размером 2МБ, в результате будет выполнено около 2000 перераспределений памяти. С аналогичной проблемой приходится столкнуться при использовании класса CAtlIsapiBuffer (altutil.h), применяемого клиентом для чтения отклика сервера:

BOOL Append(LPCSTR sz, int nLen = -1) throw()
{
	if (nLen == -1)
		nLen = (int) strlen(sz);

	if (m_dwLen + nLen + 1 > m_dwAlloc)
	{
		if (!ReAlloc(m_dwAlloc + (nLen+1 > ATL_ISAPI_BUFFER_SIZE ? nLen+1 : ATL_ISAPI_BUFFER_SIZE)))
			return FALSE;
	}

	memcpy(m_pBuffer + m_dwLen, sz, nLen);
	m_dwLen += nLen;
	m_pBuffer[m_dwLen]=0;
	return TRUE;
}

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

Только для целей тестирования скорости мы временно исправим эти проблемные участки кода ATL 7.0 так, чтобы использовался экспоненциальный алгоритм увеличения размера буфера.

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

Например, если не внести изменения в код ATL 7.0, на размере блока в 512 кБ разница во времени работы составляет порядка 1,5 секунды! Многократные распределения больших блоков памяти не такая уж дешевая операция!

Итак, Конфигурация 1 будет использовать модифицированный код ATL, чтобы исключить многократные перераспредеения памяти (на небольших блоках разницы с оригинальным кодом ATL не будет, но на больших размерах разница будет очень ощутимой).

1. Ниже приводятся результаты в ms для следующей аппаратной конфигурации: AMD Athlon 800, 384 RAM:

Размер блока Конфигурация 1 Конфигурация 2 Конфигурация 3
256 2,185 6,57 2,345
512 2,03 6,4 2,265
1024 2,425 6,41 2,42
2048 2,735 6,41 2,265
4096 3,355 6,71 2,425
8192 4,845 7,04 2,73
16384 7,58 7,5 3,205
32768 12,81 8,43 4,14
65536 23,36 10,47 6,25
131072 46,485 14,06 8,905
262144 91,405 21,1 14,845
524288 177,89 204,22 29,925
1048576 351,8 325,15 59,765
2097152 958,985 560 124,375


Рисунок 4. Получение данных от сервера

Размер блока Конфигурация 1 Конфигурация 2 Конфигурация 3
256 1,95 6,25 2,11
512 2,11 5,94 2,03
1024 2,345 6,25 2,185
2048 2,735 6,41 2,11
4096 3,595 6,56 2,345
8192 5,155 6,72 2,5
16384 8,36 7,19 2,97
32768 13,59 8,12 3,905
65536 24,77 10,94 5,39
131072 51,715 13,6 8,205
262144 104,14 20,63 14,53
524288 211,095 190,78 26,955
1048576 411,565 291,41 50,935
2097152 832,03 527,97 98,905


Рисунок 5. Отправка данных серверу

SOAP Toolkit 3.0 на небольших размерах блока оказывается медленнее всех (видимо, из-за использования медленного oleautomation, необходимости разбора wsdl- и wsml-файлов). Начиная с 16 кБ он явно опережает обычный ATL Server, но при размере в 512 КБ время резко возрастает с 20 до 200 ms. Причина в том, что STK 3.0 не пытается загрузить большие блоки данных в память целиком, а использует файлы во временной папке %TEMP%. Дисковые операции в данном случае очень негативно отразились на скорости работы. Но все же STK 3.0 оказывается быстрее, чем обычный ATL Server, и с ростом размера блока данных этот разрыв увеличивается.

Конфигурация 1 (обычное приложение ATL Server) при размере блока 2 МБ уже требует около 1 секунды (!) на передачу и прием данных.

Наконец, наша реализация DIME для ATL выигрывает у конкурентов на всех размерах, а время растет примерно линейно. Существенное отличие от STK 3.0 заключается в том, что все данные передаются через память, поэтому для передачи очень больших блоков (десятки и сотни мегабайт) наша реализация непригодна.

ПРИМЕЧАНИЕ

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

Тест производительности использует максимальный размер блока 2 МБ, хотя может показаться, что еще больший выигрыш будет получен при передаче очень больших блоков – 10-100 МБ. Однако даже на 1 МБ разница уже весьма ощутима, а передача 100 МБ данных от сервера клиенту одним запросом не обязательно свидетельствует о хорошей архитектуре системы, ведь те же самые данные могут быть переданы 100 запросами по 1 мб, а клиент сможет контролировать процесс передачи (например, прервать передачу, если пользователь отменил операцию).

Заключение

Итак, мы разобрались, как устроен формат DIME и как формируются сообщения в этом формате, а также реализовали поддержку DIME для ATL 7.0. Разумеется, эта реализация не претендует на промышленное применение. Но благодаря ей мы больше узнали о формате DIME и о внутреннем устройстве кода ATL Server, научились расширять генерируемый WSDL дополнительными элементами и изменять логику работы стандартных классов ATL Server для преобразования пользовательских типов данных.

Вероятно, в скором будущем библиотека ATL будет включать в себя классы для работы с DIME (хотя в ATL 7.1 этой поддержки еще нет). В этом случае можно будет без труда создавать высокоэффективные приложения, передающие бинарные данные от сервера клиенту и наоборот. А пока этой поддержки нет – можно использовать SOAP Toolkit или WSE (Web Services Extensions) для .NET XML Web сервисов.

Производительность сервера и скорость передачи бинарных данных между клиентом и сервером зависит от многих факторов. Но совершенно точно можно сказать, что применение формата DIME позволит уменьшить сетевой трафик (за счет ухода от преобразования в base64) и сэкономить серверные ресурсы.

Ссылки

[1] "Использование протокола SOAP в распределенных приложениях. SOAP Toolkit 3.0”

[2] “Использование протокола SOAP в распределенных приложениях. ATL 7.0”

[3] DIME: Sending Binary Data with Your SOAP Messages

[4] Understanding DIME and WS-Attachments

[5] DIME Specification Index Page

[6] DIME: Sending Files, Attachments, and SOAP Messages Via Direct Internet Message Encapsulation


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