Queued-компоненты Windows 2000

Автор: А. Новик
Источник: «Технология Клиент-Сервер»

QC и MSMQ
MSMQ
QC
Преимущества и недостатки
Пример распределенного приложения
Порядок работы приложения
Сервер MessSrv App.
Как получить доступ к QC-компоненту
Отправитель сообщений MessSender App (визуальный)
Приложение QCClient App.
Компонент EventHelper
Разработка QC интерфейса
Приемник сообщений MessReceiver App. (визуальный)
Установка приложения
Конфигурация MSMQ
Тестирование распределенного приложения
Использование транзакций
Требования безопасности
Заключение

Код к статье - 126 KB

Разработчикам распределенных приложений иногда необходимо создавать программы, работающие в асинхронном режиме.

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

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

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

В обычных, не рассчитанных на сетевую работу, приложениях, эта технология распространена довольно широко. Например, ActiveX-компоненты для обратных уведомлений используют механизм событий (Events). Механизм событий основан на синхронных COM-вызовах. Для распределенных (сетевых) приложений такой способ непригоден, так как при синхронной связи сервер попадает в зависимость от всех клиентских приложений. Это, по-видимому, явилось одной из причин отсутствия, по большому счету, технологии уведомлений в широко известных серверах баз данных. Даже если сервер БД поддерживает эту функциональность, такое решение проблемы идет вразрез с концепцией многоуровневых (распределенных) приложений, где подобные операции должны производиться на уровне бизнес-логики (БЛ). Асинхронное взаимодействие способно эффективно решить эту проблему, обеспечивая “развязку” сервера и клиента.

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

Одной из наиболее часто встречающихся проблем является временная недоступность сервера по различным причинам (высокая загрузка, неработоспособность, разрыв сетевого соединения). Независимость взаимодействующих приложений важна не только в этих случаях. Все большее количество продаваемых компьютеров – мобильные. Пользователь хочет взять свой компьютер в поездку, поработать на нем и, впоследствии подключившись к серверу, обработать информацию, сформированную за время его автономной работы. Многие уже имеют возможность работать так с e-mail сообщениями. Пользователь создает исходящие сообщения при автономной работе, отправляет их и принимает входящие после подключения компьютера к телефонной линии.

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

Иногда у пользователя существует необходимость накапливать информацию для последующей пакетной отправки в едином запросе. Предположим, что сервер обрабатывает каждый новый запрос, создавая новый объект. Он должен обрабатывать все входящие вызовы с приемлемой для пользователя скоростью. Программа пользователя вызывает методы объекта в условиях, когда множество других объектов обрабатывается параллельно (если не последовательно), при этом активно используется память сервера и другие ресурсы. В случае, когда клиентское приложение отправляет все необходимые данные в одном запросе, объект создается только на время обработки данных. Такая инфраструктура известна как Store-and-Forward Mechanism (Механизм с промежуточным накоплением). Разработчик, конечно же, может сам создать подобный механизм для своих приложений. Но создание собственной инфраструктуры с промежуточным накоплением привело бы к дополнительным затратам времени и более высокой стоимости разработки. Если такой механизм реализован в рамках операционной системы, можно предположить, что он будет более надежен, эффективен и универсален.

QC и MSMQ

В Windows существует два средства, с помощью которых можно реализовать асинхронное взаимодействие в распределенных информационных системах - MS Message Queue Server (MSMQ) и Queued Components (QC).

MSMQ – сервер очередей сообщений, который доступен на сегодняшний день для Windows 9x – NT4 в составе Option Pack. (Об аналогичном продукте IBM MQSeries писалось в предыдущем номере нашего журнала). Option Pack - дополнительный пакет приложений, который можно приобрести за $30 у представителей фирмы Microsoft или бесплатно скачать с www.microsoft.com (всего 70 Mb) ;-). Он впервые появился в декабре 1997 года. В поставку Windows 2000 будет входить преемник MTS (Microsoft Transaction Server) – Component Services (COM+), который поддерживает так называемые Queued Component-ы (компоненты, реализующие асинхронные вызовы методов). На момент написания этой статьи Windows 2000 была доступна в версии Beta 3, с которой мы и экспериментировали.

Рассмотрим эти средства по отдельности:

MSMQ

MSMQ включает в себя большой набор возможностей, позволяющих приложениям, расположенным на различных машинах, посылать и принимать асинхронные сообщения, тем самым обеспечивая базовый сервис доставки сообщений (basic message delivery service). Приложение, которое должно принимать сообщения, создает очередь – сохраняемую административную структуру, доступную операционной системе и являющуюся, по сути дела, почтовым ящиком. Приложение, которое должно посылать сообщения, размещает эту очередь через системные сервисы или собственными силами, и использует MSMQ для посылки сообщений. MSMQ-сообщение может содержать плоский текст, объекты, поддерживающие интерфейс IPersistStream или BLOB. Если компьютер получателя с очередью не доступен на момент посылки сообщения, сообщение буферизируется операционной системой передающей машины, либо другими сетевыми машинами до момента доступности получателя. Приложение получателя может принимать сообщения MSMQ путем опроса очереди сообщений или по уведомлениям обратного вызова (callback notifications).

MSMQ гарантирует, что все передаваемые сообщения в конечном счете достигнут адресата. Кроме того, эта технология обеспечивает: эффективную маршрутизацию, защиту и нужную приоритетность.

MSMQ обеспечивает функциональность, необходимую для управления посылаемыми событиями. Например, отправитель может определить, что сообщение будет удалено, если не достигнет очереди приемника в заранее определенный интервал времени, либо не будет прочитано принимающим приложением в пределах другого интервала времени. Отправитель может потребовать уведомления о приеме сообщения или удалении по причине истечения интервала времени. Подробное описание возможностей MSMQ можно прочитать в книге "Microsoft Message Queue is a Fast, Efficient Choice for Your Distributed Application" by David Chappell (MSJ, July 1998).

Использование очередей сообщений существенно отличается от RPC (удаленного вызова процедур), Windows Sockets и MAPI (messaging API - e-mail-ориентированный сервис) тем, что MSMQ реализует неориентированный на подключение способ передачи сообщений, когда взаимодействующие приложения не обязаны работать одновременно и/или ориентироваться на соединение. RPC-приложения ориентированы на соединение, Windows Sockets работают только с одновременно работающими приложениями. А MAPI, хотя и отвечает основным требованиям систем с промежуточным накоплением, все же уступает MSMQ в универсальности и ориентируется только на электронную почту.

QC

QC-компоненты являются модификацией COM+/MTS-компонентов и обеспечивают более высокий уровень абстракции, чем MSMQ. Queued Components отвечают требованиям механизма Store-and-Forward. Клиентское приложение может использовать QC-компоненты, так же, как любой другой COM-объект. Они основаны на COM (Component Object Modeling), но в качестве транспортного протокола вместо RPC используют MSMQ-сервис. Классический DCOM использует синхронный RPC для передачи информации от одного компьютера другому посредством заместителей (proxy) и заглушек (stub), как показано на Рис. 1.


Рис. 1 COM использует RPC

QC взаимодействует между клиентским proxy и stub сервера с помощью MSMQ, как показано на Рис. 2.

Во время активизации QC-объекта, клиент подключается не к COM-объекту, а к Сall Recorder. Клиентское приложение производит вызовы как обычно, но они не отправляются немедленно через RPC. Вместо этого информация о них записывается в очередь MSMQ. Когда клиент деактивирует компонент (вызывает Release()), QC использует MSMQ для асинхронной посылки последовательности вызовов серверу, на котором создается реальный компонент. Сервер читает сообщения из MSMQ-очереди, содержащей эту последовательность, активизирует компонент Player (исполнитель) и передает ему информацию о вызовах. Player создает реальный объект и возпроизводит на нем вызовы.


Рис. 2 QC использует MSMQ

Преимущества и недостатки

Достоинствами MSMQ являются:

  1. MSMQ доступен для разработчиков уже в текущей версии Windows.
  2. Сервисы MSMQ можно использовать напрямую из приложения без дополнительного создания и регистрации специальных компонентов в Component Services (бывшем MTS), как того требует QC-модель.
  3. Возможность организации полнофункциональной работы в off-line режиме.
  4. Возможность интерактивного взаимодействия приложений за счет существенно более высокой скорости и меньших накладных расходов при передаче сообщений.

