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

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

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

Источник: RSDN Magazine #2-2003
Опубликовано: 26.07.2003
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Изменения в библиотеке ATL
Состав библиотеки ATL Server
Архитектура приложений ATL Server
Internet Information Services
ISAPI-расширения
Модуль Web-приложения (Web Application Dll)
IRequestHandler
Архитектура XML Web-сервисов
"Здравствуй, мир" (ATL Server edition)
Клиент Web-сервиса “Здравствуй, мир”
Внутреннее устройство обработчика SOAP-запросов
CSoapHandler<>
Автоматическая генерация WSDL
Описание интерфейсов, методов и заголовков SOAP запросов
Отладка приложений ATL Server
CDebugReportHook и WEBDbg
Утилита трассировки SOAP-сообщений
SOAPDebugApp
Активация обработчиков запросов
Обработка ошибок
Внутреннее устройство клиентского кода ATL 7.0
Отмена SOAP вызовов
Типы данных и их преобразования
Передача структур
Передача массивов
Передача бинарных данных
Передача COM-объектов по значению
Заключение
Ссылки

Исходные тексты примеров

Введение

В предыдущей статье мы научились создавать распределенные SOAP-приложения с помощью SOAP Toolkit 3.0, увидели сильные и слабые стороны таких приложений и особенности написания серверных компонентов и клиентских приложений, работающих совместно с компонентами SOAP Toolkit 3.0.

В этой статье мы сосредоточимся на создании приложений с помощью новой библиотеки ATL 7.0, входящей в состав Visual Studio 7.0 и включающей поддержку протокола SOAP как для серверной, так и для клиентской части распределенных систем. Мы попытаемся также сравнить возможности, предоставляемые SOAP Toolkit 3.0 и ATL 7.0 при разработке SOAP-приложений, и ответить на вопрос, в каких случаях использование ATL 7.0 может дать преимущества.

В новой, седьмой версии библиотеки ATL произошли существенные изменения. Теперь она состоит из двух частей:

В состав ATL Server входит огромное количество классов, решающих типичные для серверных приложений задачи – кэширование, шифрование, поддержка MIME, SMTP, генерация HTML, реализация пула потоков, применение различных кодировок, работа с регулярными выражениями. Помимо всего прочего, в ATL 7.0 добавлена поддержка протокола SOAP – набор серверных классов, преобразующих SOAP-запрос в вызов методов, и клиентские классы, позволяющие формировать запросы к серверу и получать ответ.

Любое Web-приложение (в том числе XML Web-сервисы, использующие протокол SOAP), создаваемое на основе ATL 7.0, активно использует ATL Server, поэтому в следующих разделах мы рассмотрим состав ATL Server и архитектуру типичного приложения, созданного с использованием ATL Server.

Изменения в библиотеке ATL

Основная часть изменений в части библиотеки ATL, предназначенной для разработки COM объектов, связана с исправлением старых ошибок и улучшениями в старых классах. В этом разделе мы рассмотрим некоторые изменения, подробнее можно посмотреть, например, здесь: http://www.codeproject.com/atl/newinatl7.asp

В ATL 7.0 наконец-то появились макросы преобразования строк из ANSI в UNICODE и обратно, которые лишены проблем, характерных для “старых” макросов A2W, W2A и др. Неприятные эффекты, связанные с использованием “старых” макросов, заключались в том, что память для строк выделялась в стеке и освобождалась, когда уничтожался текущий кадр стека, т.е. при выходе из функции. Поэтому с их помощью нельзя было преобразовывать большие строки (так как объем стека ограничен), их нельзя было использовать в циклах (так как память освобождалась только при выходе из функции). Кроме того, они были небезопасны с точки зрения обработки C++ исключений (их нельзя было использовать внутри catch). В ATL 7.0 появились 3 новых класса – CA2AEx, CA2CAEx, CA2WEx и набор макросов на их основе – CX2Y, где X и Y могут принимать значения A (ANSI) , W (UNICODE) и T (ANSI или UNICODE в зависимости от символа препроцессора UNICODE). Особенность новых макросов заключается в том, что для небольших строк они выделяют память на стеке, а для больших – в хипе (размер задается параметром шаблона). Память автоматически освобождается при выходе из области видимости – поэтому их можно безопасно использовать в циклах.

ПРИМЕЧАНИЕ

Попытки использовать семейство макросов A2W, W2A и др. в блоке catch заканчиваются обычно ошибкой доступа к памяти, так как обработчик исключения работает в собственном кадре стека. Подробнее об этой проблеме написано в статье KB Q198009. Эта проблема в ATL 3.0 имеет неожиданное продолжение для ATL-проектов, которые используют inline-функции – если такая функция использует макросы преобразования и вызывается в блоке catch, а компилятор выполнил подстановку тела функии – произойдет ошибка доступа к памяти. Зато новый компилятор MSVC 7.0 теперь обнаруживает попытки вызова alloca (эту функцию используют макросы преобразования) в блоке catch и выдает ошибку компиляции.

В ATL 7.0 появились классы для управления памятью – CCRTHeap, CWIN32Heap, CCOMHeap, каждый из этих классов реализует интерфейс IAtlMemMgr, который широко используется в коде ATL для выделений/освобождений памяти, в том числе и для классов-оберток строк CString.

Добавилось несколько новых "умных" указателей – CHeapPtr, CAutoPtr, CAutoVectorPtr. Они работают подобно "умному" указателю auto_ptr из STL, который передает владение указателем при копировании. CHeapPtr использует переданный в качестве параметра шаблона аллокатор (распределитель памяти) IAtlMemMgr, CAutoPtr использует new и delete, а CAutoVectorPtr – delete[]. Еще один новый "умный" указатель – CComGITPtr – позволяет работать с указателями на интерфейсы в GIT.

Класс для работы со строками CString теперь стал шаблонным (и стал очень сильно напоминать basic_string), параметры шаблона позволяют указывать тип элемента строки, распределитель памяти и т.п. Улучшения коснулись и класса CComBSTR, который теперь определяет все необходимые операторы (например, operator+ для двух CComBSTR), и стал более удобным в использовании.

Еще одна новинка ATL 7.0 – несколько шаблонных классов-коллекций: CAtlArray, CAtlList, CAtlMap, CRBMap (красно-черное дерево), CRBMultiMap, а также их специализации для типичных ситуаций – CAutoPtrArray, CAutoPtrList, CComUnkArray, CHeapPtrList, CInterfaceArray, CInterfaceList.

ПРИМЕЧАНИЕ

Вообще все эти классы сильно напоминают STL-аналоги, для классов-коллекций не хватает только алгоритмов и итераторов. В ATL 7.0 даже есть класс CPair.

Состав библиотеки ATL Server

В состав ATL Server входит довольно много классов. Эти классы можно разделить на несколько категорий.

Категория Описание
ATL Server Framework Главные строительные блоки ATL Server – инфраструктура для создания ISAPI-расширений, Web-сервисов, классы для обработки HTTP-запросов и создания HTTP-откликов. Большинство классов находятся в atlisapi.h и atlstencil.h.
Обработка запросов Атрибуты, классы и макросы для создания обработчиков HTTP-запросов, которые встраиваются в инфраструктуру ATL Server.
Статистика обработки запросов Набор классов для сбора статистики обработки запросов. Например, реализация статистики в виде счетчиков для Performance Monitor.
Расширения для внешнего управления Web-приложением Классы, позволяющие управлять Web-приложением (пулом потоков, кэшем DLL и т.п.) удаленно по протоколу SOAP. Код находится в atlextmgmt.h
Shared Services Набор классов для создания сервисов, доступных для Web-приложений – например, кэша бинарных данных для хранения состояния компонентов Web-приложений между запросами. Код находится в atlsharedsvc.h
Session-State Набор классов для сохранения данных сессии в памяти, в базе данных и т.п. Код находится в atlsession.h
SOAP Набор серверных и клиентских классов для поддержки протокола SOAP. Код находится в atlsoap.h
Работа с кэшем Классы и интерфейсы для очистки кэша на основе времени, доступа к элементам, а также классы для поддержки статистики.
Шифрование Набор классов для работы с криптографическими сервисами, ключами, вычисления хеша на основе распространенных алгоритмов SHA, MD5 и др.
Поддержка кодировок Классы для преобразования данных в распространенные кодировки – base64, uuencode, utf8 и др. Код в atlenc.h
Генерация HTML Классы для генерации HTML, находятся в atlhtml.h
HTTP клиент Набор классов для поддержки HTTP в клиентских приложениях, позволяет формировать запросы и получать ответы сервера без использования дополнительных средств (снижая зависимость клиента от внешних компонентов). Код находится в atlhttp.h
MIME и SMTP Набор классов для работы с протоколом SMTP. Код в atlmime.h и atlsmtpconnection.
Поддержка Perfomance Monitor Набор классов и макросов, облегчающих создание и работу со счетчиками для Performance Monitor’а. Код находится в atlperf.h
Регулярные выражения Поддержка регулярных выражений. Код находится в в atlrx.h
Пул потоков Классы для создания пулов потоков и работы с ними.
IStream обертки Работа со строками, сокетами, Интернет-соединениями через IStream .

Теперь библиотека ATL поддерживает большую часть протоколов, используемых в Internet (HTTP, SMTP, SOAP), позволяет шифровать и подписывать передаваемые данные, работать с регулярными выражениями и организовывать кэш с различными критериями удаления элементов – в общем, все, что нужно типичному серверному приложению для обработки запросов клиентов.

Главная особенность классов ATL Server – они, как правило, являются тонкой оберткой соответствующего API, поэтому при их использовании снижаются требования к ресурсам, повышается быстродействие и уменьшается зависимость от внешних модулей и компонентов.

Архитектура приложений ATL Server

Архитектура любого приложения ATL Server включает в себя четыре основных элемента:

Internet Information Services

IIS используется как Web-сервер, принимающий HTTP-запросы клиента и передающий их на обработку приложениям ATL Server посредством ISAPI-интерфейса.

Во время разработки приложений ATL Server для управления настройками виртуального каталога IIS, в котором будут размещаться генерируемые файлы, и автоматизации копирования нужных файлов в этот виртуальный каталог можно использовать закладку “Web Deployment” в свойствах проекта приложения ATL Server.

На этой закладке можно настроить имя виртуального каталога IIS, в который будут копироваться файлы, указать дополнительные файлы для автоматического копирования, задать уровень защиты виртуального каталога и установить режим копирования – с остановкой Web сервера (это нужно для ISAPI-расширений, так как во время работы Web сервера модуль ISAPI нельзя перезаписать) или без.


Рисунок 1. Настройка проекта “Web Deployment”

ISAPI-расширения

Основная роль ISAPI-расширений – получать запросы от IIS и передавать их нужному обработчику, в модуль Web Application Dll. Запрос, получаемый ISAPI-расширением, обычно содержит имя модуля, которому предназначается запрос, и имя обработчика запроса.

Например, обращение клиента по следующему URL http://IVAN/WS/ws.dll?Handler=GenwsWSDL означает вызов модуля ws.dll и обработчика “GenwsWSDL” из этого модуля.

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

Интерфейс Описание
IHttpServerContext Предоставляет доступ к информации о Web сервере и об обрабатываемом запросе.
IIsapiExtension Позволяет добавлять/удалять общие сервисы ISAPI-расширения, получать доступ к пулу потоков
IServiceProvider Позволяет запросить один из общих сервисов, поддерживаемых ISAPI-расширением.

Взаимодействие с модулями Web-приложения осуществляется с помощью интерфейса IRequestHandler, основным методом которого является “HandleRequest”.

В сгенерированном мастером “ATL Server” проекте для ISAPI-расширения основная функциональность сосредоточена в унаследованном от CIsapiExtension классе, который включает в себя следующее:

ПРИМЕЧАНИЕ

Еще одна причина для внесения изменений в код ISAPI-расширений – управление инициализацией COM, так как по умолчанию все потоки из пула входят в STA, и поэтому обработчик запроса в модуле Web-приложения также будет выполняться в STA. Чтобы изменить тип апартамента, в главном классе, унаследованном от CISapiExtension, нужно переопределить методы OnThreadAttach и OnThreadTerminate, которые вызываются для каждого потока из пула (и по умолчанию вызывают CoInitialize()).

