Продолжается подписка на наши издания! Вы не забыли подписаться?

Как проектировать повторно используемые HTTP-компоненты, используя WinInet и COM

Каждый раз при использовании Windows Internet API в собственном приложении кажется, будто пишешь одни и те же строки кода снова и снова. Каждый раз написание новой WinInet-функции выглядит, словно ты повторяешь уже написанное с незначительными изменениями. Вероятно, это происходит из-за общих принципов разработки Интернет-приложений. Обычно, все транзакции, которые использует данный протокол Интернет, используют одну и ту же логику высокого уровня. Разумеется, при этом необходимо определенную часть транзакций Интернет подстраивать под конкретную задачу.

Например, представим, что нам нужно создать приложение, использующее HTTP для соединения с различными серверами в Интернет посредством TCP/IP. Естественным подходом будет написание Win32-функций на базе WinInet, которые обрабатывали бы передачу данных. Наиболее дальновидные разработчики будут искать подход, обеспечивающий повторное использование определенных функций. Разработчики, работающие с объектно-ориентированными системами смогут для инкапсуляции нужной функциональности даже разработать свои С++-классы. Независимо от реализации передачи, каждой задаче для обработки различных форматов HTTP необходимы различные методы HTTP, заголовки, входные форматы данных, имена хостов, номера портов и программа синтаксического разбора. Для того, чтобы увеличить число слоев или сложность, одним запросам необходимо шифрованное соединение между узлами (Secure Sockets Layer - SSL), в то время, как другие запросы могут передавать текст в чистом виде.

Какие пути можно найти для решения этих вопросов проектирования, если используются стандартные функции Win32 или классы С++? Нет ли лучшего подхода, который обеспечил бы возможности повторного использования и расширяемости кода WinInet? Если вы еще не догадались, то можно и подсказать - это архитектура СОМ. Если вы бизнесмен и продаете универсальные продукты, настраиваемые приложения или компоненты программного обеспечения, то вы сделаете всех вокруг себя (включая себя самого) несколько счастливее, найдя способ создания повторно используемого кода.

Большинство разработчиков стараются применить это на практике. СОМ-подход в разработке кода WinInet, предложенный здесь, поможет на один шаг приблизиться к использованию СОМ в повседневной работе. Понятно, что СОМ - это отличное решение для создания повторно используемых и расширяемых компонентов WinInet. Разбиение функциональности, описанной в сценарии HTTP, на множество СОМ-объектов, поможет повторно использовать наиболее характерные компоненты и переписывать только компоненты для определенных задач. Такой подход позволяет вам по-новому применять уже опробированные компоненты и минимизировать написание кода для задач, связанных с WinInet. Более того, СОМ-подход предусматривает динамичную расширяемость приложений простым добавлением новых компонентов к системе пользователя во время работы.

В этой статье показано, как писать повторно используемые СОМ-компоненты HTTP WinInet. Прежде всего, дано вводное обозрение WinInet и показано, как можно начать использовать WinInet в собственных приложениях. После этого мы обсудим типичную модель приложения клиент/сервер и углубимся в детали HTTP. Имея эту базу, рассмотрим, как реализовывать два COM-компонента на базе ATL - IHttpRequest и IQuoteProvider. И, наконец, покажем, как все это связать в приложении, пример которого называется Stock Watcher (Отслеживание Курса Акций). Stock Watcher получает биржевые котировки сот различных провайдеров биржевых котировок Интернет и предоставляет их пользователю. Даже если вы больше ничего не узнаете из этой статьи, то, как минимум, сможете получить полезное приложение для своего бизнеса.

Введение в WinInet

WinInet - это высокоуровневый интерфейс для протоколов HTTP, FTP и Gopher Internet, базирующихся на TCP/IP. WinInet позволяет добавлять HTTP-, FTP- и Gopher-функциональность в приложение, без необходимости понимания стандартов протоколов или слежения за изменениями в них. Эта прозрачность - одно из ключевых преимуществ WinInet. По мере развития стандартов, WinInet также развивается, сохраняя протокол и интерфейс во взаимном соответствии. Не имея такого высокоуровневого интерфейса, как Winsock или WinInet, было бы необходимо писать код, отвечающий за обработку всех уровней TCP/IP-протокола. Рисунок 1 показывает уровни протокола TCP/IP. Каждый уровень отвечает за выполнение определенных задач и играет важную роль в функциональности TCP/IP в целом.

wpe1.jpg (15317 bytes)

рис.1 Уровни протокола TCP/IP.

Уровень протокола приложения состоит из таких протоколов как HTTP, FTP или Gopher, но содержит и множество других - POP, SMTP, NNTP Telnet и Finger. Это те протоколы, с которыми приложения работают наиболее интенсивно. Наиболее часто используемые средства для разработки на уровне протокола приложения - это Winsock и WinInet. В то время, как Winsock нуждается в написании кода для протокола на уровне приложения, WinInet инкапсулирует и скрывает множество деталей протокола на уровне приложения.

Протокол приложения находится на высшем уровне транспортного протокола. Наиболее общие протоколы транспортного уровня - это TCP и UDP. TCP - это надежный протокол, ориентированный на соединение; UDP - ненадежный протокол, не ориентированный на соединение. С помощью TCP возможна организация полнодуплексной связи между машинами типа один-к-одному. С другой стороны, UDP позволяет пересылать сообщения на множество хостов. В отличие от TCP, UDP отсылает пакеты, но не отслеживает, получены ли они. В то время, как UDP имеет свои преимущества (в особенности, в играх типа Quake), TCP наиболее используемый протокол на уровне приложений.