Достоинствами QC являются:

  1. QC является более высокоуровневой технологией. С его помощью проще программировать, чем при прямом использовании MSMQ. Не нужно учитывать мелкие технологические детали.
  2. QC не вынуждает разработчика COM-объектов переходить к другой модели программирования. Многие разработчики уже используют COM и основанные на нем компоненты в своих приложениях, например, ActiveX Data Objects (ADO) для подключения к базам данных. С использованием QC, они не должны будут изменять своим правилам программирования.
  3. Создаваемые разработчиком COM-компоненты могут быть использованы асинхронно как через QC сервис, так и синхронно, через DCOM.

Пример распределенного приложения

Чтобы опробовать возможности различных способов асинхронного взаимодействия рассмотрим пример распределенного приложения, реализующего одновременно разные технологии - QC, DCOM и MSMQ.

Задачей этого приложения является передача текстовых сообщений всем участникам, подключившимся к серверу. Эта программа имеет сходство с chat’ами, распространенными сейчас очень широко. На Рис. 5 показана структурная схема приложения. Разные способы связи между приложениями мы использовали для того, чтобы можно было оценить преимущества и недостатки этих технологий. Распределенное приложение состоит из четырех частей:


Рис. 3 Структурная схема примера распределенного приложения

Здесь трудно сказать, какое из приложений является клиентом, а какое сервером. Все программы, кроме передатчика, в какой-то мере являются серверами. Сервером мы решили назвать программу, поддерживающую список имен машин для рассылки сообщений. Клиентом – QCClient App, принимающее сообщения. Так мы поступили, в основном, из-за предусматриваемой цели оценки влияния на сервер удаленных клиентских приложений, о чем говорилось ранее.

Порядок работы приложения

Визуальные приемники сообщений (Message Receiver App) (смотри Рис. 7) после загрузки создают (каждый на своей машине) по одной копии компонента, рассылающего сообщения (EventHelper), который находится в QCClient App, и подключаются к его событиям (SetText(…)). Рассылка этих событий инициируется компонентом QCReceiver посредством метода Fire(…). Компонент QCReceiver создается объектом MessServObj сервера MessSrv App только в QC- или DCOM-режимах. В MSMQ-режиме взаимодействие MessServObj и EventHelper производится через очередь MSMQ (смотри Рис. 5).


Рис. 4 Взаимодействие компонентов в QC и RPC режимах


Рис. 5 Взаимодействие компонентов в MSMQ режиме

Отдельное визуальное приложение приемника создано из-за невозможности использования GUI в объектах MTS. Можно было избежать создания промежуточного компонента (EventHelper-а), работая с MSMQ напрямую, но это невыгодно. При этом пришлось бы дублировать сетевые сообщения для каждого клиентского приложения, что негативно скажется на сетевом трафике. В нашем примере приложений приемника можно загрузить на компьютере клиента сколь угодно много - и это не скажется на загрузке сети. Их работа обеспечивается событиями компонента EventHelper, поддерживающим интерфейс IConnectionPointContainer. Для нашего примера использовать несколько приемников на одном компьютере не имеет смысла, достаточно одного, но при создании других приложений такая функциональность может быть необходима. Реализуя интерфейс IConnectionPointContainer, мы автоматически создаем эту возможность.

Компонент EventHelper, во время инициализации должен зарегистрировать имя своего компьютера на сервере, например, вызвать метод регистрации у серверного компонента (MessServObj), или послать сообщение в специализированную очередь MSMQ, чтобы сервер знал, кому рассылать сообщения. При деинициализации требуется произвести обратные действия. Такая операция отнимает много времени программиста, но для реализации примера нам не интересна. Поэтому мы решили исключить регистрацию из примера и воспользоваться статическим массивом имен машин-клиентов. Если вы захотите, можете сами реализовать регистрацию и отрегистрацию.

Порядок работы приложений со стороны отправителя сообщений довольно прост. Приложение Message Sender App подключается к компоненту MessServObj и, вызывая его метод MessServObj::Send(…), заставляет отправлять сообщения в режимах QC и RPC посредством метода QCReceiver::Send(…), а в режиме MSMQ записывать сообщения в MSMQ-очередь. Помимо информации о режиме передачи сообщений серверному компоненту через параметры его метода MessServObj::Send(…) передается признак необходимости уничтожения объекта QCReceiver после каждого вызова метода QCReceiver::Send(…). Это имеет реальный смысл только для QC-режима, как будет объяснено далее.

Сервер MessSrv App.

Вначале рассмотрим порядок создания приложения MessSrv App. Для этого в VC++ создадим ATL-проект под названием MessSrv. На этапе выбора типа приложения отметим Dynamic Link Library (DLL) с поддержкой MTS.

Далее создадим ATL компонент MessServObj с помощью ATL Object Wizard, вызвав его через пункт меню Insert/New ATL Object... В окне ATL Object Wizard выбираем Simple Object, вводим имя компонента MessServObj, на закладке Attributes принимаем значения по умолчанию.

Теперь создадим метод Send компонента MessServObj (смотри Рис. 4). Чтобы добавить его в компонент с помощью правой кнопки мыши в окне просмотра классов выделим интерфейс IMessServObj и вызовем контекстное меню. В меню выберем пункт Add Method… Заполним поля диалога следующими параметрами: Method Name – Send, Parameters - [in] BSTR Text, [in] SendMode Mode, [in] VARIANT_BOOL AutoPlay. Первый параметр – текст сообщения. Второй – определяет, каким способом мы хотим разослать сообщения: QC, DCOM или MSMQ. Третий параметр не используется в режиме MSMQ. В остальных режимах он указывает серверу, что компонент необходимо уничтожить (вызвать Release()) в конце каждого вызова. Это необходимо потому, что QC-компоненты осуществляют передачу и воспроизведение вызовов только после уничтожения. Для DCOM-компонентов это сделано чисто для сравнения производительности.

Определим режимы (SendMode) через enum в MessSrv.idl-файле. Определяя его в idl-файле, мы можем быть уверены, что его значения будут доступны не только в серверном приложении, но и в визуальном передатчике сообщений.

//Режимы передачи сообщений
typedef enum SendMode {
SEND_QC_USE = 1,//Передача в режиме QC
SEND_MSMQ_USE, //Передача в MSMQ режиме
SEND_DCOM_USE //Передача в режиме DCOM
} SendMode;

Нажмите OK для завершения создания метода. Подобным образом добавляем методы Play() – предназначенный для отправки записанных сообщений в QC режиме и MSMQTestInit() - для инициализации MSMQ режима. Как используются эти методы, можно будет увидеть в тексте программы визуального передатчика.

Программирование для MSMQ возможно двумя способами: с помощью MSMQ C API и с помощью ActiveX-компонентов. Мы воспользовались ActiveX-компонентами. Они показались нам проще в реализации, кроме того, они могут быть использованы из Visual Basic.

Воспользуемся ключевым словом #import для получения файлов описания компонентов и интерфейсов, входящих в состав библиотеки MQOA.DLL, которая поставляет ActiveX-компоненты доступа к MSMQ (эта библиотека входит в поставку MSMQ). Описание компонентов и интерфейсов после компиляции становится доступно в файлах MQOA.tlh и MQOA.tli. В них находятся не просто описания интерфейсов, а Wrapper-ы и SmartPointer-ы, так что будьте внимательны при работе с ними. Все компоненты и интерфейсы с префиксом MSMQ мы получили из этих файлов.

// MessServObj.cpp : Implementation of CMessServObj
#include "stdafx.h"
#include "MessSrv.h"
#include "MessServObj.h"
#include "..\QCClient\QCClient_i.c"

/*
Автоматическое создание описания MSMQ объектов в файле MQOA.tlh
no_implementation - предотвращает создание Wrapper классов и Smart-указателей для интерфейсов MSMQ,
   мы будем использовать напрямую методы интерфейсов и CComPtr - указатели
raw_interfaces_only - заставляет VC++ создавать описания только самих MSMQ интерфейсов
   без описания Wrapper классов
raw_native_types - предотвращает использование вспомогательных типов/классов (_bstr_t, _variant_t) 
   в описании параметров методов интерфейсов. Там где нам будет удобно будем 
   использовать CComBSTR и CComVariant
no_namespace - создает описание интерфейсов без использования пространства имен MSMQ
*/
#import "D:\W2Kb3\System32\MQOA.DLL" no_implementation raw_interfaces_only raw_native_types no_namespace

/////////////////////////////////////////////////////////////////////////////
// CMessServObj

//Глобальный smart - указатель на объект - информации об очередях
CComPtr <IMSMQQueueInfo> g_pQueueInfo;

/*   
Массив имен машин клиентов, должен быть динамическим, но у нас статический 
незабудьте подставить в него имена ваших компьютеров
*/
LPCTSTR g_CompNameArray[] = {_T("PC1"), _T("PC2"), _T("PC3")};