Модуль Web-приложения (Web Application Dll)

Этот модуль непосредственно реализует логику Web-приложения и является обработчиком запросов, полученных от ISAPI-расширения. Точкой входа в модуль является функция, возвращающая соответствующий переданному имени обработчик запроса (указатель на интерфейс IRequestHandler), а также две дополнительные функции для инициализации и очистки.

        typedef BOOL (__stdcall *GETATLHANDLERBYNAME)(
  LPCSTR szHandlerName,
  IIsapiExtension* pExtension,
  IUnknown** ppHandler
);

Сами обработчики запросов являются обычными COM-объектами, реализующими интерфейс IRequestHandler. Именно указатель на этот интерфейс возвращает функция GetAtlHandlerByName.

ПРИМЕЧАНИЕ

Новый экземпляр обработчика запроса создается при каждом вызове GetAtlHandlerByName. Для этого служит статическая функция IRequestHandlerImpl::CreateRequestHandler. Если запрос требует асинхронной обработки, функция создает экземплляр обработчика запросов с помощью new, если же запрос не требует асинхронной обработки, то объект создается в хипе текущего потока (для каждого рабочего потока из пула создается свой хип).

Реализовывать эту функцию нет необходимости – код для нее генерируется автоматически на основе атрибутов в классах-обработчиках запросов (или с помощью макроса HANDLER_ENTRY – если атрибуты не используются). ATL предоставляет и готовые реализации функций инициализации и очистки – они будут вызывать статические функции для каждого класса–обработчика запросов:

        static BOOL InitRequestHandlerClass(
  IHttpServerContext * pContext,
  IIsapiExtension * pExt
);
staticvoid UninitRequestHandlerClass( );

IRequestHandler

Обработчики запросов в модуле должны реализовывать интерфейс IRequestHandler. Стандартная реализация этого интерфейса находится в классе IRequestHandlerImpl<>, а пользовательские классы, как правило, наследуются от CRequestHandlerT или CSoapHandler (для XML Web-сервисов).

Для доступа к ISAPI-расширению класс IRequestHandlerImpl объявляет несколько переменных-членов, хранящих указатели на интерфейсы ISAPI-расширения (таблица 3).

Переменная Описание
IRequestHandlerImpl::m_spExtension IIsapiExtension
IRequestHandlerImpl::m_spServiceProvider IServiceProvider
IRequestHandlerImpl::m_spServerContext IHttpServerContext

Наиболее важные методы интерфейса IRequestHandler:

Архитектура XML Web-сервисов

XML Web-сервисы, создаваемые на основе ATL Server, используют базовую архитектуру ATL Server, расширяя ее специфичными для обработки SOAP-запросов элементами.


Рисунок 2. Архитектура XML Web сервиса.

Запросы клиентов попадают в ISAPI-расширение (диспетчер), которое выделяет из запроса имя нужного модуля и передает ему управление. В модуле Web-приложения из тела SOAP-запроса выделяются параметры, которые преобразуются из текстового (сериализованного) в бинарное представление. Далее управление получает непосредственно код Web-приложения. После обработки запроса возвращаемые значения преобразуются в текстовое представление и формируется XML-отклик сервера. Этот отклик передается клиенту через ISAPI-расширение и Web-сервер.

"Здравствуй, мир" (ATL Server edition)

Теперь, когда мы узнали, как устроены приложения ATL Server, можно перейти к созданию простейшего серверного SOAP-приложения.

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

Создадим новое приложение с помощью мастера “ATL Server”. На закладке “Application Options” укажем опцию “Create as Web Service”.

Мастер создаст два проекта – ISAPI-расширение и модуль Web-приложения (кроме того, мастер создаст виртуальный каталог IIS, в котором будут размещаться модули приложения, и установит свойства проекта так, чтобы модули копировались в этот виртуальный каталог при каждой сборке).

ПРИМЕЧАНИЕ

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

В состав проекта Web-приложения мастер включает следующие файлы:

ПРИМЕЧАНИЕ

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

В проекте нигде нет упоминания о WSDL-файле, необходимом клиентам для формирования правильных запросов к серверу. Это не ошибка мастера – WSDL-файл генерируется автоматически, когда сервер получает запрос “http://IVAN/HelloWorld/HelloWorld.dll?Handler=GenHelloWorldWSDL”. Чтобы убедиться в этом, достаточно набрать в строке адреса браузера этот URL.

СОВЕТ

Подробнее о WSDL-файлах и о том, что в них должно находиться, можно прочитать в предыдущей статье “Использование протокола SOAP в распределенных приложениях. Microsoft SOAP Toolkit 3.0”.

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

[
  uuid("45D0BAAF-BF1B-4662-909B-983ED93D2952"), 
  object
]
__interface IHelloWorldService
{
  [id(1)] HRESULT HelloWorld([in] BSTR bstrInput, 
    [out, retval] BSTR *bstrOutput);
};
[
  request_handler(name="Default", sdl="GenHelloWorldWSDL"),
  soap_handler(
    name="HelloWorldService", 
    namespace="urn:HelloWorldService",
    protocol="soap"
  )
]
class CHelloWorldService :
  public IHelloWorldService
{
  [ soap_method ]
  HRESULT HelloWorld(/*[in]*/ BSTR bstrInput, 
            /*[out, retval]*/ BSTR *bstrOutput)
  {
    ...
  }
};

Методы, которые будут доступны по протоколу SOAP, должны быть объявлены в интерфейсе – поэтому мастер создал интерфейс IHelloWorldService с единственным методом HelloWorld. Сам обработчик запросов CHelloWorldService использует ATL-атрибуты, которые скрывают механику его работы.

СОВЕТ

Чтобы “увидеть”, какой код добавляет компилятор при обработке атрибутов, нужно включить опцию “C/C++\Output Files\Expand Attribute Source” в свойствах проекта (или добавить ключ компилятора /Fx) – для каждого обработанного файла с атрибутами компилятор создаст файл с расширением .mrg.x, где .x – это расширение исходного файла.

Атрибуты request_handler и soap_handler в данном случае указывают, что:

Клиент Web-сервиса “Здравствуй, мир”

Чтобы увидеть наше серверное приложение в действии, нужен клиент. Проще всего реализовать его с помощью кода на VBScript, использующего клиентскую часть SOAP Toolkit:

        Set o = CreateObject("MSSOAP.SoapClient30")
o.MSSoapInit "http://ivan/HelloWorld/HelloWorld.dll?Handler=GenHelloWorldWSDL"
s = o.helloworld("from ATL 7.0")
MsgBox s

ATL 7.0 включает поддержку протокола SOAP не только на серверной стороне. Поэтому альтернативную реализацию клиента мы напишем на C++ с помощью все того же ATL и утилиты Sproxy.exe, которая генерирует классы-обертки C++ по описанию Web-сервиса в WSDL. Для этого мы создадим консольное Win32-приложение с поддержкой ATL и добавим ссылку на disco-файл нашего Web-сервиса (с помощью меню Project/Add Web Reference).

СОВЕТ

Того же эффекта можно добиться, сгенерировав классы-обертки вручную. Для этого нужно запустить Sproxy.exe из командной строки и передать ему путь к WSDL-файлу. Встроенная в Visual Studio функция “Add Web Reference” просто автоматизирует этот процесс.

После добавления ссылки на Web-сервис в проекте появятся еще два файла:

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

ПРИМЕЧАНИЕ

В случае большого WSDL-файла статически сгенерированные proxy-классы могут дать большой выигрыш в скорости начальной инициализации и в требованиях к памяти по сравнению с SOAP Toolkit. Дело в том, что клиентские приложения, созданные с помощью SOAP Toolkit, во время инициализации загружают WSDL-документ в память и разбирают его с помощью MSXML. Еще одно преимущество proxy-классов заключается в уходе от automation-типов и вызовов IDispatch::Invoke, что снижает время вызова и упрощает развертывание C++-клиента.

Теперь, когда у нас есть класс-обертка, осталось написать код, вызывающий метод “HelloWorld”:

        int _tmain(int argc, _TCHAR* argv[])
{
  usingnamespace HelloWorldService;
  ::CoInitialize(0);
  {
    CHelloWorldService svc;
    CComBSTR bstrResult;
    HRESULT hr = svc.HelloWorld(CComBSTR(L"ATL 7.0 Client"), &bstrResult);
    ATLASSERT(SUCCEEDED(hr));
  }
  ::CoUninitialize();
  return 0;
}
ПРЕДУПРЕЖДЕНИЕ

Сгенерированные с помощью Sproxy заголовочные файлы требуют объявления символа препроцессора _WIN32_WINNT >= 0x0400 или _WIN32_WINDOWS > 0x0400

Забавно, что в приведенном коде клиента нигде нет упоминания URL сервера, по которому происходит обращение – этот адрес был взят из WSDL-файла. Он передается в виде константы в конструкторе класса-обертки, поэтому искать его надо в сгенерированном файле HelloWorld.h. Изменить URL для подключения можно с помощью вызова метода SetUrl:

CHelloWorldService svc;
svc.SetUrl(L”http://ivan/HelloWorld/HelloWorld.dll?Handler=Default”);

Внутреннее устройство обработчика SOAP-запросов

Распределенные приложения, создаваемые с помощью SOAP Toolkit, используют наборы серверных COM(+) компонентов, которые получают внешние вызовы от компонента SoapServer, входящего в состав SOAP Toolkit. SoapServer, в свою очередь, получает запрос от кода в ASP-странице (которая генерируется мастером) или от ISAPI-расширения, также входящего в состав SOAP Toolkit. Для вызова серверных компонентов используется интерфейс IDispatch. SoapServer анализирует информацию в WSDL- и WSML-файлах и преобразует SOAP-запрос в вызов IDispatch-интерфейса у нужного серверного компонента. Для разработчика приложений на основе SOAP Toolkit SoapServer представляет собой “черный ящик”. Доступные методы изменения логики его работы – модификация WSDL- и WSML-файлов и использование mapper-ов для преобразования типов данных.

Приложениями, создаваемыми на основе ATL Server, легче управлять. Разработчик в данном случае имеет большие возможности по изменению логики работы сервера, так как у него есть доступ и к коду ISAPI-расширения, и к коду, отвечающему за диспетчеризацию входящих вызовов. Целью данного раздела является знакомство с реализацией обработчика/диспетчера SOAP запросов, входящего в состав ATL Server, и принципов его работы.

CSoapHandler<>

Основной класс, организующий обработку запросов – CSoapHandler<THandler>. Здесь THandler – пользовательский класс, который будет унаследован от CSoapHandler<> явно или неявно при использовании атрибута “soap_handler”. Класс CSoapHandler<> унаследован от базового класса CSoapRootHandler. Этот класс реализует всю логику для создания и разбора SOAP сообщений, и используется в качестве базового класса для серверного и клиентского кода.

CSoapHandler<> реализует интерфейс IRequestHandler, который необходим для взаимодействия с ISAPI-расширением. При этом переопределяются два метода этого интерфейса – “InitializeHandler” и “HandleRequest”, реализация остальных методов добавляется в класс путем наследования от IRequestHandlerImpl<> – реализации интерфейса IRequestHandler по умолчанию.

Главной особенностью базового класса CSoapRootHandler является ручная генерация XML и разбор XML-сообщений с помощью парсера SAX, входящего в MS XML. Такой подход позволяет избежать накладных расходов, связанных с использованием MS XML DOM-парсера – полной загрузки и разбора XML, а также преобразований типов данных в automation-совместимые при работе с MS XML DOM-парсером.

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

Когда пользовательский класс использует атрибут “request_handler” и задает имя параметра sdl – в заголовочный файл с объявлением класса после раскрытия атрибута добавляется макрос:

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

Этот макрос добавляет в класс объявление typedef для класса CSDLGenerator, который и отвечает за генерацию WSDL.

При генерации WSDL используется шаблон Server Response File, имя которого объявляется как строковая константа в файле atspriv.h. Этот шаблон представляет собой совокупность статического текста и набора инструкций для генерации динамической части.(srf очень напоминает ASP-страницы, которые задают статическое содержание и правила, по которым генерируется динамическая часть). В srf-шаблоне используются специальные метки, которые означают вызов методов и конструкции “while”, “if” и т.п. Ниже приведен фрагмент srf для генерации WSDL:

{{whileGetNextFunction}}
{{while GetNextParameter}}
{{if IsArrayParameter}}
  <s:complexType name=\"{{GetFunctionName}}_{{GetParameterName}}_Array\">
    <s:complexContent>
      <s:restriction base=\"soapenc:Array\">
        <s:attribute ref=\"soapenc:arrayType wsdl:arrayType=
          {{if IsParameterUDT}}s0:
          {{else}}s:
          {{endif}}{{GetParameterSoapType}}
          {{if IsParameterDynamicArray}}[]
          {{else}}{{GetParameterArraySoapDims}}
          {{endif}}\"/>
      </s:restriction>
    </s:complexContent>
  </s:complexType>
{{endif}}
{{endwhile}}
{{endwhile}}

Преобразование такого шаблона производится с помощью класса CStencil, которому передается сам шаблон и указатель на интерфейс ITagReplacer. CStencil разбирает шаблон и вызывает указанные в нем методы, генерирующие динамическую часть. Обработчики задаются макросами в классе _CSDLGenerator, отвечающем за генерацию WSDL. Каждая строка в карте (см. листинг ниже) обработчиков ставит в соответствие инструкции из SRF функцию без параметров в классе _CSDLGenerator. Сам класс _CSDLGenerator работает подобно конечному автомату, сохраняя предыдущее состояние после вызова очередного обработчика. Например, обработчик OnGetNextFunction увеличивает внутренний счетчик текущей функции, а OnGetFunctionName использует этот счетчик, чтобы получить требуемое имя функции.

BEGIN_REPLACEMENT_METHOD_MAP(_CSDLGenerator)
  REPLACEMENT_METHOD_ENTRY("GetNextFunction", OnGetNextFunction)
  REPLACEMENT_METHOD_ENTRY("GetFunctionName", OnGetFunctionName)
  REPLACEMENT_METHOD_ENTRY("GetNextParameter", OnGetNextParameter)
  REPLACEMENT_METHOD_ENTRY("IsInParameter", OnIsInParameter)
  REPLACEMENT_METHOD_ENTRY("GetParameterName", OnGetParameterName)
  REPLACEMENT_METHOD_ENTRY("NotIsArrayParameter", OnNotIsArrayParameter)
  REPLACEMENT_METHOD_ENTRY("IsParameterUDT", OnIsParameterUDT)
  REPLACEMENT_METHOD_ENTRY("GetParameterSoapType", OnGetParameterSoapType)
  REPLACEMENT_METHOD_ENTRY("IsParameterDynamicArray", OnIsParameterDynamicArray)
  REPLACEMENT_METHOD_ENTRY("IsArrayParameter", OnIsArrayParameter)
  REPLACEMENT_METHOD_ENTRY("GetParameterArraySize", OnGetParameterArraySize)
  REPLACEMENT_METHOD_ENTRY("GetParameterArraySoapDims",
    OnGetParameterArraySoapDims)
  ...
END_REPLACEMENT_METHOD_MAP()
ПРИМЕЧАНИЕ

Возникает уместный вопрос, как функция без параметров OngetFunctionName возвращает имя функции? Специфика SRF заключается в том, что обработчики пишут непосредственно в результирующий поток IWriteStream, указатель на который передается при инициализации в методе SetStream(IWriteStream* pStream).

Класс CSDLGenerator реализует интерфейс ITagReplacer и генерирует WSDL с помощью класса CStencil.

CStencil s;
HTTP_CODE hcErr = s.LoadFromString(s_szAtlsWSDLSrf, 
                  (DWORD) strlen(s_szAtlsWSDLSrf));
if (hcErr == HTTP_SUCCESS)
{
  hcErr = HTTP_FAIL;
  CHttpResponse HttpResponse(pRequestInfo->pServerContext);
  HttpResponse.SetContentType("text/xml");
  if (s.ParseReplacements(this) != false)
  {
    s.FinishParseReplacements();
    SetStream(&HttpResponse);
    SetWriteStream(&HttpResponse);
    SetHttpServerContext(m_spServerContext);
    ATLASSERT( s.ParseSuccessful() != false );
    hcErr = s.Render(this, &HttpResponse);
  }
}

Для генерации WSDL "на лету" классу CSDLGenerator требуется доступ к описанию реализуемых компонентом интерфейсов и методов, которые будут вызываться по протоколу SOAP. Как в ATL 7.0 описываются интерфейсы, методы и параметры методов, вызываемые по протоколу SOAP, мы рассмотрим в следующем разделе.

Описание интерфейсов, методов и заголовков SOAP запросов

В SOAP Toolkit для получения информации о методах и параметрах методов компонентов, вызываемых по протоколу SOAP, использовалась библиотека типов, а для динамического вызова методов на основе информации в SOAP-запросе – интерфейс IDispatch. Несомненное достоинство такого подхода состоит в том, что интерфейс IDispatch и библиотеки типов хорошо документированы и широко используются в различных приложениях. Главные недостатки такого подхода – automation работает медленно, а вызов IDispatch::Invoke связан с накладными расходами, то есть преобразованиями типов в VARIANT и использованием информации из библиотеки типов. ATL использует собственный механизм описания интерфейсов, методов и параметров, главным “двигателем” которого являются ATL-атрибуты.

Метод, который будет вызываться по протоколу SOAP, должен объявляться с атрибутом “soap_method”:

[ soap_method ]
HRESULT HelloWorld(/*[in]*/ BSTR bstrInput, 
            /*[out, retval]*/ BSTR *bstrOutput)
{
  ...
}

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

ПРИМЕЧАНИЕ

В заголовочном файле atlsoap.h имеется предупреждение о том, что формат этих структур, вероятно, будет изменяться, и использовать их явно не рекомендуется. Таким образом, разработчику остается лишь полагаться на атрибуты, благодаря которым объявления структур добавятся автоматически. Такие же точно структуры генерируются на основе WSDL-файла и в клиентских классах-обертках утилитой SProxy.exe.

Для метода объявленного с атрибутом “soap_method”, создаются следующие структуры:

        struct ___HelloWorldService_CHelloWorldService_HelloWorld_struct
{
  BSTR bstrInput;
  BSTR bstrOutput;
};
        void *pvCurrent = ((unsignedchar *)pvParam)+pEntries[i].nOffset
        struct _soapmapentry
{
  ULONG nHash;
  constchar * szField;
  const WCHAR * wszField;
  int cchField;
  int nVal;
  DWORD dwFlags;

  size_t nOffset;
  constint * pDims;

  const _soapmap * pChain;

  int nSizeIs;
  ...
};
...
{
  0xA9ECBD0B, 
  "bstrInput", 
  L"bstrInput", 
  sizeof("bstrInput")-1, 
  SOAPTYPE_STRING, 
  SOAPFLAG_NONE | SOAPFLAG_IN | SOAPFLAG_RPC | SOAPFLAG_ENCODED |
    SOAPFLAG_NULLABLE,
  offsetof(__CHelloWorldService_HelloWorld_struct, bstrInput),
  NULL,
  NULL,
  -1,
},
        struct _soapmap
{
  ULONG nHash;
  constchar * szName;
  const wchar_t * wszName;
  int cchName;
  int cchWName;
  SOAPMAPTYPE mapType;
  const _soapmapentry * pEntries;
  size_t nElementSize;
  size_t nElements;
  int nRetvalIndex;

  DWORD dwCallFlags;
  ...
};

extern__declspec(selectany) const _soapmap __CHelloWorldService_HelloWorld_map =
{
  0x46BA99FC,
  "HelloWorld",
  L"HelloWorld",
  sizeof("HelloWorld")-1,
  sizeof("HelloWorld")-1,
  SOAPMAP_FUNC,
  __CHelloWorldService_HelloWorld_entries,
  sizeof(__CHelloWorldService_HelloWorld_struct),
  1,
  -1,
  SOAPFLAG_NONE | SOAPFLAG_RPC | SOAPFLAG_ENCODED,
  0xE6CAFA1C,
  "urn:HelloWorldService",
  L"urn:HelloWorldService",
  sizeof("urn:HelloWorldService")-1
};
ПРИМЕЧАНИЕ

В каждой из приведенных структур есть поля ULONG nHash. Они используются, чтобы быстро находить нужные данные, не сравнивая строки целиком. Например, когда получен запрос на вызов метода “HelloWorld” – от имени метода будет взят хэш и поиск в структурах будет осуществляться по значению хэша.

Аналогичные структуры добавляются в код при использовании атрибута “soap_header”. Этот атрибут позволяет передавать/получать информацию в заголовке SOAP запроса. Параметры этого атрибута указывают имя переменной члена для хранения содержимого заголовка, является ли заголовок обязательным и входным или выходным. Использование заголовка иллюстрирует следующий код:

[ soap_method ]
[ soap_header("m_Hdr", false, false, true) ]
HRESULT HelloWorld(/*[in]*/ BSTR bstrInput, 
          /*[out, retval]*/ BSTR *bstrOutput)
{
  ...
  m_Hdr = L"Some header";
  return S_OK;
}
BSTR m_Hdr;

Для доступа к сгенерированным структурам в пользовательский класс добавляется несколько функций, которые объявляются как чисто виртуальные в базовом классе CSoapRootHandler:

        virtual
        const _soapmap ** GetFunctionMap() = 0;
virtualconst _soapmap ** GetHeaderMap() = 0;
virtualconst wchar_t * GetNamespaceUri() = 0;
virtualconstchar * GetServiceName() = 0;
virtualconstchar * GetNamespaceUriA() = 0;
virtual HRESULT CallFunction(
  void *pvParam, 
  const wchar_t *wszLocalName, int cchLocalName,
  size_t nItem) = 0;
virtualvoid * GetHeaderValue() = 0;

Наибольший интерес представляет функция “CallFunction” – именно с ее помощью происходит диспетчеризация вызова во время обработки запроса. Тело функции генерируется провайдером атрибутов ATL в пользовательском классе и может выглядеть так:

ATL_NOINLINE inline HRESULT CHelloWorldService::CallFunction(
  void *pvParam, 
  const wchar_t *wszLocalName, 
  int cchLocalName,
  size_t nItem)
  {
    wszLocalName;
    cchLocalName;
    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, обрабатывая запрос, вызывает метод CallFunction и передает ему адрес блока памяти, в котором размещаются параметры метода, а также номер метода в карте методов.

Отладка приложений ATL Server

Отлаживать Web-приложения в Visual Studio 7.0 стало проще, главным образом благодаря тому, что теперь отладчик может автоматически находить нужный процесс сервера IIS, в котором выполняется код Web-приложения и подключаться к нему.

Для этого в свойствах проекта на закладке Debugging нужно указать URL, при обработке которого будут загружены отлаживаемые модули. Отладчик Visual Studio сгенерирует специальный HTTP-запрос (запрос будет использовать специальную команду DEBUG), в теле которого будет передан CLSID компонента. ISAPI-расширение, получая такой запрос, создает компонент с указанным CLSID и передает ему ID текущего процесса, отладчик Visual Studio подключается к этому процессу.

Проекты, созданные с помощью мастера “ATL Server”, по умолчанию для отладки используют URL, генерирующий WSDL-файл , например, “http://ivan/HelloWorld/HelloWorld.dll?Handler=GenHelloWorldWSDL”.

Чтобы начать отладку модуля, нужно установить точки останова на отлаживаемых методах и нажать F5 (меню Debug/Start). Появится окно браузера, отображающее указанный в настройках проекта URL (по умолчанию – сгенерированный WSDL-файл). Теперь можно запускать клиентские приложения – выполнение методов будет прервано на расставленных точках останова.

ПРИМЕЧАНИЕ

Возможность автоматического подключения отладчика обеспечивается только для DEBUG-сборок ISAPI-расширения. Чтобы включить поддержку отладки в RELEASE-сборку, до включения atlisapi.h нужно объявить символ препроцессора ATLS_ENABLE_DEBUGGING.

CDebugReportHook и WEBDbg

Еще одна новая возможность отладки Web-приложений – использование класса CDebugReportHook. Он перехватывает вызовы ATLTRACE и ATLASSERT, и передает информацию в именованный канал (pipe). Клиент WEBDbg (входит в состав утилит, поставляемых вместе с Visual Studio 7.0) отображает на экране сообщения из этого канала.

ПРИМЕЧАНИЕ

У класса CDebugReportHook есть конструктор, принимающий строку – имя удаленной машины, на которой будет открываться именованный канал

Очень полезная возможность WEBDbg заключается в том, что эта утилита способна показывать стек вызовов. Для этого нужно включить в меню View опцию “Stack Trace” и генерировать прерывание остановки int 3 при появлении заданного сообщения. Выбор сообщения производится с помощью фильтра сообщений, в котором могут использоваться регулярные выражения.

При невыполнении условия ATLASSERT, или при появлении сообщения, для которого включена опция “Break On Message”, WEBDbg предлагает выбор – остановить процесс, подключить отладчик (int 3) или продолжить выполнение дальше.


Рисунок 3. Отладка с WEBDbg.

Утилита трассировки SOAP-сообщений

Для отладки приложений ATL Server можно использовать утилиту трассировки SOAPTrace из SOAP Toolkit. Для этого нужно заставить клиента обращаться не к 80-му порту, а к порту 8080, на котором работает SOAPTrace. Если в качестве клиента используется приложение, созданное с помощью SOAP Toolkit, то URL сервера берется из WSDL-файла. Но в генерируемом WSDL-файле не указано порта 8080. В этой ситуации можно поступить так: сохранить сгенерированный сервером WSDL-файл на диске, заменить в этом файле URL сервера так, чтобы использовался порт 8080, и указать клиенту этот WSDL-файл.

Если клиент создан с помощью генератора SProxy, то манипуляции с WSDL-файлом не нужны, достаточно модифицировать URL, который появится в заголовочном файле, сгенерированном SProxy.

SOAPDebugApp

В состав примеров ATL Server, поставляемых вместе с Visual Studio 7.0, входит пример SOAPDebugApp, который позволяет отлаживать серверные приложения в адресном пространстве клиента. Основная идея заключается в том, что клиентские прокси-классы, генерируемые SProxy.exe, параметризуются классом для отправки и получения запросов, т.е. фактически этот класс выступает в роли транспорта для SOAP-сообщений. Пример SOAPDebugApp предлагает реализацию транспорта, которая просто загружает серверный модуль в адресное пространство клиента и эмулирует Web сервер, реализуя интерфейс IHttpServerContext. Благодаря этому все запросы клиента передаются напрямую в серверный модуль, минуя отправку по http. С помощью SOAPDebugApp можно эффективно отлаживать серверные модули и “шагать” отладчиком в соответствующие методы серверного модуля прямо из кода клиента.

Активация обработчиков запросов

Как уже упоминалось выше, ISAPI-расширение будет создавать экземпляр обработчика запроса каждый раз, когда поступает новый запрос от клиента. Это означает, что состояние объекта-обработчика запроса будет теряться между запросами, так как следующий запрос будет обрабатывать уже другой экземпляр.

Хранить состояние обработчик запросов может с помощью ISAPI-расширения. В создаваемом мастером “ATL Server” проекте для ISAPI-расширения нужно выбрать опцию “Blob Cache". Мастер добавит в код ISAPI-расширения поддержку соответствующего сервиса, а обработчик запросов сможет получать к нему доступ с помощью вызова IServiceProvider::QueryService.

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

Наш Web-сервис будет поддерживать такой интерфейс:

__interface IStateFullService
{
  [id(1)] HRESULT SetInformation([in] BSTR bstrInput);
  [id(2)] HRESULT GetInformation([out,retval] BSTR* pbstrOutput);
};

SetInformation будет получать строку от клиента и сохранять ее в Blob Cache. GetInformation будет извлекать ее из Blob Cache и возвращать клиенту. Для промежуточного хранения строки нельзя использовать переменную-член по двум причинам – во-первых, клиентов может быть несколько, и каждый присылает свою собственную строку, во-вторых, между запросами объект-обработчик запросов разрушается и теряет свое состояние.

Мы решим эту проблему, используя кэш в памяти, поддерживаемый ISAPI-расширением IMemoryCache. Идентифицировать клиентов мы будем с помощью cookie, передаваемого в заголовке запроса. Для этого пригодится атрибут “soap_header”:

[ soap_method ]
[ soap_header("m_sCookie", false, false, true)]
HRESULT SetInformation(/*[in]*/ BSTR bstrInput)
{
   m_sCookie = createCookie().Detach();
   CFileTime ftSpan = CFileTime::GetCurrentTime() + 
     CFileTimeSpan(CFileTime::Second*10);
   m_spBlobCache->Add(CW2A(m_sCookie), bstrInput, SysStringByteLen(bstrInput),
     &ftSpan , 0, 0, 0);
  }
  return S_OK;
}
  
[ soap_method ]
[ soap_header("m_sCookie", true, true, false) ]
HRESULT GetInformation(/*[OUT,retval]*/ BSTR* pbstrOutput)
{
  HCACHEITEM hItem = NULL;
  if(!pbstrOutput) return E_POINTER;
  *pbstrOutput = 0;
  // У метода атрибут soap_header устанавливает параметр required в true, // поэтому переменная m_sCookie ВСЕГДА будет инициализрована кодом //маршалинга ATL – это ведь не просто переменная, а заголовок SOAP запроса.if(SUCCEEDED(m_spBlobCache->LookupEntry(CW2A(m_sCookie), &hItem)))
  {
    void* pData = NULL;
    DWORD dwSize;
    if(SUCCEEDED(m_spBlobCache->GetData(hItem, &pData, &dwSize)))
    {
      *pbstrOutput = CComBSTR(dwSize, 
         reinterpret_cast<LPCOLESTR>(pData)).Detach();
    }
    m_spBlobCache->ReleaseEntry(hItem);
  }
  return S_OK;
}

  
CComBSTR createCookie()
{
  CSessionNameGenerator gen;
  DWORD dwSize = MAX_SESSION_KEY_LEN - 1;
  char buf[MAX_SESSION_KEY_LEN - 1];
  gen.GetNewSessionName(buf, &dwSize);
  return buf;
}

BSTR m_sCookie;
CComPtr<IMemoryCache> m_spBlobCache;

Метод SetInformation запоминает строку клиента в кэше и возвращает cookie с именем m_sCookie в заголовке запроса, метод GetInformation использует это cookie чтобы найти нужную строку в кэше и вернуть ее клиенту. Наш кэш использует фиксированное время жизни для элементов – мы его задаем как:

CFileTime ftSpan = CFileTime::GetCurrentTime() + 
    CFileTimeSpan(CFileTime::Second*10);

Это значит, что элемент будет удален из кэша через 10 секунд.

Переменная-член, хранящая содержимое запроса, объявлена как BSTR. Ее временем жизни, а также выделением и освобождением памяти для нее занимается класс CSoapRootHandler на основе рассматривавшихся выше структур, сгенерированных провайдером атрибутов ATL. Поэтому мы не можем, например, использовать класс-обертку CComBSTR. Память для строки будет выделена кодом ATL до входа в метод и освобождена после выхода. Метод GetInformation только выделяет память для этой строки, освобождаться она также будет после вызова метода.

ПРИМЕЧАНИЕ

Для переменных-членов, хранящих содержимое заголовка запроса можно использовать те же типы данных, что и для параметров методов – практически все простые типы, структуры и BLOB. За преобразования, маршалинг и выделение/освобождение памяти отвечает ATL-код в CSoapRootHandler, пользовательский код должен выделять память только для заголовков, которые отправляются клиенту, т.е. являются выходными.

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

Выделением и освобождением памяти для параметров методов и заголовков SOAP запросов занимается код в CSoapRootHandler, который и осуществляет маршалинг, т.е. прямое и обратное преобразование данных в формат, пригодный для передачи по протоколу SOAP. Поэтому пользовательский код, который выделяет память для выходных параметров и выходных заголовков должен использовать менеджер памяти ATL, его можно получить вызовом GetMemMgr(). Для BSTR-строк и automation-типов используется обычный распределитель памяти COM. Поэтому строка BSTR создается вызовом SysAllocString.

Клиента мы создадим как консольное приложение с поддержкой ATL и добавим ссылку на Web-сервис (Add Web Reference). Код клиента очень прост:

      #include
      "stdafx.h"
      #include
      "StateFull.h"
      int _tmain(int argc, _TCHAR* argv[])
{
  usingnamespace StateFullService;
  ::CoInitialize(0);
  {
    CStateFullService svc;
    CComBSTR bstrData = L"some data";
    HRESULT hr = svc.SetInformation(bstrData);
    ATLASSERT(SUCCEEDED(hr));
    CComBSTR bstrResult;
    hr = svc.GetInformation(&bstrResult);
    ATLASSERT(SUCCEEDED(hr));
    AtlCleanupValueEx(&svc.m_sCookie, svc.GetMemMgr());
  }
  ::CoUninitialize();
  return 0;
}

SProxy создает для заголовка переменную-член m_sCookie, ее содержимое после вызова SetInformation устанавливается значением, которое вернул сервер. Это значение используется затем в вызове GetInformation. Освободить память для заголовка должен клиент – это делается с помощью вызова функции AtlCleanupValueEx.

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

В реальных приложениях серверные компоненты могут хранить свое состояние не в памяти, а, например, в базе данных. В состав примеров ATL Server входит SOAPState, который демонстрирует создание инфраструктуры для хранения и получения состояния серверного компонента. ISAPI-расширение в этом примере реализует специальный сервис, скрывающий способ сохранения состояния, а SOAP-обработчики запросов реализуют специальный интерфейс, с помощью которого клиент может повлиять на продолжительность хранения состояния.

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

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

Серверные приложения, создаваемые с помощью SOAP Toolkit 3.0, представляют собой набор компонентов, поддерживающих интерфейс IDispatch. SOAP-запросы преобразуются компонентом SoapServer30 в COM-вызовы этих компонентов. В случае возникновения ошибок SoapServer анализирует содержимое IErrorInfo после вызова метода компонента и генерирует Soap Fault, заполняя его информацией из IErrorInfo, установленного компонентом.

Обработчик запросов ATL Server не является COM-компонентом в полном смысле этого слова – модуль Web-приложения не содержит tlb, для обработчиков запросов не делается никаких записей в реестре, и они не являются coclass’ами. Обработчики запросов ATL Server не устанавливают информацию об ошибках в стиле COM (через IErrorInfo). Вместо этого они возвращают HRESULT с установленным битом ошибки, а стандартная реализация обработчика запросов CSoapHandler из atlsoap.h просто использует FormatMessage для формирования описания ошибки по этому HRESULT. Вот соответствующий код из atlsoap.h:

_ATLTRY
{
  hr = CallFunctionInternal();
}
...
if (FAILED(hr))
{
  Cleanup();
  HttpResponse.ClearHeaders();
  HttpResponse.ClearContent();
  ...
  HttpResponse.SetStatusCode(500);
  GenerateAppError(&HttpResponse, hr);
  return AtlsHttpError(500, SUBERR_NO_PROCESS);
}

Если вызов метода CallFunctionInternal завершается с ошибкой, CSoapRootHandler вызывает виртуальную функцию GenerateAppError, которая и формирует нужный SOAP Fault. Реализация этой функции по умолчанию в CSoapHandler использует FormatMessage для переданного HRESULT:

LPWSTR pwszMessage = NULL;
DWORD dwLen = ::FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER
  | FORMAT_MESSAGE_FROM_SYSTEM,
  NULL, hr, 0, (LPWSTR) &pwszMessage, 0, NULL);
