Оценка 135 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
В предыдущей статье мы научились создавать распределенные 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, предназначенной для разработки 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 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 включает в себя четыре основных элемента:
IIS используется как Web-сервер, принимающий HTTP-запросы клиента и передающий их на обработку приложениям ATL Server посредством ISAPI-интерфейса.
Во время разработки приложений ATL Server для управления настройками виртуального каталога IIS, в котором будут размещаться генерируемые файлы, и автоматизации копирования нужных файлов в этот виртуальный каталог можно использовать закладку “Web Deployment” в свойствах проекта приложения ATL Server.
На этой закладке можно настроить имя виртуального каталога IIS, в который будут копироваться файлы, указать дополнительные файлы для автоматического копирования, задать уровень защиты виртуального каталога и установить режим копирования – с остановкой Web сервера (это нужно для ISAPI-расширений, так как во время работы Web сервера модуль ISAPI нельзя перезаписать) или без.
Рисунок 1. Настройка проекта “Web Deployment”
Основная роль 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-приложения и является обработчиком запросов, полученных от 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. Стандартная реализация этого интерфейса находится в классе IRequestHandlerImpl<>, а пользовательские классы, как правило, наследуются от CRequestHandlerT или CSoapHandler (для XML Web-сервисов).
Для доступа к ISAPI-расширению класс IRequestHandlerImpl объявляет несколько переменных-членов, хранящих указатели на интерфейсы ISAPI-расширения (таблица 3).
| Переменная | Описание |
|---|---|
| IRequestHandlerImpl::m_spExtension | IIsapiExtension |
| IRequestHandlerImpl::m_spServiceProvider | IServiceProvider |
| IRequestHandlerImpl::m_spServerContext | IHttpServerContext |
Наиболее важные методы интерфейса IRequestHandler:
XML Web-сервисы, создаваемые на основе ATL Server, используют базовую архитектуру ATL Server, расширяя ее специфичными для обработки SOAP-запросов элементами.

Рисунок 2. Архитектура XML Web сервиса.
Запросы клиентов попадают в ISAPI-расширение (диспетчер), которое выделяет из запроса имя нужного модуля и передает ему управление. В модуле Web-приложения из тела SOAP-запроса выделяются параметры, которые преобразуются из текстового (сериализованного) в бинарное представление. Далее управление получает непосредственно код Web-приложения. После обработки запроса возвращаемые значения преобразуются в текстовое представление и формируется XML-отклик сервера. Этот отклик передается клиенту через ISAPI-расширение и Web-сервер.
Теперь, когда мы узнали, как устроены приложения 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 в данном случае указывают, что:
Чтобы увидеть наше серверное приложение в действии, нужен клиент. Проще всего реализовать его с помощью кода на 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 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<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-парсером.
Когда пользовательский класс использует атрибут “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 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 и передает ему адрес блока памяти, в котором размещаются параметры метода, а также номер метода в карте методов.
Отлаживать 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. |
Еще одна новая возможность отладки 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.
Для отладки приложений ATL Server можно использовать утилиту трассировки SOAPTrace из SOAP Toolkit. Для этого нужно заставить клиента обращаться не к 80-му порту, а к порту 8080, на котором работает SOAPTrace. Если в качестве клиента используется приложение, созданное с помощью SOAP Toolkit, то URL сервера берется из WSDL-файла. Но в генерируемом WSDL-файле не указано порта 8080. В этой ситуации можно поступить так: сохранить сгенерированный сервером WSDL-файл на диске, заменить в этом файле URL сервера так, чтобы использовался порт 8080, и указать клиенту этот WSDL-файл.
Если клиент создан с помощью генератора SProxy, то манипуляции с WSDL-файлом не нужны, достаточно модифицировать URL, который появится в заголовочном файле, сгенерированном SProxy.
В состав примеров 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. |
Клиент 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 – придется разрабатывать свои классы, как на клиенте, так и на сервере. |
В этом разделе мы реализуем свой собственный класс 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.
Как и 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 сигнатуры методов клиента могут не совпадать – появляются дополнительные параметры, некоторые параметры могут меняться местами. |
Оценка 135 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|