//Определяем размер массива
const int CompArrSize = sizeof (g_CompNameArray)/sizeof (g_CompNameArray[0]);

/*
Глобальный массив smart-указателей на объекты 
клиентов приема сообщений, используется в QC и DCOM режимах
*/
CComPtr <IQCReceiver> g_PtrArr[CompArrSize];

/*
Глобальный массив smart-указателей на объекты 
MSMQ очередей используется в MSMQ режиме
*/
CComPtr <IMSMQQueue> g_pQueueArr[CompArrSize];

CMessServObj::CMessServObj() {}

/*
Meтод Send - передача сообщений в различных режимах  
BSTR Text - текст сообщения  
SendMode Mode - режим передачи (SEND_QC_USE, SEND_MSMQ_USE, SEND_DCOM_USE) 
VARIANT_BOOL AutoPlay==True то:
   в QC-режиме происходит немедленная рассылка сообщений
   в DCOM-режиме происходит синхронная рассылка сообщений
   после чего DCOM-компонент уничтожается (чисто для
   сравнения производительности)
Если AutoPlay==False то:
в QC-режиме происходит накопление сообщений в CALL RECORDER-е
   в DCOM-режиме происходит синхронная рассылка сообщений,
   а DCOM-компонент не уничтожается
в MSMQ-режиме параметр AutoPlay игнорируется, а сообщение
   всегда посылается в в очередь.
*/
STDMETHODIMP CMessServObj::Send (BSTR Text, SendMode Mode, VARIANT_BOOL AutoPlay)
{
   for (int i = 0; i < CompArrSize; i++) 
   {
      HRESULT hr = S_OK;
      if (g_PtrArr[i] == NULL) {
         switch (Mode)
         {
         case SEND_QC_USE: //Режим QC
            {   
               //Структуру BIND_OPTS заполняем значениями, принятыми по умолчанию
               BIND_OPTS BO;
               BO.cbStruct = sizeof (BIND_OPTS);
               BO.dwTickCountDeadline = 0;
               BO.grfFlags = STGM_READWRITE;
               BO.grfMode = 0;
               
               //Строка моникера
               TCHAR BindString[256];
               wsprintf(BindString, _T("queue:ComputerName=%s/new:QCClient.QCReceiver.1"), g_CompNameArray[i]);
               USES_CONVERSION;
               //Получение QC объекта
               hr = CoGetObject(T2W(BindString), &BO, IID_IQCReceiver, (LPVOID*)&g_PtrArr[i]);
            }
            break;
         case SEND_DCOM_USE: //Режим DCOM (RPC)
            {   
               //Задаем IID интерфейса объекта клиента
               MULTI_QI QI;
               memset (&QI, 0, sizeof (MULTI_QI));
               QI.pIID = &IID_IQCReceiver;
               
               //Задаем имя компьютера клиента               
               COSERVERINFO ServInfo;
               memset (&ServInfo, 0, sizeof (COSERVERINFO));
               USES_CONVERSION;
               ServInfo.pwszName = T2W(g_CompNameArray[i]);
               
               //Получение DCOM объекта
               hr = CoCreateInstanceEx (CLSID_QCReceiver, NULL, CLSCTX_SERVER, &ServInfo, 1, &QI);
               
               if (SUCCEEDED (QI.hr))
                  g_PtrArr[i].p = (IQCReceiver*)QI.pItf;
               else
                  hr = QI.hr;
            }
            break;
         case SEND_MSMQ_USE: //Режим MSMQ
            {   
               //Smart - указатель MSMQ сообщения
               CComPtr <IMSMQMessage> g_pMessage;
               
               //Получаем экземпляр сообщения
               hr = g_pMessage.CoCreateInstance (__uuidof(MSMQMessage));
               
               if (SUCCEEDED (hr)) {
                  //Вставляем текст сообщения
                  hr = g_pMessage->put_Body (CComVariant (Text));
                  
                  //Отправка сообщения
                  if (SUCCEEDED (hr))
                     hr = g_pMessage->Send (g_pQueueArr[i]);
               }
            }
            break;
         default:
            hr = E_INVALIDARG;
         }
      }
      if (FAILED (hr))
         continue;
      //В режиме MSMQ режиме передача сообщений уже произведена
      if (Mode == SEND_MSMQ_USE) 
         continue;
      
      //В QC режиме производится запись в CALL RECORDER,
      //в DCOM режиме непосредственная передача
      hr = g_PtrArr[i]->Send (Text);
      if (FAILED (hr))
         continue;
      
      /*В QC режиме отправка накопленных сообщений из 
      CALL RECORDER'а QC компоненту,
      В DCOM режиме - просто уничтожение компонентов
      */
      if (AutoPlay)
         g_PtrArr[i].Release ();
   }
   return S_OK;
}

//Метод Play - отправляет накопленные сообщения
//из CALL RECORDER'а всем клиентам в QC режиме,\
//в режиме DCOM - просто уничтожает объекты
STDMETHODIMP CMessServObj::Play()
{
   for (int i = 0; i < CompArrSize; i++)
      g_PtrArr[i].Release ();
   
   return S_OK;
}

//Метод MSMQTestInit - создает и открывает очереди сообщений
STDMETHODIMP CMessServObj::MSMQTestInit() 
{
   //Создание экземпляра объекта информации об очередях
   //с помощью него можно создавать и открывать очереди,
   //нам нужен лишь один экземпляр, поэтому проверяем, создан ли он
   if (g_pQueueInfo == NULL) 
      g_pQueueInfo.CoCreateInstance (__uuidof(MSMQQueueInfo));
   HRESULT hr = S_OK;
   USES_CONVERSION;
   for (int i = 0; i < CompArrSize; i++)
   {
      //Нам нужен лишь один экземпляр объекта - очереди сообщений,
      //поэтому проверяем, создан ли он
      if (g_pQueueArr[i] == NULL) {
         //Задаем имя очереди, Snt - имя сервера домена,
         //MQTest_ - произвольный префикс,
         TCHAR QueuePathName[256];
         wsprintf(QueuePathName, _T("snt\\MQTest_%s"), g_CompNameArray[i]);
         hr = g_pQueueInfo->put_PathName(CComBSTR(T2OLE(QueuePathName)));      
         //Открытие очереди
         hr = g_pQueueInfo->Open (MQ_SEND_ACCESS, MQ_DENY_NONE, &g_pQueueArr[i]);
         if (FAILED (hr)) {
            //Возможно очередь отсутствует, попробуем ее создать
            hr = g_pQueueInfo->Create(&CComVariant (MQ_TRANSACTIONAL_NONE), &CComVariant (MQ_DENY_NONE));
            //Открытие очереди
            if (SUCCEEDED (hr))
               hr = g_pQueueInfo->Open (MQ_SEND_ACCESS, MQ_DENY_NONE, &g_pQueueArr[i]);
         }
      }
      //Если очередь не открыта, делать больше нечего
      ATLASSERT (SUCCEEDED (hr));
   }
   return S_OK;
}

Как получить доступ к QC-компоненту

В случае использования QC-технологии получение указателя на интерфейс объекта производится не через CoCreateInstance, а через CoGetObject – функцию COM, описания которой, как ни странно, в MSDN мы не нашли. Ее эквивалент в VISUAL BASIC – GetObject - хорошо известен. Эта функция использует моникер (текстовую строку) для получения инициализированного объекта. Моникеры были созданы еще на заре OLE, тогда они использовались для связывания составных документов.

Использование такого механизма для Queued Components вместо классического CoCreateInstance решает две проблемы: во-первых, строка моникера позволяет определять дополнительные параметры, например имя компьютера, на котором будет создан объект, и которому Call recorder будет направлять сохраненные вызовы, во-вторых, позволяет создавать объекты как в режиме QC, так и в обычном DCOM-режиме.

Моникер - это COM-объект, умеющий создавать другой COM-объект на основе текстовой строки. Фабрика класса тоже знает, как создавать объекты, но способа передачи ей дополнительной информации о начальном состоянии объекта не существует. Процесс создания компонента и получение указателя на него с помощью моникера называется связыванием (binding). Некоторые моникеры могут быть созданы с помощью других. Такие моникеры называются композитными. Строка композитного моникера в нашем примере будет выглядеть следующим образом:

"queue:ComputerName=PС1/new:QCClient.QCReceiver.1"