if(dwLen == 0)
{
  pwszMessage = L"Application Error";
}
hr = SoapFault(SOAP_E_SERVER, pwszMessage, dwLen ? dwLen : -1);

Поскольку GenerateAppError – виртуальная функция, нам ничего не стоит переопределить ее так, чтобы описание ошибки “вытаскивалось” из IErrorInfo, если он установлен.

Первая попытка передать клиенту информацию об ошибке выглядит так:

      virtual ATL_NOINLINE HRESULT GenerateAppError(IWriteStream *pStream, HRESULT hr)
{
  CComPtr<IErrorInfo> spInfo;
  if(::GetErrorInfo(0, &spInfo) == S_OK)
  {
    CComBSTR bstrDesc;
    spInfo->GetDescription(&bstrDesc);
    hr = SoapFault(SOAP_E_SERVER, bstrDesc, bstrDesc.Length());
  }
  else
    hr = CSoapHandler<CErrHandlingService>::GenerateAppError(pStream, hr);
  return hr;
}

В методе компонента устанавливаем IErrorInfo (сам компонент нужно унаследовать от CComCoClass<CLSID_NULL>):

[ soap_method ]
HRESULT HelloWorld(/*[in]*/ BSTR bstrInput, /*[out, retval]*/ BSTR *bstrOutput)
{
  return Error(L"evil error ocurred during request processing", 
    __uuidof(IErrHandlingService), E_UNEXPECTED);
}