Следующий уровень - это протокол Интернет (Internet Protocol -IP). IP выполняет единственную базовую задачу: он ищет способы соединения. Например, при посылке электронной почты кому-либо, IP находит пути для передачи ему данных. IP не отслеживает ошибки при передаче; это - обязанность транспортного протокола. Подобно UDP, IP - протокол без соединения; он просто определяет, как именно пакет должен попасть к своему адресату. Для небольшой сети это слишком очевидно, но в Интернет существует множество хостов, через которые может пройти маршрут пакета.

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

Рисунок 2 показывает, как модель протокола работает в случае с HTTP. Пользователь организует HTTP-запрос, который обрабатывается фоновым HTTP-интерпретатором на машине клиента. Все данные, включая запрос, пересылаются на сервер по TCP/IP. После обработки запроса HTTP-интерпретатором сервера, посылается ответ, который обрабатывается HTTP-интерпретатором клиента. Мы сейчас рассмотрим ту часть процесса, которая задействует HTTP-интерпретатор со стороны клиента.

wpe2.jpg (16677 bytes)

Рисунок 2. HTTP-модель клиент/сервер

Как видите, без использования интерфейсов высокого уровня, разработка Интернет-приложений достаточно сложная задача. Но как определить, что использовать - Winsock или WinInet? Если в приложении требуется использовать HTTP, FTP или Gopher, то WinInet - лучший выбор по определению. Если требуется использовать другие протоколы Интернет уровня приложений, то единственный возможный выбор - Winsock.

Знакомство с WinInet

Общая функциональность WinInet содержится в файле WinInet.dll. Если у вас установлен Internet Explorer 3.0 или выше, то вы сможете найти его в системном каталоге Windows - Internet Explorer строится на высшем уровне WinInet. Имея Internet Explorer как часть операционной системы Windows, вы автоматически имеете установленный WinInet.