где “PС1” – имя компьютера. Композитный моникер состоит из нескольких частей. CoGetObject/GetObject, во время вызова пытается найти моникер компонента, описываемый самой левой подстрокой, ограниченной символом двоеточия. В нашем случае это queue – ProgID MSMQ сервиса, записанный в реестре Windows. Символы между “queue:” и “/” служат параметрами создания queue-моникера (смотри Таблица 1). Символы, находящиеся после “/” являются строкой инициализации для другого типа моникера. Это мониер new, имитирующий действие одноименного оператора создания нового объекта, присутствующего во многих языках программирования. Этот моникер узнает как создать новый объект, основанный на ProgID, следующий за ним в общей строке. Созданный моникер затем сохраняется в поток IStream и передается в начале набора COM вызовов.

Чтобы получить больше информации о моникерах, можно обратиться к “ActiveX®/COM Q&A column in the July 1997 issue of MSJ”.

Используя моникеры вместо вызова CoCreateInstance, сервер не создает компонентов на машине клиента. Вместо этого он подключается к Call recorder (регистратору) – специальному COM+ компоненту. Пока эта техника выглядит экзотично, но на самом деле ничего нового здесь нет. Известно, что когда клиент создает объект, он получает указатель на его заместитель (proxy) во внутрипроцессном обработчике. Даже создание заместителя на основе библиотеки типа во время выполнения программы не является новшеством; дуальные интерфейсы используют эту возможность уже давно. Можно представить регистратор как другой вид заместителя, который вызывает MSMQ вместо RPC.

Call recorder узнает из библиотеки типов, описывающей компонент QCReceiver, какие методы и свойства и с какими параметрами он имеет. Call recorder перехватывает исходящие вызовы и сохраняет их до тех пор, пока компонент не будет уничтожен вызовом метода Release(). Это заставляет Call recorder передавать все записанные им вызовы в одном MSMQ сообщении, на машину, где создается QCReceiver компонент.

Таблица 1
Параметры MSMQ, доступные для QC-компонентов

Наименование параметра Описание параметра Значение по умолчанию
AppSpecific Специфическая для приложения информация, прикрепленная к сообщению Отсутствует
AuthLevel Определяет необходимость подключения сигнатуры к сообщению Зависит от установок QC-приложения
ComputerName Имя компьютера на котором находится очередь получателя Зависит от установок QC-приложения
Delivery Определяет автоматическое сохранение MSMQ сообщений на диске при передаче в целях надежности (recoverable), либо в оперативной памяти для обеспечения скорости (express). Recoverable
EncryptAlgorithm Алгоритм кодирования при передаче частных сообщений По умолчанию MSMQ
FormatName Название формата очереди По умолчанию MSMQ
HashAlgorithm Алгоритм вычисления контрольной суммы, для обеспечения безопасности информации По умолчанию MSMQ
Journal Определяет необходимость и параметры ведения журнала сообщений По умолчанию MSMQ
Label Читаемая метка, присоеденяемая к сообщению По умолчанию MSMQ
MaxTimeToReachQueue Время после которого сообщение будет удалено если не будет передано получателю Infinite (бесконечно)
MaxTimeToReceive Время после которого сообщение будет удалено если не будет прочитано получателем Infinite (бесконечно)
PathName Полный путь имени файла очереди приемника Зависит от установок QC приложения
Priority Уровень приоритетности. От 1 до 7 MSMQ игнорирует приоритетность для транзактных очередей
PrivLevel Уровень секретности сообщений. Определяет необходимость засекречивания. По умолчанию MSMQ
QueueName Имя очереди на приемной машине Зависит от установок QC приложения
Trace Определяет необходимость генерации ответных сообщений, прослеживающих прохождение передаваемого сообщения по сети Off (отключено)

Первым параметром CoGetObject является структура BIND_OPTS. Мы заполнили ее значениями, устанавливаемыми методом GetBindOptions системного интерфейса IBindCtx по умолчанию, вторым – строка моникера, третим параметром является ID запрашиваемого интерфейса.

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

Получение указателя на интерфейс удаленного объекта в RPC-режиме с помощью CoCreateInstanceEx знакомо всем, кто знаком с DCOM и, так как DCOM приводится нами только для сравнения, мы не будем его обсуждать.

Отправитель сообщений MessSender App (визуальный)

Чтобы VISUAL BASIC смог увидеть наш компонент, в диалоге References (пункт меню Project/Add References…) отметим MESSSRVLib – библиотеку типов созданного нами сервера.

Внешний вид передатчика сообщений представлен на Рис. 7.


Рис. 7 Приложение-передатчик сообщений

Ниже приведен исходный текст отправителя сообщений:

Dim ServObj As New MESSSRVLib.MessServObj

Private Sub AutoPlayBox_Click()
    'На всякий случай пытаемся разослать накопленные сообщения
    ServObj.Play
End Sub

Private Sub MessageWindow_Change()
    Dim Mode As Long
    If rbQC Then 'Если выбран режим "Queued Components"
        Mode = SEND_QC_USE
    Else
        If rbDirectMQ Then 'Если выбран режим "Direct MSMQ"
            Mode = SEND_MSMQ_USE
        Else 'Если выбран режим "DCOM (RPC)"
            Mode = SEND_DCOM_USE
        End If
    End If
    'Метод Send отправляет сообщения на сервер (ServObj)
    'Если AutoPlayBox=True то:
    '   в QC-режиме происходит немедленная рассылка сообщений
    '   в DCOM-режиме происходит синхронная рассылка сообщений
    '     после чего DCOM-компонент уничтожается (чисто для
    '     сравнения производительности)
    'Если AutoPlayBox=False то:
    '   в QC-режиме происходит накопление сообщений в CALL RECORDER-е
    '   в DCOM-режиме происходит синхронная рассылка сообщений,
    '     а DCOM-компонент не уничтожается
    'в MSMQ-режиме параметр AutoPlayBox игнорируется, а сообщение
    '  всегда посылается в в очередь.
    ServObj.Send MessageWindow, Mode, AutoPlayBox
End Sub

Private Sub Play_Click()
    'В "Direct MSMQ" и "DCOM (RPC)" режимах сообщения
    'отправляются в процедуре MessageWindow_Change (ServObj.Send...)
    If rbQC Then
        'В режиме "Queued Components" вызов метода Play
        'уничтожает QC-компонент, что приводит к рассылке
        'накопленных CALL RECORDER-ом сообщений (вызовов).
        ServObj.Play
    End If
End Sub

Private Sub rbDirectMQ_Click()
    'При выборе режима "Direct MSMQ"
    'вызов метода Play приводит к освобждению
    'DCOM-компонентов или QC-компонентов.
    'В QC-режиме это приводит к отправке
    'накопленных сообщений
    ServObj.Play
    'Вызов метода MSMQTestInit создает и
    'открывает очереди MSMQ
    ServObj.MSMQTestInit
End Sub

Private Sub rbQC_Click()
    'На всякий случай пытаемся отправить накопленные сообщения
    ServObj.Play
End Sub

Private Sub rbDCOM_Click()
    'На всякий случай пытаемся отправить накопленные сообщения
    ServObj.Play
End Sub

Приложение QCClient App.

Приступим к созданию программы-клиента. Создадим ATL-проект DLL-библиотеки, поддерживающей MTS (меню Project/Add to project/New… ) с названием проекта QCClient.

Компонент EventHelper

Чтобы передавать текстовые сообщения визуальному приложению приемника, нам потребуется создать специальный компонент, который, с одной стороны, должен принимать сообщения от QC или DCOM объектов, созданных сервером (смотри Рис. 7, Рис. 5), с другой - поддерживать связь с визуальными приемниками сообщений. Мы назвали его EventHelper. Он должен поддерживать события, чтобы визуальные приложения могли к нему подключиться. Создавая его, на закладке Attributes отметим пункт Support Connection Points, а на закладке Attributes - Support FreeThread Marshaller. FreeThread-маршалер необходим для того, чтобы компоненты, созданные MTS, а он их порождает в различных потоках, были потокобезопасными.

Чтобы все визуальные приемники сообщений получали сообщения, они должны подключиться к одному источнику событий. Для этого нам необходимо его реализавать таким образом чтобы существовала только одна копия этого объекта. В COM это называется SINGLETON. Чтобы сделать это, необходимо добавить в описание класса CEventHelper ATL макрос:

DECLARE_CLASSFACTORY_SINGLETON(CeventHelper).

Вставить его лучше перед:

DECLARE_REGISTRY_RESOURCEID(IDR_EVENTHELPER).

У нас имеется еще один интерфейс - _IEventHelperEvents. Это интерфейс событий. Его реализации нет в QCClient-е. Зато при описании COM-объекта EventHelper (в файле QCClient.idl) он помечен как source:

coclass EventHelper
{
    [default] interface IEventHelper;
    [default, source] dispinterface _IEventHelperEvents;
};

Добавим к интерфейсу _IEventHelperEvents метод SetText([in] BSTR Text) – событие, на которое визуальный приемник осуществит отображение данных.

Необходимо обязательно откомпилировать проект, так как при этом автоматически создается и региструется библиотека типов. Она нужна нам для того чтобы ATL Wizard, анализируя интерфейс _IEventHelperEvents мог сгенерировать код, необходимый для вызова (firing-а) событий. После компиляции ATL поможет нам реализовать IСonnetcionPointContainer.

Для реализации IConnectionPointContainer из контекстного меню класса CEventHelper выбираем пункт Implement Connection Point… Отмечаем в диалоге _IeventHelperEvents, нажимаем OK. В описании класса появляются строки:

public IDispatchImpl<IEventHelper, &IID_IEventHelper, &LIBID_QCClientLib>,
public CProxy_IEventHelperEvents< CEventHelper >

Автоматически созданный класс CProxy_IEventHelperEvents реализует рассылку событий в функции Fire_SetText(BSTR text). Функциональность IConnectionPointContainer реализуется в шаблоне IConnectionPointContainerImpl.

Перед компиляцией проекта необходимо заменить

CONNECTION_POINT_ENTRY(IID__IEventHelperEvents) 
на 
CONNECTION_POINT_ENTRY(DIID__IEventHelperEvents).

Это ошибка разработчиков ATL Object Wizard, без нее все было бы слишком легко. Добавляем метод Fire() в компонент. Его реализация будет выглядеть так:

STDMETHODIMP CEventHelper::Fire(BSTR Text)
{
   Fire_SetText(Text);
   return S_OK;
}

Теперь объект QCReceiver сможет вызывать метод Fire() компонента EventHelper для рассылки текста сообщений всем подключившимся визуальным приемникам. QCReceiver может быть создан в режиме QC или DCOM. А как быть с режимом MSMQ?

При программировании как с помощью MSMQ API, так и с помощью MSMQ-компонентов существует возможность принимать сообщения, как в синхронном, так и в асинхронном режиме. Мы выбрали асинхронный режим, где события принимаются с помощью компонента MSMQEvent, реализующим знакомый нам IConnectionPointContainer. Чтобы подключиться к ним, нам необходимо реализовать outgoing интерфейс _DMSMQEventEvents, описанный в библиотеке типов MQOA.TLB. При реализации передачи сообщений в MSMQ-режиме в методе Send(…) компонента MessServObj мы использовали ключевое слово #import, чтобы Microsoft VC++ создал tlh-файл описания MSMQ интерфейсов. При реализации EventHelper-а мы использовали header-файл описания MSMQ интерфейсов, поставляемый вместе с Microsoft VC++ 6.0 - чтобы описать разнообразные возможности использования MSMQ-компонентов в VC++, а заодно сравнить сложность разработки разными способами.

Ключевое слово #import почему-то не создает в tlh-файле описания GUID библиотеки типов MQOA.TLB, нужного нам для реализации класса-обработчика событий с помощью ATL (хоть это и не является настоящей проблемой, мы легко смогли получить его с помощью утилиты ”OLE/COM Object Viewer”).

Для этого нам нужно реализовать Dispatch-интерфейс, поддерживающий два метода - Arrived и ArrivedError. Реализовать его можно либо в новом компоненте, либо поместив его в уже существующий компонент. EventHelper нам вполне подходит, см. Рис. 7.

Чтобы снабдить его нужной функциональностью мы создали дополнительный класс CMSMQEventEvents – реализацию интерфейса _DMSMQEventEvents и включили его в список базовых классов нашего CEventHelper’а. Описание этих классов выглядит следующим образом:

// EventHelper.h : Declaration of the CEventHelper

#ifndef __EVENTHELPER_H_
#define __EVENTHELPER_H_
#include "resource.h"  // main symbols
#include "QCClientCP.h"

//Описание интерфейсов MSMQ, поставляемое с Microsoft VC++
#include <MQOAI.H>

//Объект MSMQEvent - источник событий о приеме сообщений
extern CComPtr <IMSMQEvent> g_pEvent;

/////////////////////////////////////////////////////////////////////////////
// CMSMQEventEvents - класс - приемник событий от MSMQEvent,
class ATL_NO_VTABLE CMSMQEventEvents :    
   public IDispatchImpl<_DMSMQEventEvents, &DIID__DMSMQEventEvents, &LIBID_MSMQ> //MQOALib
{
   STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid,
      LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
      EXCEPINFO* pexcepinfo, UINT* puArgErr);
};

/////////////////////////////////////////////////////////////////////////////
// CEventHelper
class ATL_NO_VTABLE CEventHelper : 
   public CComObjectRootEx<CComMultiThreadModel>,
   public CComCoClass<CEventHelper, &CLSID_EventHelper>,
   public ISupportErrorInfo,
   public IConnectionPointContainerImpl<CEventHelper>,
   public IDispatchImpl<IEventHelper, &IID_IEventHelper, &LIBID_QCClientLib>,
   public CMSMQEventEvents,
   public CProxy_IEventHelperEvents< CEventHelper >
{

public:
   CEventHelper() {}
   
DECLARE_CLASSFACTORY_SINGLETON(CEventHelper)
DECLARE_REGISTRY_RESOURCEID(IDR_EVENTHELPER)
DECLARE_GET_CONTROLLING_UNKNOWN()

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CEventHelper)
   COM_INTERFACE_ENTRY(IEventHelper)
//DEL    COM_INTERFACE_ENTRY(IDispatch)
   COM_INTERFACE_ENTRY(ISupportErrorInfo)
   COM_INTERFACE_ENTRY(IConnectionPointContainer)
   COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
   COM_INTERFACE_ENTRY2(IDispatch, IEventHelper)
   COM_INTERFACE_ENTRY_IID(DIID__DMSMQEventEvents, _DMSMQEventEvents)
   COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
END_COM_MAP()

BEGIN_CONNECTION_POINT_MAP(CEventHelper)
   CONNECTION_POINT_ENTRY(DIID__IEventHelperEvents)
END_CONNECTION_POINT_MAP()

   HRESULT FinalConstruct()
   {
      //Создание маршалера, реализовано ATL Vizard-ом
      return CoCreateFreeThreadedMarshaler(
         GetControllingUnknown(), &m_pUnkMarshaler.p);
   }

   void FinalRelease()
   {
      //Отключение от событий приема MSMQ сообщений
      AtlUnadvise (g_pEvent, DIID__DMSMQEventEvents, m_dw);
      //Деактивация маршалера, реализовано ATL Vizard-ом
      m_pUnkMarshaler.Release();
   }

   //Маршалер, реализован ATL Vizard-ом
   CComPtr<IUnknown> m_pUnkMarshaler;

// ISupportsErrorInfo
   STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

// IEventHelper
public:
   //Создание и открытие MSMQ очереди, подключение к событиям
   STDMETHOD(MSMQInit)();

   //Mетод Fire, с помощью которого можно
   //инициировать рассылку событий
   STDMETHOD(Fire)(/*[in]*/ BSTR Text);

private:
   //Smart - указатель на объект - информации об очередях
   CComPtr <IMSMQQueueInfo> m_pQueueInfo;

   //Smart - указатель на объект - очередь
   CComPtr <IMSMQQueue> m_pQueue;

   //Cookie (число), получаемое от AtlAdvise()
   //для AtlUnadvise()
   DWORD m_dw;
};

#endif //__EVENTHELPER_H_

Для того что бы реализовать методы Arrived и ArrivedError мы просто переопределили метод Invoke в классе CMSMQEventEvents. Их DISPID можно получить с помощью утилиты "OLE/COM Object Viewer". Определение функций классов CMSMQEventEvents и CEventHelper выглядит следующим образом:

// EventHelper.cpp : Implementation of CEventHelper
#include "stdafx.h"
#include "QCClient.h"
#include "EventHelper.h"

/////////////////////////////////////////////////////////////////////////////
// CEventHelper 

//Объект MSMQEvent - источник событий о приеме сообщений
CComPtr <IMSMQEvent> g_pEvent;