SOAP Fault, генерируемый сервером для вызова метода HelloWorld, выглядит так:

<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP:Body>
     <SOAP:Fault>
       <faultcode>SOAP:Server</faultcode> 
       <faultstring>SOAP Server Application Faulted</faultstring> 
       <detail>evil error ocurred during request processing</detail> 
     </SOAP:Fault>
   </SOAP:Body>
</SOAP:Envelope>

Как видим, содержание SOAP Fault достаточно сильно отличается от того, который генерирует SOAP Toolkit в аналогичной ситуации. В отклике сервера нет кода ошибки HRESULT, которая произошла на сервере, а <faultstring> всегда устанавливается в “SOAP Server Application Faulted”, что может сбить с толку клиентское приложение. Сравните это с SOAP Fault, генерируемым сервером SOAP Toolkit:

<?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:errorInfo>
   </detail>
  </SOAP-ENV:Fault>
 </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Такой отклик сервера содержит гораздо больше информации об ошибке, которая произошла во время обработки запроса.

Создадим другой вариант GenerateAppError, который будет создавать XML-описание ошибки <mserror:errorInfo>:

      virtual ATL_NOINLINE HRESULT GenerateAppError(IWriteStream *pStream, HRESULT hr)
{

  CComBSTR bstrDesc, bstrSource; 
  CComPtr<IErrorInfo> spInfo;
  if(::GetErrorInfo(0, &spInfo) == S_OK)
  {
    spInfo->GetDescription(&bstrDesc);
    spInfo->GetSource(&bstrSource);
  }
  else
  {
     LPWSTR pwszMessage = NULL;
     DWORD dwLen = ::FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr, 0, (LPWSTR) &pwszMessage,
      0, NULL);

     if (dwLen == 0)
     {
       bstrDesc = L"Application Error";
     }
     else
     {
       bstrDesc = pwszMessage;
       LocalFree(pwszMessage);
     }
  }

  const LPCWSTR s_szErrorFormat = 
  L"<mserror:errorInfo xmlns:mserror=\"http://schemas.microsoft.com/"
  L"soap-toolkit/faultdetail/error/\">"
  L"   <mserror:returnCode>%d</mserror:returnCode>" 
  L"   <mserror:serverErrorInfo>"
  L"   <mserror:description>%ws</mserror:description>" 
  L"   <mserror:source>%ws</mserror:source>" 
  L"  </mserror:serverErrorInfo>"
  L"</mserror:errorInfo>";

  CStringW strFault;
  strFault.Format(s_szErrorFormat, hr, (WCHAR*)bstrDesc, (WCHAR*)bstrSource);

  hr = SoapFault(SOAP_E_SERVER, strFault, strFault.GetLength());
  return hr;
}

Теперь клиент SOAP Toolkit получает более полную информацию об ошибке:

      Set o = CreateObject("MSSOAP.SoapClient30")
o.MSSoapInit "http://ivan/ErrHandling/ErrHandling.dll?Handler=GenErrHandlingWSDL"onerrorresumenext
s = o.helloworld("from ATL 7.0")
msgbox "Code: " & hex(err.Number) & " Description: " & err.description

Клиент, написанный с помощью ATL 7.0, не ожидает такой подробной информации от сервера. Поэтому, чтобы добиться правильной обработки сообщений об ошибках на клиенте, придется написать код, который будет разбирать серверное сообщение <mserror:errorInfo> и устанавливать IErrorInfo в соответствии с SOAP Fault. Подробнее устройство клиента ATL 7.0 мы рассмотрим позже, а пока достаточно знать, что клиентская Proxy, сгенерированная с помощью SProxy.exe, получает в качестве параметра шаблона класс, отвечающий за передачу сообщений серверу. Вот пример объявления Proxy в сгенерированном h-файле:

      template <typename TClient = CSoapSocketClientT<> >
class CErrHandlingServiceT

Можно создать собственный класс TClient, который будет посредником между Proxy и настоящим классом для передачи сообщений. В функции нашего класса будет входить перехват ошибок, возвращенных сервером, разбор <mserror:errorInfo> , установка на клиенте правильного IErrorInfo и возвращение клиенту правильного кода ошибки сервера.

      template<class TClient>
class ExtendedClient : public TClient
{
public:
  ExtendedClient(LPCTSTR szUrl) : TClient(szUrl) {}
  ExtendedClient(LPCTSTR szServer, LPCTSTR szUri, ATL_URL_PORT nPort=80) : 
  TClient(szServer, szUri, nPort) {}

  HRESULT SendRequest(LPCTSTR szAction)
  {
    HRESULT hr = TClient::SendRequest(szAction);  
    if( (FAILED(hr)) && (GetClientError() == SOAPCLIENT_SOAPFAULT))
    {
      CStringA detail = CW2A(m_fault.m_strDetail);
      CReadStreamOnCString stm(detail);
      if(SUCCEEDED(m_ErrInfo.ParseFault( &stm )) &&
        (m_ErrInfo.m_nReturnCode != S_OK))
      {
        CComPtr<ICreateErrorInfo> spInfo;
        ::CreateErrorInfo(&spInfo);
        spInfo->SetDescription((LPOLESTR)m_ErrInfo.
          m_strDescription.GetString());
        spInfo->SetSource((LPOLESTR)m_ErrInfo.m_strSource.GetString());
        CComPtr<IErrorInfo> spErrInfo;
        spInfo.QueryInterface(&spErrInfo);
        ::SetErrorInfo(0, spErrInfo);
        hr = m_ErrInfo.m_nReturnCode;
      }
    }
    return hr;
  }

  CSoapErrInfo m_ErrInfo;
};

Наш класс параметризуется настоящим транспортным классом TClient и переопределяет метод SendRequest. Если запрос к серверу закончился с ошибкой (код ATL в таком случае всегда возвращает E_FAIL), и статус указывает на наличие дополнительной информации об ошибке (SOAPCLIENT_SOAPFAULT), мы получаем значение тега “detail”, в котором и находится расширенная информация об ошибке. Разбор этой информации осуществляется с помощью пары классов CSoapErrInfo и CSoapErrInfoParser. Эти классы осуществляют разбор XML с помощью парсера SAX, а их реализация сделана на основе ATL классов CSoapFault и CSoapFaultParser, которые разбирают SOAP Fault. Принцип работы парсера SAX напоминает алгоритм генерации SRF (Server Response File) – когда парсер встречает в разбираемом XML элементы или атрибуты, он вызывает методы обработчика, а обработчик использует модель конечного автомата, чтобы запоминать свое состояние между вызовами.

      class CSoapErrInfo;

class CSoapErrInfoParser : public ISAXContentHandlerImpl
{
private:

  CSoapErrInfo *m_pErrInfo;

  DWORD m_dwState;

  conststatic DWORD STATE_ERROR     = 0;
  conststatic DWORD STATE_ERRINFO    = 1;
  conststatic DWORD STATE_RETURNCODE   = 2;
  conststatic DWORD STATE_SERVERERRINFO = 4;
  conststatic DWORD STATE_SOURCE     = 8;
  conststatic DWORD STATE_DESC      = 16;
  conststatic DWORD STATE_RESET     = 32;
  conststatic DWORD STATE_SKIP      = 64;


  CComPtr<ISAXXMLReader> m_spReader;
  CSAXStringBuilder m_stringBuilder;
  CSkipHandler m_skipHandler;

  const wchar_t *m_wszSoapPrefix;
  int m_cchSoapPrefix;

public:

  // IUnknown interface
  HRESULT __stdcall QueryInterface(REFIID riid, void **ppv)
  {
    if (ppv == NULL)
    {
      return E_POINTER;
    }

    *ppv = NULL;

    if (InlineIsEqualGUID(riid, IID_IUnknown) ||
      InlineIsEqualGUID(riid, IID_ISAXContentHandler))
    {
      *ppv = static_cast<ISAXContentHandler *>(this);
      return S_OK;
    }

    return E_NOINTERFACE;
  }

  ULONG __stdcall AddRef()
  {
    return 1;
  }

  ULONG __stdcall Release()
  {
    return 1;
  }

  // constructor

  CSoapErrInfoParser(CSoapErrInfo *pErrInfo, ISAXXMLReader *pReader)
    :m_pErrInfo(pErrInfo), m_dwState(STATE_ERROR), m_spReader(pReader)
  {
    ATLASSERT( pErrInfo != NULL );
    ATLASSERT( pReader != NULL );
  }

  // ISAXContentHandler interface
  HRESULT __stdcall startElement( 
    const wchar_t * wszNamespaceUri,
    int cchNamespaceUri,
    const wchar_t * wszLocalName,
    int cchLocalName,
    const wchar_t * /*wszQName*/,
    int/*cchQName*/,
    ISAXAttributes * /*pAttributes*/)
  {
    struct _errinfomap
    {
      const wchar_t *wszTag;
      int cchTag;
      DWORD dwState;
    };

    conststatic _errinfomap s_errinfoParseMap[] =
    {
      { L"errorInfo", sizeof("errorInfo")-1,
         CSoapErrInfoParser::STATE_ERRINFO },
      { L"returnCode", sizeof("returnCode")-1,
         CSoapErrInfoParser::STATE_RETURNCODE },
      { L"serverErrorInfo", sizeof("serverErrorInfo")-1, 
         CSoapErrInfoParser::STATE_SERVERERRINFO },
      { L"description", sizeof("description")-1, 
         CSoapErrInfoParser::STATE_DESC },
      { L"source", sizeof("source")-1,
         CSoapErrInfoParser::STATE_SOURCE }
    };

    if (m_spReader.p == NULL)
    {
      return E_INVALIDARG;
    }

    m_dwState &= ~STATE_RESET;
    for (int i=0; 
      i<(sizeof(s_errinfoParseMap)/sizeof(s_errinfoParseMap[0])); i++)
    {
      if ((cchLocalName == s_errinfoParseMap[i].cchTag) &&
        (!wcsncmp(wszLocalName, 
          s_errinfoParseMap[i].wszTag, cchLocalName)))
      {
        DWORD dwState = s_errinfoParseMap[i].dwState;
        if ((dwState & (STATE_ERRINFO | STATE_SERVERERRINFO)) == 0)
        {
          m_stringBuilder.SetReader(m_spReader);
          m_stringBuilder.SetParent(this);

          m_stringBuilder.Clear();
          m_spReader->putContentHandler( &m_stringBuilder );
        }
        else
        {
          if ((dwState <= m_dwState) || 
            (cchNamespaceUri != 
              sizeof(SOAPERR_NAMESPACEA)-1) ||
            (wcsncmp(wszNamespaceUri, SOAPERR_NAMESPACEW,
              cchNamespaceUri)))
          {
            return E_FAIL;
          }
        }

        m_dwState = dwState;
        return S_OK;
      }
    }
    if (m_dwState > STATE_ERRINFO)
    {
      m_dwState = STATE_SKIP;
      m_skipHandler.SetReader(m_spReader);
      m_skipHandler.SetParent(this);

      m_spReader->putContentHandler( &m_skipHandler );
      return S_OK;
    }

    return E_FAIL;
  }