Для того, чтобы использовать WinInet в своем приложении, необходимо сделать две вещи. Во-первых, следует включить WinInet.h везде, где планируется вызывать функцию WinInet. Во-вторых, подключите WinInet.lib. Предположим, что в системе используется Visual C++ 5.0. Тогда WinInet.h можно найти в каталоге DevStudio\VC\include, а WinInet.lib - в каталоге DevStudio\VC\lib. Если используется другая среда разработки, либо предыдущие версии Microsoft Visual C++, то необходимо установить Microsoft Internet Client SDK (также называемый Internet Explorer 4.0 Author's Toolkit). Internet Client SDK, который можно получить на http:// www.microsoft.com/msdn/sdk/inetsdk/asetup/default.htm, содержит файлы WinInet, которые вам понадобятся при разработке приложений. После того, как Microsoft Internet Client SDK будет установлен, WinInet.h будет находиться в каталоге INetSDK\include, а WinInet.lib - в каталоге INetSDK\lib.

Документация по WinInet может быть получена из двух основных источников. Можно получить полную документацию по WinInet API, интегрированную с help-файлами Visual С++ 4.2 или выше. Однако, наиболее полной и обновленной документацией с примерами кодов и ссылками является online-документация по Client SDK.

Изначально WinInet была создана для программистов, использовавших Win32, поэтому сходство со стандартным Win32 API очень велико. Все Internet API, используют новый тип обработки в Интернет, называемый HINTERNET. Полную иерархию HINTERNET можно найти в документации по Internet Client SDK.

Кроме того, Microsoft предусматривает набор оболочек классов, инкапсулирующий функциональность WinInet API. (см. рис. 3). Классы WinInet MFC предусматривают параметры по умолчанию, исключения при обработке и буферизованные I/O. Более того, при работе с MFC, они включаются в проект приложения. Если планируется использовать MFC-классы, вместо WinInet.h необходимо включить afxinet.h.

wpe4.jpg (32458 bytes)

Рисунок 3. Классы MFC WinInet

Протокол HTTP

Вероятно, HTTP - наиболее широко применяемый на уровне приложений протокол Интернет. Когда вы обращаетесь к Web-сайту, то для соединения с Web-сервером и пересылки всех файлов (текстов, изображений, звуков, управляющих элементов ActiveX и пр.), браузер использует протокол HTTP вместе с определенным URL. Поскольку HTTP чаще всего используется для пересылки по Интернет HTML-файлов, он может использоваться и для передачи любых данных в любых форматах.

HTTP определяет формат запроса со стороны клиента и ответа со стороны сервера. Базовая транзакция HTTP состоит из четырех шагов:

1. Осуществление соединения (TCP/IP).

2. Клиент посылает запрос на сервер.

3. Сервер посылает ответ клиенту.

4. Прекращение соединения (TCP/IP).

Большинство HTTP-серверов ожидают запросы по известному HTTP-порту 80. Хотя обычно для HTTP-соединения используется порт 80, для осуществления такого соединения можно использовать и другие заданные порты.

В отличие от бинарных протоколов TCP и IP, HTTP-протокол запроса и ответа базируется на ASCII и гораздо более прост в работе. Например, HTTP-запрос содержит метод, URI (унифицированный идентификатор ресурсов - Uniform Resource Identifier), версию протокола, список заголовков, пустую строку и тело сущности (в смысле, данные):

Method URI ProtocolVersion
Headers
CRLF (пустая строка)
Enity-Body

Использование CRLF (последовательности возврата каретки) очень важно в формате запроса. CRLF сообщает HTTP-серверу, где начинается тело сущности. Если CRLF не использовать, это приведет к лишней головной боли. Приведем пример типичного HTTP-запроса:

POST /cgi-bin/basketball-scores
HTTP/1.0
Accept: text/plain, text/html
Accept: image/gif, image/jpeg
Accept-Encoding: x-compress; x-zip
Accept-Language: en
User-Agent: MSIE/4.0
If-Modified-Since: Mon, 10 Jan 1998
     13:22:34 GMT
Pragma: no-cache
Content-Type: text/plain
Content-Lenght: 43
Team=Utah Jazz&Team2=Portland 
Trailblazers

Формат HTTP-ответа во многом схож с форматом запроса. Все, за исключением первой строки, точно такое же. Приведем формат HTTP-ответа:

HTTP-Version Status-Code Reason
Phrase
Headers
CRLF (пустая строка)
Enity-Body

Первая строка в формате ответа является статусной строкой ответа. В ней содержится важная информация о статусе запроса. Тело сущности содержит данные, возвращенные сервером. Приведем пример HTTP-ответа, возвращенного на предыдущий запрос:

HTTP/1.0 200 OK
Server: Microsoft-IIS/2.0
Date: Mon, 3 Jan 1998 13:22:34 GMT
Content-Type: text/html
Last-Modified: Mon, 10 Jan 1988 13:22:34 GMT
Content-Lenght: 179
<html>
<head><html>Basketball Scores</title></head>
<body>
Utah Jazz vs. Chicago Bulls: 101-99<br>
Portland Trailblazers vs. Phoenix Suns: 113-88<br>
<br>
<b> GO JAZZ! </B>
</body>
</html>

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

Приложение Stock Watcher

Приложение Stock Watcher иллюстрирует много интересного в WinInet. В частности, для реализации функциональности транзакций HTTP, Stock Watcher использует MFC-классы WinInet - CInternetSession, CHttpConnection и CHttpFile. Кроме того, здесь показано, как для переопределения метода CInternetSession::OnStatusCallback, получать свой собственный класс из CInternetSession.

В Stock Watcher демонстрируется поколение мощных повторно используемых СОМ-компонентов, использующих ATL. Компоненты Stock Watcher реализуют СОМ-интерфейсы IUnknown, IConnectionPointContainer, IConnectionPoint, а также интерфейсы IQuoteProvider и IHttpRequest, определенные приложением. Кроме того, Stock Watcher иллюстрирует использование категорий СОМ-компонентов.

Приложение Stock Watcher чрезвычайно просто. Это SDI-приложение, базирующееся на MFC с единственным окном. Окно состоит из единственного CListCtrl, в котором показан портфель акций, определенный пользователем (Рис.4). Этот пример позволяет пользователю добавлять символы, обозначающие акции к своему портфелю, сохранять портфель в файл, обновлять информацию в портфеле, подключаясь к провайдеру биржевых котировок и пересылая последнюю информацию об акциях (стоимость, изменения, доступность, количество и прочее).

winine4.gif (9320 bytes)

Рисунок 4. Приложение Stock Watcher

Код и исполняемый файл Stock Watcher доступен в http://www.microsoft.com/msj. Stock Watcher был написан с помощью Visual C++ 5.0 и некоторых новых общих управляющих элементов, предусмотренных Internet Explorer 3.x в библиотеке управляющих элементов Windows. При компиляции рабочей области, все управляющие элементы зарегистрируются должным образом. Если требуется запустить исполняемый файл без предварительной компиляции проекта, для регистрации компонентов следует предварительно выполнить SW.BAT.

Если открыть Stock Watcher в Developer Studio, то можно увидеть три следующих проекта: Stock Watcher, HttpObjectServer и QuoteProviders. Stock Watcher - это MFC-приложение, сгенерированное AppWizard и обеспечивающее выполнение пользовательского интерфейса. Проекты HttpObjectServer и QuoteProviders являются ATL COM-проектами, сгенерированными AppWizard. Они обслуживают интерфейсы HttpObjectServer и QuoteProviders.

На рис.5 представлена высокоуровневая модель проектирования Stock Watcher. Когда пользователь инициирует HTTP-запрос, MFC-приложение создает экземпляры компонентов CHttpRequest и CQuoteProvider. Приложение имеет событийно-базирующуюся обратную связь между компонентом CQuoteProvider и собственным компонентом CQuoteProviderEventSink. В этом месте, приложение передает указатель интерфейса IQuoteProvider компоненту CHttpRequest, который в дальнейшем напрямую соединяется с интерфейсами. В случае, когда компоненту CHttpRequest для осуществления запроса необходима информация о провайдере котировок, он просто запрашивает интерфейс IQuoteProvider. Наконец, по завершении разбора HTTP-результата, происходит событие, соответственно информирующее приложение.

wpe7.jpg (26172 bytes)

Рисунок 5. Модель проекта Stock Watcher

При вовлечении менеджера памяти, MFC-приложению нужно только отслеживать указатели интерфейса IHttpRequest. После того, как приложение передает указатель интерфейса IQuoteProvider компоненту CHttpRequest, последний вызывает IUnknown::AddRef, который следует за вызовом MFC-приложения Unknown::Release. Таким образом, CQuoteProvider будет находиться в памяти до тех пор, пока CHttpRequest не высвободит интерфейс IQuoteProvider.

Теперь рассмотрим каждый интерфейс и компонент более подробно.

Компонент QuoteProvider

Прежде всего, проверим проект и компоненты QuoteProviders. Был создан новый проект ATL COM AppWizard, названный QuoteProviders, который был добавлен к чистому рабочему пространству Stock Watcher. В данный момент, ATL COM AppWizard содержит одно действие, позволяя определять тип СОМ-сервера, в случае, если потребуется объединить код заместителя/заглушки или обеспечить поддержку MFC. Чтобы не усложнять работу, лучше организовать QuoteProviders как СОМ-сервер (DLL) в процессе и добавить поддержку MFC.

При нажатии на Finish, мастер сгенерирует файлы для ATL-проекта без каких-либо первоначальных проектов. Мастер сгенерирует входные точки основной DLL, необходимые СОМ-серверу, включая DllCanUnloadNow, DllGetClassObject, DllRegisterServer и DllUnregisterServer. Эти функции реализуются в QuoteProviders.cpp и экспортируются в QuoteProviders.def. Кроме того, мастер генерирует QuoteProviders.idl и QuoteProviders.rgs. Файл .idl содержит описания интерфейса и компонента, а .rgs - информацию о том, как компонент будет устанавливаться в файл регистрации. Эти файлы стоит рассмотреть более пристально, поскольку они содержат весьма интересную информацию об интерфейсе. После создания ATL-проекта, посредством ATL Object Wizard, нужно будет добавить новые интерфейсы и компоненты.

MFC - Да или нет?

При использовании MFC одновременно в СОМ-сервере и его клиентском приложении, возникают более сложные ситуации. Это связано с тем, что возникают возможности динамического подключения как к обычным DLL, так и к исполняемым файлам приложений, загружающим DLL.

Проблемой такой конфигурации является способ того, каким образом MFC управляет своими глобальными данными. Глобальные данные MFC состоят из текущих указателей CWinApp и CWinThread, средств управления ресурсами и временными и постоянными картами окон. (Карты окон соотносят классы окон MFC с соответствующими им HWWD). Каждый процесс, использующий Win32 DLL получает собственную копию данных DLL. Трудности могут возникнуть с моделью AFXDLL, в которой подразумевается наличие в каждом процессе только одного CWinApp-объекта и одного набора карт обработки. Исходя из этого, можно предположить, что все из того, что было приведено, может быть связано только с самой MFC DLL.

Однако, сейчас возможно иметь в одном процессе более чем один объект CWinApp. Загрузочный файл приложения, а также каждая динамически подключаемая DLL присваивают CWinApp-объект. Таким образом, обращение к AfxGetApp в DLL возвращает указатель на CWinApp-объект DLL, вместо того, чтобы привязывать CWinApp к загрузочному файлу. Для того, чтобы обеспечить корректность статуса MFC-модуля, в этом механизме необходимо учесть возможность переключения статуса модуля при пересечении его пределов.

Такое переключение статуса модуля реализуется в макросе AFX_MANAGE_STATE. Необходимо удостовериться, что все входные точки DLL явно задают статус модуля при вызове AFX_MANAGE_STATE. Однако, диспетчеры сообщений при помощи MFC автоматически следят за безопасностью ваших действий. Поскольку в проекте QuoteProviders было решено осуществить поддержку MFC, каждая входная точка, сгенерированная мастером, должна содержать следующую строку кода:

AFX_MANAGE_STATE(AfxGetStaticModuleState( ))

После того, как завершится выполнение макроса AFX_MANAGE_STATE, предыдущий статус модуля будет переустановлен. В то время, как мастера ATL следят за большинством деталей, вам это необходимо делать вручную для каждой входной точки, которая не была сгенерирована ATL. Это особенно справедливо для точек подключения и диспетчеров обратной связи по событию. Если разработчик найдет подобное "коловращение миров" слишком сложным, то он может просто не использовать MFC в своем СОМ-сервере.

Компонент CProviderYahoo

Поскольку и интерфейс IHttpRequest, и приложение MFC зависят от интерфейса IQuoteProvider, имеет смысл проделать следующее. Можно все подробности реализации переложить на ATL ObjectWizard, после чего вручную "подтянуть" интерфейс и код под свои конкретные нужды.

Первым действием будет запуск ATL ObjectWizard, для чего следует выбрать Insert | New ATL Object. ATL ObjectWizard предоставляет множество различных типов ATL-компонентов (объекты (Objects), управляющие элементы (Controls), Смешанные типы (Miscellaneous) и компоненты доступа к данным (Data Access)). И, кроме того, для каждого типа компонентов возможно множество функциональных установок. Для данного проекта достаточно выбрать Простой Объект (Simple Object).

Мастер позволяет вам определять имена класса C++ и реализуемого в нем СОМ-интерфейса. Давайте выберем класс С++, называемый CProviderYahoo, который реализует интерфейс IProviderYahoo. В этом примере, CProviderYahoo предлагает информацию, необходимую для подключения к серверу котировок Yahoo! и для разбора файлов, возвращаемых по запросу котировок. Обратите внимание, что тип интерфейса изменен со сдвоенного на пользовательский. Как известно, если планируется использовать компонент языка сценариев, необходимо придерживаться сдвоенного интерфейса. Если компонент будет использоваться только в С++, лучше использовать пользовательские интерфейсы, обладающие большей гибкостью. Кроме того, стоит включить опцию Поддержки Точек Подключения (Support Connection Point). Это просто добавит несколько строк в файл определения классов (его мы рассмотрим позже). После нажатия на ОК, мастер начинает священнодействовать, генерируя файлы и коды, необходимые новому компоненту.

Теперь класс CProviderYahoo и интерфейс IProviderYahoo должны быть доступны для просмотра из Developer Studio. Прежде всего, взглянем на интерфейс IProviderYahoo, который был определен в QuoteProviders.idl (рис.6). Если вы не знакомы с языком описания интерфейсов, обратитесь к online-документации по Visual С++.

ATL-мастера будут продолжать изменять этот файл по мере добавления методов к интерфейсу, и даже при добавлении новых интерфейсов. Каждый раз при компиляции проекта, для выполнения QuoteProviders.idl, Visual C++ использует MIDL-компилятор. MIDL-компилятор генерирует библиотеку типов (QuoteProviders.tlb) и различные вспомогательные файлы, используемые этим и другими проектом. На рис.7 более подробно представлены файлы, сгенерированные MIDL.

Файл  

Описание

QuoteProviders.tlb  

Библиотека типов QuoteProviders

DllData.c  

Реализует DLL, содержащую код заместителя/заглушки

QuoteProviders_i.c  

Определяет GUID’ы, используемые в IDL-файле  

QuoteProviders_p.c  

Реализует код заместителя/заглушки   

QuoteProviders.h  

Файл заголовка, содержащий объявления всех интерфейсов, определенных в IDL-файле   

Рисунок 7. Файлы, сгенерированные MIDL

Теперь обратимся к файлу определения CProviderYahoo (см. рис.8). CProviderYahoo наследует от некоторых шаблонов стандартных ATL-классов, таких как CComObjectRootEx и CComCoClass. В CProviderYahoo эти шаблоны отвечают за реализацию фабрики классов и Unknown. Поскольку в ATL Object Wizard мы выбрали поддержку точек подключения, CProviderYahoo наследует и от IConnectionPointContainerImpl. Наконец, CProviderYahoo наследует и от интерфейса IProviderYahoo, что мы видели раньше. Так же, как и в случае с файлом IDL, при добавлении методов к интерфейсам и при добавлении новых интерфейсов, мастера ATL продолжают модифицировать этот файл.

Добавление методов интерфейсов

Сейчас мы можем начать добавлять методы к интерфейсу IProviderYahoo. Щелкнем правой кнопкой мыши на IProviderYahoo в окне классов и выберем Add Method (Добавить Метод). Появится диалоговое окно, представленное на рис.9.

wpe9.jpg (12961 bytes)

Рисунок 9. Диалоговое окно Add Method

Добавление метода к интерфейсу таким способом, добавит определение метода в QuoteProviders.idl, а реализацию заглушки метода CProviderYahoo - к ProviderYahoo.cpp.

Перед тем, как начать добавлять методы к интерфейсу IProviderYahoo, вернемся назад и еще раз обсудим предназначение этого модуля. Компонент CHttpRequest должен использовать компоненты провайдера котировок для получения информации о данном сервере, а также для разбора данных, полученных с того же сервера котировок. Очевидно, каждый сервер котировок имеет различные имена хостов и, вероятно, различные номера портов HTTP. Более того, различные сервера котировок будут иметь различные имена сценариев со стороны сервера и нуждаются в различных входных параметрах. И еще, поскольку мы не контролируем сценарии со стороны серверов, необходимо особенно внимательно анализировать результаты, полученные от различных провайдеров котировок акций.

Компонент IProviderYahoo должен быть достаточно интеллектуален для того, чтобы суметь сообщить компоненту IHttpRequest все, что тому необходимо знать для подключения к серверу Yahoo! и для разбора полученных с него данных. Рис.10 описывает методы, являющиеся частью интерфейса IProviderYahoo. Настоятельно рекомендуем еще раз взглянуть на ProviderYahoo.cpp для того, чтобы взглянуть, как эти методы реализуются.

Метод  

Описание

GetHost  

Предоставляет имя хоста сервера котировок Yahoo! (quote.yahoo.com)

GetPort  

Предоставляет номер порта сервера котировок Yahoo!

GetMethod  

Предоставляет HTTP-метод запроса котировок (GET, POST и т.д.)

GetURL  

Предоставляет идентификатор запроса котировок

GetHttpVersion  

Предоставляет версию HTTP, понимаемую сервером (HTTP/0.1)

GetHeaders  

Предоставляет дополнительные заголовки HTTP, используемые в запросе котировок

GetAcceptTypes  

Предоставляет дополнительные типы, допускаемые HTTP, используемые в запросе котировок

GetData  

Предоставляет корректно форматированные входные данные при запросе котировок

GetFlags  

Предоставляет дополнительные флаги WinInet, используемые при запросе котировок

InitializeData  

Клиент использует этот метод для получения доступа к символам акций

LoginIsRequired  

Определяет, нуждается ли провайдер котировок в аутентификации

ParseResult  

Разбирает HTML-ответ, возвращаемый сервером Yahoo!

Рисунок 10. Методы IProviderYahoo

Откуда взялся IQuoteProvider?

Вернитесь к рис.5 и вы увидите, что IProviderYahoo в проекте приложения нет. Более того, модель проекта показывает, что CProviderYahoo реализует интерфейс IQuoteProvider. Что же произошло? Может быть, мы как-то реализовали IQuoteProvider?

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

Однако, поскольку сгенерировать IDL-код можно мастерами ATL, то лучше сначала реализовать первый компонент с помощью мастеров, а уже после этого обобщить описание получившегося IDL-интерфейса. Поверьте, этот подход гораздо удобнее написания IDL-кода вручную. Другими словами, интерфейс IProviderYahoo, имеющийся в файле QuoteProviders.idl, нужно переименовать в IQuoteProvider. Это можно сделать, просто заменив все встречающиеся в файле QuoteProviders.idl упоминания IProviderYahoo на IQuoteProvider. При следующей компиляции проекта, QuoteProviders.h будет содержать объявления нового интерфейса IQuoteProvider.

Точки подключения

Компонент ProviderYahoo должен иметь механизм оповещения своих клиентов об окончании разбора полученных HTTP-данных. Для обслуживания этих событий используются интерфейсы IConnectionPoint и IConnectionPointContainer. Если раньше вы пытались реализовать точки подключения вручную, то уже оценили сокращенную форму команд, предлагаемую ATL.

Следует помнить, что при создании компонента ProviderYahoo была включена опция Support Connection Points. Эта опция добавляет несколько строк кода к объявлению класса ATL-объекта. Например, если взглянуть на ProviderYahoo.h (см.рис.8), то видно, что CProviderYahoo, помимо прочего, наследует и от IConnectionPointContainerImpl.

CProviderYahoo : public
	IConnectionPointContainerImpl< CProviderYahoo>.

Кроме того, для IConnectionPointContainer к СОМ-карте CProviderYahoo мастер добавляет COM_INTERFACE_ENTRY_IMPL.

BEGIN_COM_MAP(CProviderYahoo)
   COM_INTERFACE_ENTRY(IQuoteProvider)
   COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()

И, наконец, мастер добавляет карту точек подключения без каких-либо первоначальных входных данных.

BEGIN_CONNECTION_POINT_MAP(CProviderYahoo)
END_CONNECTION_POINT_MAP()

Этот сгенерированный код полностью реализует интерфейс IConnectionPointContainer. Теперь нужно реализовать собственный интерфейс точек подключения.

IQuoteProviderEvent

Точка подключения, которая была реализована для CProviderYahoo, называется IQuoteProviderEvent. IQuoteProviderEvent состоит из одного метода, называемого UpdateSymbol, который возвращает клиенту информацию, собранную в Интернет. Для того, чтобы реализовать точку подключения с использованием ATL, прежде всего, необходимо описать интерфейс в IQuoteProviders.idl. Для этого нужно сгенерировать с помощью guidgen.exe, который находится в каталоге \Devstudio\VC\bin, новый GUID. После этого можно будет добавить новый интерфейс к разделу интерфейсов в IDL-файле. Покажем, как выглядит IQuoteProviderEvent:

[
uuid(BFD86BC0-A004-11d1-9912-004033D06B6E),
helpstring("IQuoteProviderEvent Interface"),
pointer_default(unique)
]
interface IQuoteProvidersEvent : IUnknown
{
import "oaidl.idl";
HRESULT UpdateSymbol (   LPCTSTR lpszSymbol,
            LPCTSTR lpszPrice,
            LPCTSTR lpszChange,
            LPCTSTR lpszOpen,
            LPCTSTR lpszVolume);
};

Теперь, для генерации новой библиотеки типов, содержащей информацию об интерфейсе IQuoteProviderEvent, нужно откомпилировать проект QuoteProviders еще раз. После того, как это будет сделано, можно использовать компонент ATL Proxy Generator.

ATL Proxy Generator можно вызвать через Components and Controls Gallery (Project | Add To Project | Components and Controls).После двойного щелчка на каталоге Developer Studio Components, выберите компонент ATL Proxy Generator и нажмите на Insert. После подтверждения появится диалоговое окно ATL Proxy Generator. Далее следует ввести путь к библиотеке типов QuoteProvider (QuoteProvider.tlb); слева появятся интерфейсы QuoteProvider и QuoteProviderEvent. Наконец, выберем интерфейс QuoteProviderEvent и перед нажатием на Insert удостоверимся, что в Proxy Type установлено значение Connection Point.

ATL Proxy Generator создает новый файл с именем CPQuoteProviders.h (см.рис.8). В нем содержится объявление CProxyIQuoteProviderEvent, который реализует интерфейс IConnectionPoint и способен вызвать UpdateSymbol всех клиентов этой точки подключения посредством метода Fire_UpdateSymbol.

Для того, чтобы добавить эту функциональность к компоненту CProviderYahoo, CProviderYahoo должен унаследовать от шаблона CProxyIQuoteProviderEvent, для чего в список базовых классов CProviderYahoo следует добавить:

public CProxyIQuoteProviderEvent<CProviderYahoo>

Наконец, к карте точек подключения CProviderYahoo был добавлен IQuoteProviderEvent:

BEGIN_CONNECTION_POINT_MAP(CProviderYahoo)
   CONNECTION_POINT_ENTRY(IID_IQuoteProviderEvent)
END_CONNECTION_POINT_MAP( )

На рис.8 показано, как выглядит определение класса CProviderYahoo с добавленной поддержкой точек подключения и методами. Такой подход, при котором функциональность точек подключения добавляется к ATL COM-компонентам, значительно уменьшает объем кода, который необходимо написать.

Категория компонентов Quote Provider

Цель этого проекта - возможность добавления дополнительных компонентов IQuoteProvider во время работы. Приложение Stock Watcher должно уметь определять эти новые компоненты и использовать их.

Простым способом определения компонентов приложением Stock Watcher является использования категорий СОМ-компонентов. Категория компонентов - это простое соглашение о регистрации, сообщающее вам, реализует ли компонент определенные интерфейсы, необходимые категории компонентов.

Для того, чтобы использовать все преимущества категории компонентов, следует использовать системный компонент Component Category Manager. В нем реализуется два интерфейса: ICatRegister и ICatInformation. Как и подразумевается их именами, эти интерфейсы используются для регистрации категорий компонентов, компонентов (вместе с их категориями) и для восстановления информации о категория компонентов.

Чтобы создать категорию компонентов Quote Provider, был сгенерирован новый GUID, который получил имя CATID_QuoteProviders:

// {32014981-9CD4-11d1-9912-004033D06B6E}
DEFINE_GUID(CATID_QuoteProviders,
      0x32014981, 0x9cd4, 0x11d1, 0x99,
      0x12, 0x0, 0x40, 0x33, 0xd0, 0x6b, 0x6e);

После этого, в категорию компонентов был добавлен ряд вспомогательных функций, которые упрощают создание категорий компонентов и регистрации/отмены регистрации компонентов (вместе с категориями) (см.рис.8). Чтобы зарегистрировать категорию компонентов QuoteProvider и компонент CProviderYahoo, в DllRegisterServer были добавлены строки кода:

hr = CreateComponentCategory(
   CATID_QuoteProviders,
   L"Stock Watcher Quote Providers");
if (FAILED(hr))
   return hr;
hr =
   RegisterCLSIDInCategory(CLSID_ProviderYahoo,
            CATID_QuoteProviders);
if (FAILED(hr))
   return hr;

Чтобы отменить регистрацию компонента CProviderYahoo, в DllUnregisterServer были добавлены следующие строки:

hr =
   UnRegisterCLSIDInCategory(CLSID_ProviderYahoo,
            CATID_QuoteProviders);
if (FAILED(hr))
   return hr;

Для запроса у категории компонентов CATID_QuoteProviders информации о всех доступных компонентах Quote Provider, приложение Stock Watcher будет использовать менеджера компонентной категории (Component Category Manager). После установки Stock Watcher, к пользовательской системе можно добавлять дополнительные компоненты провайдерова котировок, просто регистрируя их с помощью категории компонентов CATID_QuoteProviders.

Компонент HTTP

Компонент HTTP ответственен только за функциональность HTTP-транзакций. Прежде всего, функциональность HTTP-транзакций состоит из передачи и приема данных. Чтобы HTTP-компонент мог быть абстрактным и повторно используемым, в интерфейс IQuoteProvider должна быть встроена логика запросов HTTP.

Для HTTP-компонента был создан отдельный проект, названный HttpObjectServer и содержащий отдельный СОМ-сервер для компонента CHttpRequest. Компонент CHttpRequest реализует интерфейс IHttpRequest. ATL-проект и объекты были созданы так же, как и в случае с проектом QuoteProviders.

Интерфейс IHttpRequest определен в HttpObjectServer.idl, а определение класса находится в HttpRequest.h (см.рис.11). IHttpRequest содержит всего два метода - GetProviderInterface и ProcessRequest. GetProviderInterface возвращает интерфейс IQuoteProvider, который используется компонентом в данный момент. ProcessRequest инкапсулирует процесс HTTP-транзакции в целом. Приведем реализацию ProcessRequest:

STDMETHODIMP CHttpRequest::ProcessRequest(
            IUnknown* pQuoteProvider,
            long lMainHwnd)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState())
   m_pQuoteProvider =
    static_cast<IQuoteProvider*>(pQuoteProvider);
   m_pQuoteProvider ->AddRef();
   m_hwnd_Main = reinterpret_cast<HWND>(lMainHwnd);
   AfxBeginThread(HttpWorkerThread, this);
   return S_OK;
}