//Создание и открытие очереди, подключение к событиям
STDMETHODIMP CEventHelper::MSMQInit()
{
   HRESULT hr = S_OK;

   //Нам нужен лишь один экземпляр компонента, поэтому проверяем
   //не создан ли он уже
   if (m_pQueueInfo == NULL)
      ::CoCreateInstance(CLSID_MSMQQueueInfo, NULL, CLSCTX_ALL, 
         IID_IMSMQQueueInfo, (void**)&m_pQueueInfo);

   //Получаем имя собственного компьютера, чтобы 
   //определить полный путь очереди
   ATLASSERT (m_pQueueInfo);
   char ComputerName[256];
   DWORD Len = 255;

   GetComputerName (ComputerName, &Len);
   USES_CONVERSION;

   //Задаем полный путь очереди (snt - имя сервера домена)
   TCHAR QueuePathName[256];
   wsprintf (QueuePathName, _T("snt\\MQTest_%s"), ComputerName);
   hr = m_pQueueInfo->put_PathName(CComBSTR(T2OLE(QueuePathName)));
   ATLASSERT(SUCCEEDED(hr));
   
   //Проверяем на создание компонента - источника событий
   //если не создан, то создаем
   if (g_pEvent == NULL)
      ::CoCreateInstance(CLSID_MSMQEvent, NULL, CLSCTX_ALL, 
         IID_IMSMQEvent, (void**)&g_pEvent);
   ATLASSERT(SUCCEEDED(hr));

   //Получаем интерфейс для подключения к событиям
   CComPtr<_DMSMQEventEvents> sp_DMSMQEventEvents;
   hr = QueryInterface(DIID__DMSMQEventEvents, (VOID**)&sp_DMSMQEventEvents);
   ATLASSERT(SUCCEEDED (hr));
   
   //Подключаемся к источнику событий g_pEvent
   hr = AtlAdvise (g_pEvent, sp_DMSMQEventEvents, DIID__DMSMQEventEvents, &m_dw);
   ATLASSERT(SUCCEEDED (hr));

   //Если очередь была уже открыта, закроем ее,
   //путем деактивизации компонента MSMQQueue
   m_pQueue.Release ();

   //Открытие очереди или создание, если не существует
   hr = m_pQueueInfo->Open (MQ_RECEIVE_ACCESS, MQ_DENY_NONE, &m_pQueue);
   if (FAILED (hr)) {
      m_pQueueInfo->Create (&CComVariant (MQ_TRANSACTIONAL_NONE), &CComVariant (MQ_DENY_NONE));
      hr = m_pQueueInfo->Open (MQ_RECEIVE_ACCESS, MQ_DENY_NONE, &m_pQueue);
   }
   
   //Включаем обработку событий
   if (SUCCEEDED (hr))
      hr = m_pQueue->EnableNotification (g_pEvent, &CComVariant (MQMSG_FIRST), &CComVariant (-1));

   return hr;
}

//Прием событий
STDMETHODIMP CMSMQEventEvents::Invoke(DISPID dispidMember, REFIID riid,
   LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
   EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
   CComPtr<IEventHelper> spIEventHelper;
   HRESULT hr = QueryInterface(IID_IEventHelper, (VOID**)&spIEventHelper);
   ATLASSERT(SUCCEEDED (hr));

   switch (dispidMember)
   {
   case 0: {   //Метод _DMSMQEventEvents::Arrived
      ATLASSERT(VT_DISPATCH == pdispparams->rgvarg[1].vt && 
               VT_I4 == pdispparams->rgvarg[0].vt);

      //Smart - указатель на очередь
      CComPtr<IMSMQQueue> pQ;
      
      //На всякий случай получим его через QueryInterface,
      //так как это может быть указатель на IUnknown
      hr = pdispparams->rgvarg[1].pdispVal->QueryInterface (IID_IMSMQQueue, (VOID**)&pQ);
      ATLASSERT(SUCCEEDED (hr));
      
      //svMissing - отсутствующий optional-параметр
      CComVariant svMissing;
      svMissing.vt = VT_ERROR;

      CComVariant svTransaction(0, VT_ERROR);
      CComVariant svWantDestinationQueue(0, VT_ERROR);
      CComVariant svWantBody(0, VT_ERROR);
      CComVariant svReceiveTimeout(0, VT_ERROR);
      
      //Получаемое сообщение
      CComPtr<IMSMQMessage> spMessage;

      hr = pQ->ReceiveCurrent(&svTransaction, &svWantDestinationQueue, &svWantBody,
         &svReceiveTimeout, &spMessage);
      ATLASSERT(SUCCEEDED (hr));

      //Получаем само сообщение
      CComVariant svBody;
      hr = spMessage->get_Body(&svBody);
      ATLASSERT(SUCCEEDED (hr));
      
      //Рассылаем текст сообщения всем визуальным приемникам
      spIEventHelper->Fire(svBody.bstrVal);

      //Заново включаем обработку событий
      pQ->EnableNotification (g_pEvent, &CComVariant (MQMSG_FIRST), &CComVariant (-1));
      ATLASSERT(SUCCEEDED (hr));
         }
      break;
   case 1: {   //Метод _DMSMQEventEvents::ArrivedError
      ATLASSERT(VT_DISPATCH == pdispparams->rgvarg[2].vt && 
               VT_I4 == pdispparams->rgvarg[1].vt &&
               VT_I4 == pdispparams->rgvarg[0].vt);

      WCHAR ErrorDescr[256];
      swprintf (ErrorDescr, L"Error 0x%X", pdispparams->rgvarg[1].lVal);

      //Рассылаем номер ошибки всем визуальным приемникам
      spIEventHelper->Fire (ErrorDescr);
         }
      break;
   }
   return S_OK;
}

Разработка QC интерфейса

Программирование QC-компонентов так же просто, как и обычных COM-компонентов. Вы описываете COM-объект так, как привыкли это делать, теми средствами, которые вам удобны (Visual Basic, ATL и т.д.). На первый взгляд, логика QC довольно проста и требует минимального обдумывания. Взаимодействие ваших QC-компонентов с COM+ не требует больших усилий, подобно посещению синагоги в субботу. Однако изо дня в день, согласно накладываемым иудаизмом ограничениям, необходимо кормить семью только кошерной пищей, три раза в день, семь дней в неделю, а это заставляет продумывать будущее более тщательно.

Все написанные вами ранее программы, использующие COM, создавались с учетом возможности синхронного взаимодействия с объектами. Вы вызывали метод объекта и получали обратно информацию, используя выходные (out) параметры. Каждый COM-вызов имел как минимум потенциальную возможность двустороннего взаимодействия, хотя бы через HRESULT, возвращаемый каждым методом COM-интерфейса (программисты на VB тоже имели с ним дело, хотя и в скрытом виде – через объект Err). Как следствие, вызываемый объект должен был быть обязательно активен в момент вызова.

В случае работы с QC-объектами это не так. Все интерфейсы, используемые в QC должны быть однонаправленными (не иметь выходных параметров), чтобы они могли быть в последующем записаны и исполнены, за исключением специальных - IUnknown и IDispatch. Другими словами, ваши интерфейсы не должны иметь параметров типа output и input/output, потому, что вы не можете знать, когда вызываемый объект получит ваш запрос, или что вызывающий объект будет работать, когда придет ответ от вызываемого. Короче говоря, флаги IDL [out], [in/out] и [out, retval] – запрещаются.

Component Services проверяет каждый интерфейс в библиотеке типа компонента, когда компонент помещается в приложение Component Services (в терминах MTS – package). Если интерфейс удовлетворяет этим критериям, он считается потенциально “очередизируемым”, и флаг Queued в свойствах “лояльного” интерфейса становится доступным. Это позволяет администратору помечать, если необходимо, интерфейсы как поддерживающие работу через очередь сообщений. В противном случае этот флаг будет недоступен, и никто не сможет получить доступ к этому интерфейсу как к QC.

Более того, параметры не только должны быть входящими, но и передаваться по значению. Передача параметров по ссылке не поддерживается. Могут передаваться даже COM-объекты, поддерживающие интерфейс IPersistStream.

Те, кто создает компоненты на VB, могут здесь столкнуться с определенными проблемами. По умолчанию, Visual Basic передает все параметры по ссылке, используя [in, out] атрибут, который QC не поддерживает. Если вы хотите использовать VB для написания компонентов, вы должны определить ByVal атрибут для передачи параметров, а С++ программистам придется забыть про “*”.

Более тонкое ограничение QC в том, что COM-вызовы, производимые клиентом в течение сеанса с компонентом, должны быть самодостаточными. Сервер не сможет в будущем связаться с клиентом для получения отсутствующей информации. Например, передача наборов информации с помощью коллекций или энумераторов является общей практикой COM-клиентов. Клиент становится, в сущности, сервером, а сервер – его клиентом. Ведь коллекция - это тоже COM-объект. Такой подход не будет работать с QC, если только, конечно, коллекция не поддерживает IPersistStream. Если клиент хочет передать набор данных для компонента, он может использовать столько вызовов, сколько необходимо, либо передавать их в виде массива.

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

