Сообщений 1 Оценка 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 сигнатуры методов клиента могут не совпадать – появляются дополнительные параметры, некоторые параметры могут меняться местами. |
Сообщений 1 Оценка 135 Оценить |