После сохранения указателя IQuoteProvider в m_pQuoteProvider, ProcessRequest должен вызвать указатель интерфейса Unknown::AddRefon. ProcessRequest инициирует начало работы рабочего потока - HttpWorkerThread, на котором с этого момента лежит забота о работе WinInet. Поскольку MFC-объекты не являются поточно-защищенными, вызовы, манипулирующие MFC-объектами основного приложения , должны синхронизироваться оконными сообщениями.

HttpWorkerThread

Первым действием рабочего потока является создание объекта CInternetSession. Для переопределения метода OnStatusCallback, был объявлен экземпляр CMyInternetSession, который происходит от CInternetSession (см.рис.11). После этого, для включения метода обратного вызова статуса, следует вызвать CInternetSession::EnableStatusCallback. При изменении статуса CInternetSession, WinInet вызывает метод OnStatusCallback.

Не углубляясь в детали, HttpWorkerThread вызывает следующие методы WinInet:

CInternetSession::GetHttpConnection
   // создает CHttpConnection
CHttpConnection::OpenRequest // создает CHttpFile
CHttpFile::SendRequest // передает данные
CHttpFile::Read // читает ответ Http

HttpWorkerThread, вызывая соответствующие методы IQuoteProvider, собирает всю информацию, которая необходима ему для осуществления этих вызовов WinInet. Например, CInternetSession::GetHttpConnection нуждается в имени хоста и номере порта. Тогда, для получения информации о провайдере перед вызовом этого метода, HttpWorkerThread вызывает IQuoteProvider::GetHost и IQuoteProvider::GetPort. Более того, после того, как HttpWorkerThread завершит прием HTTP-ответа, он позволяет каждому QuoteProvider при помощи IQuoteProvider::ParseResult, обработать эти ответы отдельно.