Компонент QCReceiver

Создаем с помощью ATL компонент QCReceiver. Это и есть наш QC-компонент. На Рис. 7 он показан пунктирной линией. Это означает, что, в отличие от всех других созданных нами компонентов, он создается не на все время работы QCClient App, а может быть деактивирован в QC режиме для обработки последовательности вызовов. Вызываем с помощью пункта меню Insert/New “ATL Object Wizard”. В окне ATL Object Wizard выбераем Simple Object, записываем имя компонента QCReceiver.

Чтобы вызвать метод Fire() из QC-объекта QCReceiver, создадим в нем метод Send([in]BSTR Text). Как можно видеть, он полностью удовлетворяет ограничениям, накладываемым на методы интерфейсов QC-компонентов. Реализация его будет выглядеть следующим образом:

{
   CComPtr<IEventHelper> spIEventHelper;
   spIEventHelper.CoCreateInstance(CLSID_EventHelper);
   return spIEventHelper->Fire(Text);
}

После компиляции можно создать визуальное приложение приемника сообщений. Его целью являются получение сообщений от EventHelper и отображение полученного текста в текстовом окне.

Приемник сообщений MessReceiver App. (визуальный)

Эта программа написана на VB отчасти потому, что на нем одно удовольствие подключаться к событиям, а скорее для разнообразия. Внешний вид программы-приемника сообщений представлен на Рис.8.


Рис. 8 Приложение-приемник сообщений

Ниже приведен текст программы:

'Атрибут WithEvents говорит VB что при создании
'объекта Helper необходимо подключиться к его событиям
Dim WithEvents Helper As QCClientLib.EventHelper

Private Sub Form_Load()
    'На загрузку формы создаем компонент EventHelper
    Set Helper = New QCClientLib.EventHelper
    'Производим инициализацию MSMQ режима
    Helper.MSMQInit
End Sub

'Реализация события SetText объекта Helper
Private Sub Helper_SetText(ByVal Text As String)
    'Отобразить пришедшее сообщение в текстовом окне "MessageWindow"
    MessageWindow = Text
End Sub

Проще не придумаешь. Не правда ли?

Чтобы использовать созданный нами компонент EventHelper, добавляем ссылку на библиотеку типа QCClientLib в диалоге References.

Как видно из процедуры Form_Load(), мы вызываем функцию инициализации MSMQ-режима компонента EventHelper. Сначала мы пытались обойтись без нее, производя создание, открытие и подключение к событиям MSMQEvent в конструкторе компонента. Все работало, кроме Helper_SetText(…). Происходил сбой при вызове Invoke в сгенерированной ATL’ом функции Fire_SetText(). После вывода инициализации MSMQ в отдельный метод все заработало.

Установка приложения

Чтобы приложение заработало, созданные компоненты необходимо зарегистрировать в Component Services Console. На передающей машине выбираем пункт COM+ Applications, из контекстного меню выбираем New/Application… В панели диалога выбираем пункт Create an empty application… и называем его MessServer. В панели Set Application Identity выбираем Interactive User. В ветке COM+ Applications находим созданное нами приложение-сервер. Выбираем пункт Components, и из контекстного меню New/Component вызываем Install new components…, в диалоге выбора файлов выбираем MessSrv.dll. На Рис. 9 показана структура установленного приложения-сервера.


Рис. 9 Структура установленного приложения сервера

Подобные образом создаем приложение QCClient, только на этот раз выбираем библиотеку QCClient.dll. Дополнительно необходимо зарегистрировать его как поддерживающее QC-компоненты. В ветке COM+ Applications выбираем установленное нами приложение QCClient. Из контекстного меню выбираем Properties. На закладке Advanced отмечаем Live running while idle, на закладке Queuing отмечаем Queued и Listen. Таким образом мы определили, что приложение QCClient будет поддерживать работу через очередь сообщений и во время выполнения ожидать приема MSMQ-сообщений.

Выбираем интерфейс IQCReceiver. Из контекстного меню выбираем Properties. На закладке Queuing отмечаем Queued. Component Services отслеживает возможность использования интерфейсов в queued-режиме. Если бы этот интерфейс не соответствовал требованиям queued-режима, флажок Queued был бы недоступен.

Чтобы установить созданные нами компоненты на нужные компьютеры мы попытались экспортировать их, как описано в помощи к Component Services. Это нам не удалось по причине постоянно возникающей ошибки File not found… Вероятно, это одна из пока еще не устраненных ошибок W2k Beta 3. По этой причине нам пришлось устанавливать все вручную. Приложение-сервер находилось на том же компьютере, что и визуальный передатчик. Один экземпляр QCClient App. и визуального приемника мы установили на том же компьютере, а второй установили на другой компьютер, произведя все действия по установке приложения клиента, так же, как было описано ранее.


Рис. 10 Структура установленного приложения QCClient

Конфигурация MSMQ

Сразу после установки наше распределенное приложение работать не захотело. Сообщения мы смогли передать только в RPC-режиме. При попытке передачи сообщений в QC-режиме при создании компонента устойчиво возникала ошибка 0xC00E0030 – “A cryptogrphic function has failed”. В приложениях мы не пользовались никакими возможностями кодирования сообщений. Поэтому нам пришлось проверить системную конфигурацию MSMQ.

Наши приложения мы устанавливали на Windows 2000 Server Beta3, входящие в домен NT4 с установленным Options Pack. Для работы Windows 2000 с MSMQ необходима установка Active Directory и DNS. Если MSMQ работает под управлением NT4, Windows 2000 необходимо устанавливать как Independent Client для сервера MSMQ. Эту операцию возможно произвести на этапе установки ОС или позже, выбрав пункт панели управления (Control Panel) Add/Remove Programs.

Но и после правильной установки системы необходимо произвести конфигурацию MSMQ. Может быть, при использовании ОС одной версии как на сервере, так и на рабочих станциях этот шаг можно было бы опустить, но в нашем случае это было необходимо. Мы сделали это так, как описано в файлах помощи по установке Windows 2000.

В Control Panel запустили Message Queuing. На закладке Security нажали кнопку Renew Internal Certificate. После этого нажали кнопку Renew Cryptographic Keys. После перезагрузки компьютеров все заработало. Не забудьте, что MSMQ может работать на NT4 только c установленным и запущеным MS SQL Server версии 6.5 и выше. В Windows 2000 необходимость в MS SQL Server отпадает.

Тестирование распределенного приложения

При тестировании мы обратили внимание на нерасторопность работы приложений в QC-режиме с установленным AutoPlay. В этом режиме сообщение передается при каждом вводе символа в передатчике сообщений. Надо заметить, что при использовании MSMQ AutoPlay-режим действует всегда (его нельзя отключить при помощи соответствующего переключателя), а при использовании DCOM этот режим хотя и оставлен для сравнения производительности, но смысла не имеет, поскольку при этом сообщения все равно передаются синхронно, а лишние вызовы Release() и CoCreateInstance(…) только замедляют работу. При использовании QC приложения работали асинхронно, но режим AutoPlay приводил к таким задержкам, что время на отработку запросов по сети было незначительным по сравнению со временем, затрачиваемым на создание и уничтожение QC-компонента. Временами казалось, что приложение работает в синхронном режиме.

Мы пытались различными способами увеличить производительность QC-компонента. Первое, что пришло на ум – установить значение параметра Delivery, отвечающего за сохранение сообщений на диске в “Express”. Значение по умолчанию “Recoverable” заставляло MSMQ сохранять сообщения на диске для повышения надежности. В результате производительность если и выросла, то очень незначительно. Видимо дело здесь в том, что доля накладных расходов в которой повинен MSMQ, настолько мала, что даже самые суровые режимы его работы были каплей в море времени, затрачиваемого на создание и уничтожение QC-компонентов.

Возможно, проблема связана со значительными накладными расходами на создание рекордера объекта QCReceiver, или с тем, что QC-подсистема не хотела воспринимать заданные нами параметры. Мы пытались создать пул QCReceiver-объектов, но производительность осталась по-прежнему низкой.

При увеличении количества одновременно загруженных визуальных приложений–приемников скорость отработки запросов не изменялась. С выключенным режимом AutoPlay (когда сообщение передавалось при помощи отдельной кнопки) задержки были менее существенными и асинхронность была хорошо видна (после каждого нажатия на кнопку Play можно было продолжать редактировать текст без каких-либо видимых задержек).