  HRESULT __stdcall startPrefixMapping(
    const wchar_t * wszPrefix,
    int cchPrefix,
    const wchar_t * wszUri,
    int cchUri)
  {
    if ((cchUri == sizeof(SOAPERR_NAMESPACEA)-1) &&
      (!wcsncmp(wszUri, SOAPERR_NAMESPACEW, cchUri)))
    {
      m_wszSoapPrefix = wszPrefix;
      m_cchSoapPrefix = cchPrefix;
    }

    return S_OK;
  }

  HRESULT __stdcall characters( 
    const wchar_t * wszChars,
    int cchChars);
};


class CSoapErrInfo
{
private:


public:

  // members
  HRESULT m_nReturnCode;
  CStringW m_strDescription;
  CStringW m_strSource;

  CSoapErrInfo()
    : m_nReturnCode(S_OK)
  {
  }


  HRESULT ParseFault(IStream *pStream, ISAXXMLReader *pReader = NULL)
  {
    if (pStream == NULL)
    {
      return E_INVALIDARG;
    }

    CComPtr<ISAXXMLReader> spReader;
    if (pReader != NULL)
    {
      spReader = pReader;
    }
    else
    {
      if (FAILED(spReader.CoCreateInstance(ATLS_SAXXMLREADER_CLSID)))
      {
        return E_FAIL;
      }
    }

    Clear();
    CSoapErrInfoParser parser(const_cast<CSoapErrInfo *>(this), spReader);
    spReader->putContentHandler(&parser);

    CComVariant varStream;
    varStream = static_cast<IUnknown*>(pStream);

    HRESULT hr = spReader->parse(varStream);
    spReader->putContentHandler(NULL);
    return hr;
  }


  void Clear()
  {
    m_nReturnCode = S_OK;
    m_strDescription.Empty();
    m_strSource.Empty();
  }
}; // class CSoapErrInfo

ATL_NOINLINE inline HRESULT __stdcall CSoapErrInfoParser::characters( 
  const wchar_t * wszChars,
  int cchChars)
{
  if (m_pErrInfo == NULL)
  {
    return E_INVALIDARG;
  }

  if (m_dwState & STATE_RESET)
  {
    return S_OK;
  }

  HRESULT hr = E_FAIL;
  _ATLTRY
  {
    switch (m_dwState)
    {
    case STATE_RETURNCODE:
      if (m_pErrInfo->m_nReturnCode == S_OK)
      {
        m_pErrInfo->m_nReturnCode = _wtol(wszChars);
        hr = S_OK;
      }
      break;
    case STATE_DESC:
      if (m_pErrInfo->m_strDescription.GetLength() == 0)
      {
        m_pErrInfo->m_strDescription.SetString(wszChars, cchChars);
        hr = S_OK;
      }
      break;
    case STATE_SOURCE:
      if (m_pErrInfo->m_strSource.GetLength() == 0)
      {
        m_pErrInfo->m_strSource.SetString(wszChars, cchChars);
        hr = S_OK;
      }
      break;
    case STATE_ERRINFO: case STATE_SERVERERRINFO : case STATE_SKIP:
      hr = S_OK;
      break;
    default:
      ATLASSERT( FALSE );
      break;
    }
  }
  _ATLCATCHALL()
  {
    hr = E_OUTOFMEMORY;
  }

  m_dwState |= STATE_RESET;

  return hr;
}

Если в теге detail есть вся необходимая информация – устанавливается IErrorInfo для клиента и возвращается правильный HRESULT.

Клиент использует этот класс так:

      void CheckError(HRESULT hr)
{
  if(FAILED(hr))
  {
    IErrorInfo* pInfo = 0;
    ::GetErrorInfo(0, &pInfo);
    _com_raise_error(hr, pInfo);
  }
}

int _tmain(int argc, _TCHAR* argv[])
{
  usingnamespace ErrHandlingService;
  ::CoInitialize(0);
  {
    CErrHandlingServiceT<ExtendedClient<CSoapSocketClientT<> > > svc;
    CComBSTR bstrResult;
    try
    {
      CheckError( svc.HelloWorld(CComBSTR(L"ATL 7.0 Client"),
         &bstrResult) );
    }
    catch(_com_error& e)
    {
      usingnamespace std;
      CStringA sDesc = CT2A((e.Description().length() == 0)
        ? e.ErrorMessage() : e.Description());
      cout << "Error caught: " << hex << e.Error()
        << " Description: " <<sDesc.GetString()  << endl;
    }
  }
  ::CoUninitialize();
  return 0;
}

Теперь и клиент ATL 7.0 способен получать от сервера расширенную информацию об ошибке и возвращать правильный HRESULT, соответствующий тому, который вернул метод Web сервиса.

ПРИМЕЧАНИЕ

Даже если необходимости в использовании IErrorInfo на сервере и клиенте нет, может оказаться полезным передавать клиенту корректный HRESULT, так как стандартный код ATL этого не делает, и клиент всегда будет получать E_FAIL. Все, что предоставляет стандартный код – возможность передать клиенту строку с ошибкой в теге detail (путем переопределения GenerateAppError, так как стандартная версия вызовет FormatMessage для кода ошибки). Кроме того, описанная выше схема обработки ошибок будет совместима с SOAP Toolkit.

Внутреннее устройство клиентского кода ATL 7.0

Клиент Web-сервиса создается с помощью утилиты Sproxy.exe, которая на основе WSDL генерирует proxy-класс, содержащий все методы серверного компонента, объявленные с атрибутом “soap_method”. Для методов и их параметров SProxy создает такие же точно структуры, как и те, которые создаются провайдером атрибутом ATL на серверной стороне (они были рассмотрены выше). Класс Proxy унаследован от CSoapRootHandler (как и серверный обработчик запросов) и использует тот же код для генерации и разбора SOAP-сообщений. Каждый из сгенерированных методов преобразует параметры в SOAP-представление, используя код маршалинга из CSoapRootHandler, и передает запрос серверу. Транспорт, используемый Proxy классом для коммуникаций с сервером, задается параметром шаблона TClient. По умолчанию используется HTTP через сокеты.

      template <typename TClient = CSoapSocketClientT<> >
class CErrHandlingServiceT

Транспортный класс должен обеспечивать методы, перечисленные в таблице 4:

Метод Описание
Конструктор Получает URL для соединения с сервером.
HRESULT GetClientReader(ISAXXMLReader **pReader) Возвращает интерфейс ISAXXMLReader для разбора XML сообщений.
GetClientError/SetClientError Позволяет прочитать/установить тип ошибки, тип описывается перечислением SOAPCLIENT_ERROR – например, SOAPCLIENT_OUTOFMEMORY, SOAPCLIENT_CONNECT_ERROR и т.п.
IWriteStream * GetWriteStream() Используется для записи исходящих SOAP сообщений.
HRESULT GetReadStream(IStream **ppStream) Используется для чтения отклика сервера.
void CleanupClient() Очистка клиента.
HRESULT SendRequest(LPCTSTR szAction) Передает серверу запрос, записанный в IWriteStream.
SetUrl/GetUrl Читает/изменяет URL сервера.
HRESULT SetProxy(LPCTSTR szProxy = NULL, short nProxyPort = 80) Задает настройки Proxy-сервера.
void SetTimeout(DWORD dwTimeout) Позволяет установить таймаут вызова.
int GetStatusCode() Возвращает код выполнения последней операции.

Так как TClient задается параметром шаблона, а proxy-класс наследуется от TClient, пользовательский класс TClient может реализовать не все перечисленные методы, а только те, которые используются кодом, сгенерированным SProxy.exe. Кроме того, пользовательский класс может реализовать свои собственные методы, которые будут доступны клиенту, так как proxy-класс наследуется от TClient.

При вызове метода proxy-класса производятся следующие действия:


Рисунок 4. Вызов метода proxy-класса

ПРИМЕЧАНИЕ

TClient в ATL 7.0 является аналогом коннектора в SOAP Toolkit. Коннектор представляет собой COM-компонент с заданным интерфейсом. TClient является обычным C++ классом и имеет большие возможности по взаимодействию с кодом клиента, чем коннектор.

ATL 7.0 включает несколько реализаций класса TClient, использующих разные API для передачи сообщений по HTTP (таблица 5).

Класс Описание
CSoapMSXMLInetClient Использует для передачи запросов ServerXMLHTTP.
CSoapSocketClientT Использует сокеты. Является параметром по умолчанию для Proxy класса. Параметризован классом для работы с сокетами (используя различный API).
CSoapWinInetClient Использует WinInet API.
ПРИМЕЧАНИЕ

Все эти реализации работают только по HTTP, что было характерно и для SOAP Toolkit. Если нужен другой вид транспорта – SMTP или MSMQ – придется разрабатывать свои классы, как на клиенте, так и на сервере.

Отмена SOAP вызовов

В этом разделе мы реализуем свой собственный класс TClient для использования в клиентских приложениях. Этот класс позволит отменять исходящие вызовы с помощью механизма callback-функций. Двигателем нашего класса (как и в примере коннектора, который рассматривался в статье “Использование протокола SOAP в распределенных приложениях. Microsoft SOAP Toolkit 3.0”) будет компонент XMLHTTPRequest из MSMXML. Одно из достоинств этого компонента заключается в том, что при доступе к защищенным ресурсам он использует стандартный GUI для запроса имени пользователя и пароля. При создании клиентских приложений это гораздо удобнее, чем задание информации для аутентификации через “свойства”, что приходится делать при использовании ATL-реализаций класса TClient.

Реализацию TClient мы разделим на два класса – один, базовый – CSoapClientBase, и унаследованный от него CSoapXMLHTTPClient. Первый будет обеспечивать общую функциональность – установку и чтение URL, задание таймаутов, а второй – CSoapXMLHTTPClient будет реализовывать логику работы с XMLHTTP и будет параметризован функтором для обратных вызовов.

        template<class Callback>
class CSoapXMLHTTPClient : public CSoapClientBase
{
public:
  // конструкторы
  ...
  void SetCallHandler(Callback cb)
  {
    m_cb = cb;
  }

  HRESULT GetReadStream(IStream **ppStream)
  {
    // получает свойство responseStream у объекта XMLHTTP
  }

  HRESULT SendRequest(LPCTSTR szAction)
  {
    HRESULT hr = ConnectToServer();
    if (FAILED(hr))
    {
      SetClientError(SOAPCLIENT_CONNECT_ERROR);
      return hr;
    }

    hr = SetActionHeader(szAction);
    if (FAILED(hr))
    {
      SetClientError(SOAPCLIENT_SEND_ERROR);
      return hr;
    }

    hr = m_spHttpRequest->send(CComVariant(GetWriteStreamData()));
    long nReadyState = 0;
    m_spHttpRequest->get_readyState(&nReadyState);
    while(nReadyState != 4)
    {
      if(m_cb() == true)
      {
        m_spHttpRequest->abort();
        hr = E_ABORT;
        break;
      }
      // обработка очереди сообщений
      MSG msg;
      const DWORD dwSleepTime = 100;
      DWORD dwTotal = 0;
      while(dwTotal < GetTimeout())
      {
        while(::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
        {
          ::TranslateMessage(&msg);
          ::DispatchMessage(&msg);
        }
        Sleep(dwSleepTime);
        dwTotal += dwSleepTime;
      }
      m_spHttpRequest->get_readyState(&nReadyState);
    }

    if(FAILED(hr))
    {
      SetClientError(SOAPCLIENT_SEND_ERROR);
      return hr;
    }

    if (GetStatusCode() == 500)
    {
      hr = E_FAIL;
      CComPtr<ISAXXMLReader> spReader;
      if (SUCCEEDED(GetClientReader(&spReader)))
      {
        SetClientError(SOAPCLIENT_SOAPFAULT);

        CComPtr<IStream> spReadStream;
        if (SUCCEEDED(GetReadStream(&spReadStream)))
        {
          if (FAILED(m_fault.ParseFault(spReadStream, spReader)))
          {
            SetClientError(SOAPCLIENT_PARSEFAULT_ERROR);
          }
        }
      }
    }

    return hr;
  }

  int GetStatusCode()
  {
    long lStatus;
    if (m_spHttpRequest->get_status(&lStatus) == S_OK)
    {
      return (int) lStatus;
    }
    return 0;
  }

  ~CSoapXMLHTTPClient()
  {
    m_spHttpRequest.Release();
  }

private:

  HRESULT ConnectToServer()
  {
    HRESULT hr = S_OK;

    if(m_spHttpRequest)
    {
      hr = m_spHttpRequest->open( CComBSTR(L"POST"), CComBSTR(GetUrl()), 
               CComVariant(true), CComVariant(), CComVariant());
    }
    else
      hr = E_FAIL;
    return hr;
  }

  void Init()
  {
    m_spHttpRequest.CoCreateInstance(__uuidof(XMLHTTP30));
  }

  HRESULT SetActionHeader(LPCTSTR szAction)
  {
	  // вызывает setRequestHeader у объекта XMLHTTP
  }

private:
  Callback m_cb;
  CComPtr<IXMLHTTPRequest> m_spHttpRequest;
};

Клиентский код использует этот класс так:

        struct call_handler
{
  booloperator()()
  {
    std::cout << ".";
    returnfalse;
  }
};

int _tmain(int argc, _TCHAR* argv[])
{
  usingnamespace HelloWorldService;
  ::CoInitialize(0);
  {
    CHelloWorldServiceT<CSoapXMLHTTPClient<call_handler> > svc;
    svc.SetTimeout(1000);
    CComBSTR bstrResult;
    HRESULT hr = svc.HelloWorldDelayed(CComBSTR(L"ATL 7.0 Client"),
       &bstrResult);
    ATLASSERT(SUCCEEDED(hr));
  }
  ::CoUninitialize();
  return 0;
}

Если задать “Basic”-аутентификацию для доступа к соответствующему виртуальному каталогу IIS, то при подключении клиента будет отображен стандартный диалог запроса имени пользователя и пароля.


Рисунок 5. Basic аутентификация

Типы данных и их преобразования

SOAP Toolkit ориентирован на вызов компонентов с помощью IDispatch::Invoke. Поэтому он поддерживает только automation-совместимые типы. Для преобразования пользовательских типов данных используются специальные компоненты – mapper-ы, преобразующие сложные типы в текстовое представление.

ATL 7.0 использует свои собственные механизмы для вызова серверных компонентов и преобразования параметров, и поэтому не ограничивается automation-типами. С другой стороны, в ATL 7.0 нет аналога mapper-ов, и поэтому разработчик Web-сервиса может использовать только те типы, поддержка которых встроена в код ATL.

ПРИМЕЧАНИЕ

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

В таблице 6 перечислены типы данных, которые можно использовать в параметрах и заголовках SOAP запросов.

Типы Описание
Простые типы bool, char, unsigned char, short, unsigned short, wchar_t, int, unsigned int, long, unsigned long, __int64, unsigned __int64, double, float, BSTR (а также __int8, __in16, __in32 и unsigned __int8, unsigned __int16, unsigned __int32)
Структуры объявляемые пользователем структуры; могут иметь членами любые поддерживаемые типы (в том числе и другие структуры)
Массивы одномерные и многомерные, размер должен описываться атрибутом “size_is”; элементы массива – любые поддерживаемые типы
Blob бинарные данные; описываются структурой ATLSOAP_BLOB
Enumeration перечисления передаются символьными именами элементов

Для передачи строк используется только один тип данных – BSTR. Попытки передавать строку как LPSTR приведут к тому, что в WSDL параметр будет описан как массив байтов. Кроме того, когда атрибут “size_is” не задан, указатель трактуется как массив из одного элемента, поэтому будет передан только первый байт такой строки.

Хотя полностью поддерживаются структуры, поддержки объединений нет (по крайней мере, пока). Хорошо известный пример объединения – VARIANT. Это означает, что при попытке передать VARIANT обязательно возникнут проблемы, и параметр придется описать по-другому.

Следует быть осторожным при объявлении параметров, имеющих тип "указатель", так как в этом случае для массивов нужно явно задавать атрибут “size_is”, с помощью которого описывается размер массива. Маршалинг массивов в ATL 7.0 очень похож на аналогичный маршалинг в COM, когда при описании параметров в IDL также нужно было указывать атрибут “size_is”. Применение атрибута “size_is” полностью соответствует правилам применения этого атрибута в IDL для описания массивов.

Если все же появляется необходимость передать тип, который не может быть описан поддерживаемыми типами (например, передать COM-объект по значению), можно использовать структуру ATLSOAP_BLOB – поток бинарных данных.

При описании методов и их параметров для вызова по протоколу SOAP допустимо использовать следующие атрибуты (Таблица 7).

Атрибут Описание
in Входной параметр
out Выходной параметр
size_is Задает размерность массива
retval Выходной параметр, который в языках высокого уровня будет являться “возвращаемым значением”.

Ниже мы рассмотрим несколько примеров работы со структурами, массивами и типом ATLSOAP_BLOB.

Передача структур

Рассмотрим пример передачи структур:

        struct Simple
{
  bool b;
};
struct SomeData
{
  long nSize;
  [ size_is(nSize) ]
  long* pData;
  Simple embedded;
  BSTR s;
};

Структура SomeData содержит вложенную структуру Simple и массив, размер которого задается с помощью атрибута size_is и хранится в переменной nSize. У метода серверного объекта есть один входящий (типа SomeData) и один возвращаемый параметр:

[id(3)] HRESULT StructTest([in] SomeData* sd, [out] SomeData* psd);

Код серверного объекта выделяет память для членов структуры, на которую указывает параметр psd:

[ soap_method ]
HRESULT StructTest(/*[in]*/ SomeData* sd, /*[out]*/ SomeData* psd)
{
   ZeroMemory(psd, sizeof(SomeData));
   psd->nSize = 3;
   psd->pData = reinterpret_cast<long*>(
    GetMemMgr()->Allocate(psd->nSize*sizeof(long)));
   ZeroMemory(psd->pData, psd->nSize*sizeof(long));
   psd->s = CComBSTR(L"s").Detach();
   psd->embedded.b = true;
   return S_OK;
}

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

Клиент вызывает метод серверного объекта так:

CTypesSampleService svc;
SomeData sIn = {0};
SomeData sOut = {0};
svc.StructTest(&sIn, 1, &sOut);
AtlCleanupValueEx(&sOut, svc.GetMemMgr());

Если присмотреться внимательно, то можно заметить, что на клиенте метод StructTest принимает 3 параметра, а не два, как в описании метода на сервере. Это связано с тем, что генератор WSDL на сервере описал входной in-параметр как массив, поэтому SProxy на клиенте сгенерировал еще один параметр метода StructTest – размер массива для первого параметра. Логика генератора WSDL понятна – размерность входного параметра-массива задавать с помощью атрибута size_is необязательно, она может быть вычислена на основе анализа количества элементов SomeData в запросе SOAP. С другой стороны, трактовка параметра [in] SomeData* sd как массива привела к тому, что на клиенте изменилась сигнатура метода StructTest, который теперь принимает три параметра, один из которых – размерность входного массива.

В этом примере мы уже не сможем реализовать клиента с помощью SOAP Toolkit, так как в нем структуры передаются как UDT и для клиента должна быть доступна библиотека типов с описанием структуры, а также WSML-файл, описывающий использование UDT-mapper-а. Хотя, возможно, если сгенерировать соответствующую tlb (в том случае, если структура содержит только automation-типы, а массивы к таковым не относятся) и написать WSML-файл, то для некоторых структур совместимости с SOAP Toolkit добиться удастся.

ПРИМЕЧАНИЕ

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

SOAP Toolkit также поддерживает структуры, но только как UDT (User Defined Type) – т.е. структуры, состоящие только из automation-совместимых типов. Такие UDT-структуры для приложений SOAP Toolkit должны передаваться через указатель (и такое же точно требование накладывает на передачу UDT Visual Basic 6.0).

Передача массивов

Для описания массивов во входящих и исходящих параметрах используется атрибут size_is. Рассмотрим небольшой пример с передачей одного входного и одного выходного массива:

[id(5)] HRESULT ArrTest([in]long nSize, [in, size_is(nSize) ]long* pData,
    [out]long* pnSize, [out, size_is(*pnSize)]long** ppOutData);

Реализация метода на сервере:

[ soap_method ]
HRESULT ArrTest(/*[in]*/long nSize, /*[in, size_is(nSize) ]*/long* pData,
    /*[out]*/long* pnSize, /*[out, size_is(, *pnSize)]*/long** ppOutData)
{
   *pnSize = 3;
   *ppOutData = reinterpret_cast<long*>(GetMemMgr()->Allocate(3*sizeof(long)));
   (*ppOutData)[0] = 1;
   (*ppOutData)[1] = 2;
   (*ppOutData)[2] = 3;
   return S_OK;
}

И код клиента:

CTypesSampleService svc;
int n, np;
int* p = NULL;
HRESULT hr = svc.ArrTest(&n, 1, &p, &np );
svc.GetMemMgr()->Free(p);

Как показали эксперименты со структурами, для входного параметра указателя необязательно указывать атрибут size_is – генератор WSDL все равно будет трактовать его как одномерный массив.

Передача бинарных данных

Единственный способ передавать сложные данные, которые не описываются поддерживаемыми типами (например, COM-объекты по значению) – использовать BLOB (ATLSOAP_BLOB). ATLSOAP_BLOB – это структура с двумя полями:

[ export ]
typedefstruct _tagATLSOAP_BLOB
{
  unsignedlong size;
  unsignedchar *data;
} ATLSOAP_BLOB;

Код маршалинга в CSoapRootHandler использует при передаче бинарных данных кодировку base64. В остальном использование ATLSOAP_BLOB не отличается от использования других структур. Рассмотрим такой пример:

[id(4)] HRESULT BlobTest([in]  ATLSOAP_BLOB* pData, 
                         [out] ATLSOAP_BLOB* ppData );

[ soap_method ]
HRESULT BlobTest(/*[in]*/ATLSOAP_BLOB* pData, /*[out]*/ATLSOAP_BLOB* ppData)
{
  ppData->size = 2;
  ppData->data = reinterpret_cast<byte*>(GetMemMgr()->Allocate(2));
  ppData->data[0] = 'A';
  ppData->data[1] = 'B';
  return S_OK;
}

Код клиента:

CTypesSampleService svc;
ATLSOAP_BLOB bIn = {0};
bIn.size = 1;
bIn.data = reinterpret_cast<byte*>(svc.GetMemMgr()->Allocate(2));
ATLSOAP_BLOB bOut = {0};
hr = svc.BlobTest(&bIn, 1, &bOut);
AtlCleanupValueEx(&bIn, svc.GetMemMgr());
AtlCleanupValueEx(&bOut, svc.GetMemMgr());

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

Передача COM-объектов по значению

Как и SOAP Toolkit, ATL не поддерживает передачу объектных ссылок и не содержит механизмов для управления временем жизни серверных объектов. Но, как и в случае SOAP Toolkit, можно осуществлять передачу COM-объектов по значению, преобразуя их в ATLSOAP_BLOB на сервере и выполняя обратное преобразование на клиенте.

ПРИМЕЧАНИЕ

Таким же способом можно передать по значению не только COM-компонент, но и любой сериализуемый объект.

В качестве иллюстрации этого подхода будет использован пример TView (см. статью “Использование протокола SOAP в распределенных приложениях. SOAP Toolkit 3.0”, RSDN Magazine 3'2002). TView позволяет просматривать информацию о запущенных процессах, модулях, хэндлах, используя архитектуру клиент-сервер и DCOM в качестве транспортного протокола. Серверная часть TView представляет собой COM+-компонент TView, а клиентская часть – MMC SnapIn, создающий экземпляр серверного объекта и получающий от него данные с помощью набора данных ADO (ADO Recordset). В предыдущей статье рассмотривался способ модификации TView, заставляющий его использовать протокол SOAP вместо DCOM,

Код TView и его описание можно найти в MSDN Magazine (декабрь 2000 г., http://msdn.microsoft.com/msdnmag/issues/1200/tview/default.aspx).

В этой статье мы заставим TView использовать SOAP с помощью Web-сервиса.

На сервере мы создадим Web-сервис-посредник, преобразующий COM-объекты в бинарное представление, а на клиенте – proxy-объект, выполняющий обратное преобразование.

Наш серверный проект TViewServer импортирует библиотеку типов TView:

        // ADO
        #import
        "libid:00000200-0000-0010-8000-00AA006D2EA4" no_namespace  rename("EOF", "adoEOF") 
// TView library#import"libid:48BBFB46-B3C3-11D1-860C-204C4F4F5020" no_namespace raw_interfaces_only

Здесь используется новая возможность директивы import – по LIBID библиотеки типов.

Код сервера создает настоящий компонент TView с помощью функции GetComponent и преобразует Recordset в бинарный поток с помощью функции Serialize:

CComPtr<ITView> GetComponent()
{
  CComPtr<ITView> spTView;
  HRESULT hr = spTView.CoCreateInstance(__uuidof(TView));
  ATLASSERT(SUCCEEDED(hr));
  return spTView;
}

template<class T>
HRESULT Serialize(T spT, ATLSOAP_BLOB& data)
{
  CComPtr<IPersistStream> spPStm;
  if(spT == 0) return E_UNEXPECTED;
  HRESULT hr = spT.QueryInterface(&spPStm);
  if(SUCCEEDED(hr))
  {
    CWriteStreamOnMemory<> stm(GetMemMgr());
    CLSID ref = CLSID_NULL;
    hr = spPStm->GetClassID(&ref);
    if(SUCCEEDED(hr))
    {
      hr = stm.Write(&ref, sizeof(CLSID), 0);
      if(SUCCEEDED(hr))
      {
        hr = spPStm->Save(&stm, FALSE);
        if(SUCCEEDED(hr))
        {
          data.size = stm.GetDataSize();
          data.data = stm.GetData();
        }
        else
          stm.CleanUp();
      }
    }
  }
  return hr;
}

В этом коде CWriteStreamOnMemory – созданный мной класс, использующий распределитель памяти ATL и реализующий интерфейс IStream на блоке памяти (динамически увеличивая блок по мере необходимости). Использование такого класса позволяет избежать лишнего копирования, как это было бы в случае использования CreateStreamOnHGlobal.

        template<ULONG dwInitialSize = 512>
class CWriteStreamOnMemory : public IStreamImpl
{
public:
  CWriteStreamOnMemory(IAtlMemMgr* pMemMgr) : m_pMemMgr(pMemMgr), 
    m_pb(0), m_size(0), m_cb(0){}
  
  byte* GetData()
  {
    return m_pb;
  }
  ULONG GetDataSize()
  {
    return m_cb;
  }
  void CleanUp()
  {
    if(m_pb)
      m_pMemMgr->Free(m_pb);
    m_pb = 0;
    m_size = 0;
    m_cb = 0;
  }
  
  STDMETHOD(Write)(constvoid * pv, ULONG cb, ULONG * pcbWritten)
  {
    ULONG offset = m_cb;
    m_cb += cb;
    if(m_cb > m_size) 
      Reallocate(m_cb);
    
    if(!m_pb) return E_OUTOFMEMORY;
    
    memcpy(m_pb + offset, pv, cb);
    if(pcbWritten)
      *pcbWritten = cb;
    return S_OK;
  }
  HRESULT __stdcall QueryInterface(REFIID riid, void **ppv)
  {
    if (ppv == NULL)
    {
      return E_POINTER;
    }

    *ppv = NULL;

    if (InlineIsEqualGUID(riid, IID_IUnknown) ||
      InlineIsEqualGUID(riid, IID_IStream) ||
      InlineIsEqualGUID(riid, IID_ISequentialStream))
    {
      *ppv = static_cast<IStream *>(this);
      return S_OK;
    }

    return E_NOINTERFACE;
  }

  ULONG __stdcall AddRef()
  {
    return 1;
  }

  ULONG __stdcall Release()
  {
    return 1;
  }

protected:
  void Reallocate(ULONG newSize)
  {
    if(m_pb)
    {
      while(m_size < newSize)
      {
        m_size *= 2;
      }
      m_pb = reinterpret_cast<byte*>(m_pMemMgr->Reallocate(m_pb, m_size));
    }
    else
    {
      m_size = max(dwInitialSize, newSize);
      m_pb = reinterpret_cast<byte*>(m_pMemMgr->Allocate(m_size));
    }
  }
private:
  IAtlMemMgr* m_pMemMgr;
  byte* m_pb;
  ULONG m_cb, m_size;
};

Методы серверного объекта повторяют интерфейс TView за исключением того, что тип _Recordset заменяется на ATLSOAP_BLOB, реализация перенаправляет вызов настоящему компоненту TView, а после этого преобразует Recordset в бинарный поток:

[ soap_method ]
HRESULT GetProcesses(/*[out, retval]*/ ATLSOAP_BLOB *ppRecordset)
{
  CComPtr<ITView> spTView = GetComponent();
  CComPtr<_Recordset> spRs;
  HRESULT hr = spTView->GetProcesses(&spRs);
  if(SUCCEEDED(hr))
    hr = Serialize(spRs, *ppRecordset);
  return hr;
}

Клиентская часть представляет собой обычный ATL-проект для in-process сервера, в котором находится единственный объект Proxy, поддерживающий интерфейс ITView. Он будет заменять клиентам настоящий TView. Для обратного преобразования из бинарного потока в Recordset используется метод Restore:

        template<class T>
HRESULT Restore(CComPtr<T>& spT, ATLSOAP_BLOB& data)
{
  HRESULT hr = S_OK;
  CComPtr<IStream> spStm;
  CComObject<CMemStream>* pStm;
  CComObject<CMemStream>::CreateInstance(&pStm);
  hr = pStm->init(data.data + sizeof(CLSID), data.size - sizeof(CLSID));
  spStm = pStm;
  
  if(SUCCEEDED(hr))
  {   
    CLSID clsid = *(reinterpret_cast<CLSID*>(data.data));
    CComPtr<IUnknown> spUnk;
    hr = spUnk.CoCreateInstance(clsid);
    if(SUCCEEDED(hr))
    {
      CComPtr<IPersistStream> spPStm;
      hr = spUnk.QueryInterface(&spPStm);
      if(SUCCEEDED(hr))
      {
        hr = spPStm->Load(spStm);
        if(SUCCEEDED(hr))
        {
          hr = spPStm.QueryInterface(&spT);
        }
      }
    }
  }
  return hr;
}

Метод Restore использует вспомогательный класс CMemStream, который позволяет обращаться к блоку памяти в структуре ATLSOAP_BLOAB через интерфейс IStream. Реализация этого вспомогательного класса тривиальна и здесь не приводится.

Методы proxy-объекта перенаправляют вызовы классу, сгенерированному Sproxy.exe, а затем преобразуют ATLSOAP_BLOB в Recordset:

STDMETHOD(GetProcesses)(/*[out, retval]*/ _Recordset **ppRecordset)
{
  ATLSOAP_BLOB data = {0};
  HRESULT hr = m_svc.GetProcesses(&data);
  if(SUCCEEDED(hr))
  {
     CComPtr<_Recordset> spRs;
     hr = Restore(spRs, data);
     AtlCleanupValueEx(&data, m_svc.GetMemMgr());
     if(SUCCEEDED(hr))
       *ppRecordset = spRs.Detach();
  }
  return hr;
}

CTViewServerServiceT<CSoapMSXMLInetClient> m_svc;

Как и в прошлый раз (см. статью “Использование протокола SOAP в распределенных приложениях. SOAP Toolkit 3.0”) в коде клиента TView нам потребуется изменить лишь CLSID компонента, чтобы вместо настоящего TView использовался наш Proxy объект, перенаправляющий вызов серверу по протоколу SOAP, для этого в файле processfolder.cpp нужно найти строчку с вызовом CoCreateInstance и заменить ее на такой код:

CLSID clsid = CLSID_NULL;
CLSIDFromProgID(L"TViewProxy.Proxy", &clsid);
HRESULT hr = CoCreateInstanceEx(clsid, NULL,
   CLSCTX_ALL, &csi, 1, &mqi);

Существенное различие по сравнению с использованием SOAP Toolkit заключается в том, что в этом примере нам понадобился объект-заместитель на стороне сервера. Этот заместитель выполняет преобразование параметров и после этого передает вызов настоящему компоненту TView. Аналогичный пример для SOAP Toolkit не требовал на сервере ничего, кроме настоящего компонента TView и созданных вручную файлов WSDL и WSML, описывающих способ передачи набора данных ADO.

Заключение

ATL 7.0 предоставляет разработчику распределенных приложений инфраструктуру для быстрой разработки – большое количество классов, поддерживающих различные протоколы, шифрование и подпись данных, работу с пулом потоков, менеджеры памяти и многое другое. При этом код ATL Server уменьшает накладные расходы, связанные с обработкой запроса, за счет эффективного использования потоков, специализированных распределителей памяти, работы с XML с помощью парсера SAX, отказа от automation-типов данных и преобразований в них. Поэтому среди существующих на сегодняшний день программных средств для разработки SOAP-приложений ATL Server может оказаться наиболее эффективным по использованию серверных ресурсов и скорости обработки запросов. Усовершенствованные со времен Visual Studio 6.0 средства отладки Web-приложений, автоматическое копирование файлов в виртуальный каталог IIS и поддержка ATL-атрибутов в значительной степени облегчают процесс разработки.

В таблице 8 сравниваются возможности SOAP Toolkit и ATL Server по разработке SOAP приложений.

SOAP Toolkit ATL Server
Механизм вызова серверного кода SoapServer вызывает COM компонент с помощью IDispatch::Invoke. Используются специальные структуры, генерируемые провайдером атрибутов ATL. Вызов происходит как обычный C++-вызов виртуальной функции.
WSDL и WSML-файлы Генерируются утилитой, входящей в состав SOAP Toolkit на основе библиотеки типов серверных компонентов. Необходимы во время выполнения как серверу SoapServer, так и клиенту – на основе информации из этих файлов происходит диспетчеризация вызова. Используется только WSDL-файл, который при необходимости генерируется автоматически кодом ATL Server. WSDL на сервере не используется, а на клиенте необходим для генерации Proxy класса однократно и во время выполнения не используется.
Клиентский Proxy COM-объект SoapClient, поддерживающий динамический IDispatch. Все вызовы происходят с помощью IDispatch::Invoke C++ класс, генерируемый утилитой Sproxy.exe на основе WSDL.
Открытость для изменения функциональности Позволяет задавать пользовательские компоненты-коннекторы, отвечающие за передачу SOAP-сообщений, и mapper-ы для преобразования типов данных.Код SoapServer и SoapClient недоступен разработчику и не может быть изменен. Разработчику доступны исходные тексты ATL. Для клиентских классов есть аналог коннектора, аналогов mapper-ов нет. Единственная недоступная часть ATL Server – структуры с описаниями методов и параметров, которые генерируются автоматически провайдером атрибутов.
Поддерживаемые типы данных Все automation-типы, UDT, VARIANT. Преобразования пользовательских типов возможны за счет использования mapper-ов (но пользовательский тип должен быть automation-совместимым). Простые типы, строки, структуры, массивы и перечисления, а также бинарные данные ATLSOAP_BLOB.
Обработка ошибок На основе интерфейса IErrorInfo, который в точности передается клиенту. Строка с описанием ошибки.
Поддержка DIME С версии 3.0. Не поддерживается.
Эффективность SOAP Toolkit использует automation и MSXML DOM-парсер, что несколько снижает эффективность. ATL Server повсеместно использует SAX-парсер для разбора XML, что несколько эффективнее.
Сохранение сигнатуры метода В точности сохраняет. При работе с массивами и атрибутом size_is сигнатуры методов клиента могут не совпадать – появляются дополнительные параметры, некоторые параметры могут меняться местами.

Ссылки

  1. Раздел в MSDN, посвященный ATL Server и всему, что с ним связано
  2. ATL Server reference
  3. Статья на codeproject об изменениях в ATL 7.0
  4. Статья на codeproject о ATL Server
  5. Пример TView, использовавшийся для демонстрации перехода от DCOM к SOAP


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