Объекта CHttpRequest достаточно для поддержания всех типов Интернет-провайдеров котировок акций. При реализации и использовании дополнительных компонентов IQuoteProvider, компонент CHttpRequest способен обмениваться данными с этими новыми провайдерами, не меняя ни одной строки кода.

Детали реализации Stock Watcher

Недостаток места не позволяет нам рассмотреть все детали приложения Stock Watcher, но, тем не менее, хотелось бы кратко обсудить некоторые моменты - функциональность Refresh All, CQuoteProviderDlg и CQuoteProviderEventSink.

На рис.12 показано использование провайдера CQuoteProviderDlg. Сейчас реализован и зарегистрирован только один провайдер котировок (CProviderYahoo). Однако, CQuoteProviderDlg перечисляет все компоненты, которые относятся к категории компонентов CATID_QuoteProviders и позволяют пользователю выбирать в поле со списком провайдера котировок один из них.

wpeB.jpg (9153 bytes)

Рисунок 12. Провайдеры котировок

После выбора пользователем провайдера акций и нажатия на ОК, CQuoteProviderDlg сохраняет CLSID компонента в регистрационном ключе ProviderCLSID, относящемся к приложению. Начиная с этого момента, приложение использует выбранный компонент провайдера котировок.

Чтобы Stock Watcher получал события, инициированные компонентами IQuoteProvider, необходимо ввести в схему взаимодействия объект обратной связи (sink) и добиться соединения с точкой подключения IQuoteProvider. Назовем такой объект обратной связи, используемый приложением, CQuoteProviderEventSink. Объявление класса CQuoteProviderEventSink выглядит следующим образом:

class CQuoteProviderEventSink :
   public CComObjectRoot,
   public IQuoteProviderEvent
{
public:
   CStockWatcherDoc* m_pDoc;
   CQuoteProviderEventSink() {}

BEGIN_COM_MAP(CQuoteProviderEventSink)
   COM_INTERFACE_ENTRY(IQuoteProviderEvent)
END_COM_MAP()
// Методы IHttpEvent
   STDMETHOD(UpdateSymbol)(LPCTSTR lpszSymbol,
               LPCTSTR lpszPrice,
               LPCTSTR lpszChange,
               LPCTSTR lpszOpen,
               LPCTSTR lpszVolume);
};

CStockWatcherDoc создает экземпляр этого объекта (см.рис.13). После этого, при каждом создании компонента IHttpRequest, для получения точки подключения IQuoteProvider, он вызывает AtlAdvise. Когда CProviderYahoo::ParseResult вызывает Fire_UpdateSymbol, вызывается метод CQuoteProviderEventSink::UpdateSymbol.

Выбором Refresh All из меню Symbol, приложение Stock Watcher позволяет пользователю решать, когда информацию о котировках акций следует обновлять. Программа обработки CStockWatcherDoc::OnRefreshAllSymbols создает экземпляр выбранного в данный момент провайдера котировок вместе с компонентом IHttpRequest. Затем, используя метод IQuoteProvider::InitializeData, она передает информацию о символе акции компоненту провайдера котировок. Наконец, после создания точки подключения с IQuoteProvider, она вызывает IHttpRequest::ProcessRequest (см.рис.13), который инкапсулирует всю функциональность HTTP-транзакций.