В режимах MSMQ и DCOM скорость обработки была намного выше. В DCOM-режиме, как и следовало ожидать, при загрузке большого количества приемников сообщений производительность пропорционально падала, в MSMQ, напротив, производительность передатчика вообще не зависела от количества приемников сообщений.

Мы пробовали временно отключать компьютер приемника от сети. В QC-режиме это не вызывало никаких изменений, в отличие от RPC-режима, при котором происходило “зависание” передатчика. Переданные сообщения терялись. В режимах QC и MSMQ все было, как и обещано: сообщения буферизировались MSMQ-сервером и после восстановления связи с приемником успешно воспроизводились в окне программы-приемника. Однако и в этом испытании прямая работа с MSMQ оказалась более выигрышна. Дело в том, что для передачи сообщения через MSMQ надо, чтобы очередь, через которую ведется передача, была известна передатчику. Для этого ее надо создать или хотя бы однажды открыть с передающего компьютера. Причем при последующем подключении не надо даже быть подключенным к сети. Физически же сообщения будут переданы при первом же подключении к сети. Возможна даже передача в несколько приемов (подключений). С QC-компонентом не совсем так, хотя, судя по документации и рекламе, все должно быть еще проще, чем с MSMQ. Дело в том, что QC-компонент (по крайней мере в наших тестах) при создании упорно лез на сервер. Зачем ему понадобился сервер, мы выяснить так и не смогли. Возможно, проблема в том, что мы сделали что-то не так. Может, дело в том, что мы имели дело с бета-версией. А возможно, это действительно ограничение технологии. Но тогда к чему столь шумная реклама несуществующей на самом деле функциональности.

После установки QC-приложения в Component Services на сервере MSMQ создается очередь с таким же именем. Сообщения, проходящие в очереди можно журнализировать и просматривать их содержимое с помощью Computer Management, как показано на Рис. 11. Как это сделать, можно прочесть в документации.


Рис. 11 Просмотр содержимого сообщения в Computer Management

Что касается сложности разработки, то тем, кто хорошо знаком с компонентной моделью Microsoft, будет легко создавать и использовать QC-компоненты. Хорошим помощником в этом служат ATL и VB.

Программирование с помощью MSMQ-компонентов, поставляемых MQOA.dll, на VISUAL BASIC не трудно, но на VC++ довольно трудоемко и громоздко (смотри EventHelper.cpp). Программирование на MSMQ C API, по нашему опыту, еще сложней.

Использование транзакций

Многие разработчики оценили пользу транзакций для обеспечения целостности данных в распределенных системах. Эти проблемы встречаются и при асинхронных операциях. QC-компоненты поддерживают транзакции как при взаимодействии с другими объектами, так и для обеспечения собственной надежности.

И MSMQ, и QC прекрасно поддерживают транзакции. К примеру, компоненты могут делать изменения в базе данных SQL Server и посылать сообщения в очередь с помощью MSMQ. Если транзакция подтверждается (Commit), то изменения вносятся в базу данных, а сообщения становятся доступны конечному пользователю. Если транзакция отменяется (Abort), ни того, ни другого не произойдет. Более подробно о транзакциях в MSMQ можно узнать в "Use MSMQ and MTS to Simplify the Building of Transactional Applications" by Mark Bukovec and Dick Dievendorff (MSJ, July 1998).

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

Транзакции имеют еще одно важное преимущество. Если по какой либо причине сервер MSMQ терпит неудачу при попытке передачи вызовов, то у разработчика QC-приложений нет никаких способов повторить операцию. QC делает это автоматически! Без использования механизма транзакций, разработчику пришлось бы создавать свой механизм повторной передачи.

Исходя из этого, некоторые входящие сообщения могут вызывать постоянную отмену транзакций. Например, пользователь, не знающий, что схема базы данных изменилась, посылает запросы к ней, тем самым обрекая на неудачу переданные запросы. Автоматический механизм повторения возвращает запрос обратно в очередь, после чего все повторяется до бесконечности. Очевидно, таких ситуаций быть не должно. Механизм автоматических повторений имеет встроенное решение этой проблемы.

QC создает не одну, а целых семь очередей для установленного приложения. В нашем случае они именуются QCClient, QCClient_0, QCClient_1, …, QCClient_5, QCClient_DeadQueue. При передаче сообщения оно помещается в очередь QCClient, при первой неудаче в QCClient_0 и так далее. Временные задержки повторения сообщений возрастают вместе с номером очереди. Если передача не состоялась в очереди QCClient_5, то оно попадает в DeadQueue, страшное название которой говорит само за себя. Системная утилита MessageMover позволяет диагностировать проблему и очистить очередь.

Ценой устойчивости к ошибкам и универсальности при использовании транзакций в QC является снижение производительности. Хотя во многих случаях для пользователя работа будет более приемлемой в QC-режиме, нежели при использовании DCOM. Для повышения быстродействия использование транзакций в QC-приложении можно отключить. Для этого необходимо до того, как приложение будет отмечено в Component Services как Queued с помощью Computer Management создать очередь с именем приложения. В нашем случае это “QCClient”, при ее создании нужно отметить ее, как не поддерживающую транзакции. Component Services сопоставит наше приложение с этой очередью и примет ее параметры.

Требования безопасности

QC компоненты имеют те же требования по безопасности, что и обычные компоненты. Механизм защиты информации в асинхронных системах действует иначе, нежели в синхронных системах, ориентированных на соединение. COM+ использует усовершенствованную схему безопасности, основанную на ролях, знакомых тем, кто уже использовал MTS.

Аутентификация – первый вопрос защиты информации. Является ли вызывающий тем, кем он себя называет? Механизм вопрос-ответ, заложенный в DCOM, не может быть использован для QC-компонентов, так как MSMQ не гарантирует одновременной работы. MSMQ использует другой принцип аутентификации, основанный на цифровых сигнатурах, путешествующих вместе с сообщениями. Они способны вычислять и кодировать хеш, предотвращающий вмешательство извне. После прибытия сообщения на приемную сторону MSMQ проверяет сигнатуру, вычисляет хеш и, если что-то не так, направляет сообщение в deadletter-очередь.

При создании QC-компонента, QC включает опознавательный механизм MSMQ, основанный на установках приложения на машине клиента, либо при передаче значения через параметр моникера функции CoOGetObject/GetObject.

Второй вопрос – авторизация. Кем является клиент и может ли он делать то, что хочет? COM+ решает эту проблему на основе ролевого механизма. Причем неважно, как создается компонент - на основе QC или синхронной DCOM.

Что касается передачи информации по сети, то в некоторых приложениях необходимо ее кодировать, по крайней мере, в некоторых операциях. Синхронная DCOM обеспечивает эту возможность при установке уровня аутентификации в значение Packet Privacy. MSMQ обеспечивает эту возможность при установке атрибута PrivLevel в значение MQ_MSG_PRIV_LEVEL_BODY при вызове функции GetObject/CoGetObject. Это заставляет MSMQ кодировать содержимое сообщения перед передачей. Атрибут EncryptAlgorithm позволяет выбрать способ кодирования.

Заключение

Способность поддерживать разъединенную архитектуру чрезвычайно полезна в распределенных системах. QC-сервис, основывающийся на транспортном механизме MSMQ Windows 2000 позволяет с успехом заменить RPC для доступа к компонентам. Для разработчиков QC-компоненты дают относительно простой способ обеспечить приложение преимуществами асинхронной обработки без существенного изменения модели программирования. Хотя производительность QC компонентов и оставляет желать лучшего, скорость работы для многих приложений не является определяющим фактором.

Выбор синхронного или асинхронного способа взаимодействия, программирования с помощью QC или MSMQ остается за программистом. Единого решения не существует. Все зависит от тех задач, которые должно решать приложение, от условий, в которых оно будет выполняться, от требований к надежности и производительности. Как быть – решать разработчику.

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

Тем же русским, кто жить не может без быстрой езды и тем, кому не пристало ждать, когда в руки попадет журавль, мы советуем воспользоваться MSMQ - ведь он уже доступен в Windows NT 4 Server, к тому же он прекрасно интегрируется с MTS (дополняя синхронную идеологию необходимой гибкостью) и показал выдающиеся скоростные характеристики.


Впервые статья была опубликована в журнале <Технология Клиент-Сервер>.
Эту и множество других статей по программированию, разработке БД, многоуровневым технологиям (COM, CORBA, .Net, J2EE) и CASE-средствам вы можете найти на сайте www.optim.su и на страницах журнала.