Заключение

Мы увидели, как использовать СОМ для создания повторно используемых компонентов WinInet. WinInet является высокоуровневым интерфейсом, инкапсулирующим большую часть кода протокола Интернет, но почему бы с помощью СОМ не снабдить свой код большей возможностью повторного использования? Более того, имеет смысл еще больше усилить эту мощную архитектуру, благодаря простым в использовании мастерам и шаблонам ATL.

Приложение Stock Watcher показывает, как простой интерфейс IHttpRequest может инкапсулировать всю функциональность HTTP-транзакций. Он также демонстрирует, как во время работы добавлять к пользовательской системе новые компоненты провайдера котировок (IQuoteProvider).

Мы рассмотрели только один компонент провайдера котировок - CProviderYahoo, использующий сервер финансовой биржи Yahoo!. Пример приложения имеет два других компонента провайдера: CProviderFastQuote и CProviderDatek, которые работают с серверами бирж Quote.com и Datek (см.рис.12). Серверу Datek необходима аутентификация пользователя. Эти два компонента демонстрируют, насколько легко можно реализовать дополнительные компоненты провайдера.

Надеемся, что мы помогли вам прочувствовать мощность СОМ/ATL и понять, как хорошо они стыкуются с моделью проектирования WinInet. Наращиваемой архитектурой СОМ имеет смысл пользоваться, поскольку Интернет эволюционирует быстро. СОМ позволяет в случае необходимости заменять отдельный компонент, а не все приложение в целом. Между прочим, если какой-нибудь из провайдеров котировок, использованных в приложении, изменит свои HTTP-форматы перед выходом настоящей статьи в свет, вы можете попробовать модифицировать соответствующие подпрограммы ParseResult сами. Пользуйтесь!


Copyright © 1994-2016 ООО "К-Пресс"