ПРОГРАММИРОВАНИЕ

Технологии компонентного программирования

Добрынин В.



Процедурное, объектно-ориентированное и компонентное программирование

Процедурное программирование

Цель данного раздела состоит в демонстрации некоторых недостатков технологии процедурного программирования.

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

// Книги и журналы (процедурное программирование)

#include

#include

#define MAX LENGTH 100 // максимальное число символов в имени

// автора или в названии издания

#define MAX ID 100 // максимальное число изданий


// Книга

typedef struct {

char szAuthor[MAX_LENGTH]; // автор

char szTitle[MAX_LENGTH]; // название

int nYear; // год издания

} Book;


// Журнал

typedef struct {

char szTitle[MAX LENGTH]; // название

int nYear; // год выпуска

int nNumber// номер;

} Journal;


// Массив изданий

typedef struct {

int nPubldx; // индекс издания (1 — книга, 2 — журнал)

void* pPub; // указатель на издание

} Pub;


Pub aPub[MAX ID];


int nNewID = 0; // индекс нового издания


// Прототипы глобальных функций

void DisplayBook(Book &book); // вывод описания книги

void DisplayJournal(Journal Sjournal); // вывод описания журнала

void NewBook(); // ввод данных о новой книге

void NewJournal; // ввод данных о новом журнале


// Главная функция

void main() {

Book *pBook;

Journal *pJournal;

int nMenu, flag = 1;

while(nNewID < MAX_ID && flag)

{

// Вывод пунктов меню

cout << "Новое издание. Введите: " << endl;

cout << "1 для книги,\n 2 для журнала\n 3 — выход" << endl;


// Выбор пункта меню


сin >> nMenu;

switch (nMenu)

{ case 1: NewBook(); break;

case 2: NewJournal(); break;

default: flag = 0; break;

}

}


// Вывод описаний изданий

for (int id = 0; id < nNewID; id++)

{

switch (aPub[id].nPubldx)

{ case 1: pBook = (Book*) aPub[id].pPub;

DisplayBook(*pBook);

delete pBook;

break;

case 2: pJournal = (Journal*) aPub[id].pPub;

DisplayJournal(*pJournal);

delete pJournal;

break;

}

}

}


// Реализация глобальных функций

void DisplayBook(Book &book) {

cout << "BOOK: " << endl;

cout << "Author: " << book.szAuthor << endl;

cout << "Title: " << book.szTitle << endl;

cout << "Year: " << book.nYear << endl;

}


void DisplayJournal(Journal Sjournal) {

cout «"JOURNAL: " << endl;

cout <<"Title: " << journal.szTitle << endl;

cout << "Year: " << journal.nYear << endl;

cout << "Number: " << journal.nNumber << endl;

}


void NewBook() {

char szBuffer[MAX LENGTH];

int nY;


Book* pBook = new Book;


cout << "Author: ";

cin >> szBuffer;

strcpy(pBook —> szAuthor, szBuffer);

cout << "Title: ";

cin >> szBuffer;

strcpy(pBook — > szTitle, szBuffer);

cout << "Year: "; cin >> nY;

pBook —> nYear = nY;

aPub[nNewID].nPubldx = 1;

aPub[nNewID++].pPub = pBook;

}

void NewJournal() {

char szBuffer[MAX_LENGTH];

int nY, nN;


Journal* pJournal = new Journal;


cout << "Title: ";

cin >> szBuffer;

strcpy(pJournal —> szTitle, szBuffer);

cout << "Year: "; cin >> nY;

pJournal —> nYear = nY;

cout << "Number: "; cin >> nN;

pJournal —> nNumber = nN;

aPub[nNewID].nPubldx = 2;

aPub[nNewID++].pPub = pJournal;

}


Данный код содержит ряд недостатков, которые (хотя бы отчасти) можно объяснить стремлением придерживаться процедурного подхода

• Наличие нескольких глобальных функций и связанных с ними глобальных данных

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

• Хранение в массиве aPub не только указателя на издание (void* pPub), но и информации о типе издания (int nPubldx)

Информация о типе необходима для выполнения явного преобразования типа

pBook = (Book*) aPub[id].pPub,

pJournal = (Journal*) aPub[id].pPub,

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


Объектно-ориентированное программирование

Теперь рассмотрим объектную реализацию предыдущей программы. Проект включает в три класса: базовый класс CPubiication и производные от него классы CBоок и CJournal, главной функции в файле publications.срр реализуется основная бизнес-логика.

// Заголовочный файл CPublication.h для класса CPublication

#ifndef _CPUBLICATION_

#define _CPUBLICATION_


#define MAX LENGTH 100 // максимальное число символов в имени автора и названии публикации


// Базовый класс

class CPublication

{

public:

CPublication (); // конструктор

virtual ~CPublication(); // деструктор

virtual void Display (); // вывод описания публикации


protected:

char m_szTitle[MAX_LENGTH]; //название публикации

int m_nYear; // год издания

};

#endif

Следующий файл содержит реализацию класса CPublication

// Файл CPublication.срр — реализация класса CPublication

#include "CPublication.h"

#include

// Конструктор

CPublication::CPublication()

{

// Ввод года издания и названия

cout << "Year: ";

cin >> m_nYear;

cout << "Title;";

cin >> m_szTitle;

}


// Деструктор

CPublication::~CPublication ()

{

}


// Вывод описания публикации

void CPublication::Display()

{

cout << "PUBLICATION: " << endl;

cout << "Title: "<< m_szTitle << endl;

cout << "Year: "<< m_nYear «endl;

}


Файлы CBook.h и CBook.cpp задают класс CBook.

// СВоок.h

#ifndef _СВООК_

#define _СВООК_


#include "CPublication.h"

class CBook: public CPublication // класс CBook — производный от CPublication

{

public:

CBook(); // конструктор

virtual ~CBook(); //деструктор

virtual void Display(); // вывод описания книги


protected;

char m_szAuthor [MAX_LENGTH]; //имя автора

};


#endif

// CBook.cpp


#include

#include "CBook.h"


// Конструктор

CBook::CPublication()

{

// Ввод имени автора

cout <<"Author: ";

cin >> m_szAuthor;

}


// Деструктор

CBook::~CBook()

{

}


// Вывод описания книги

void CBook::Display ()

{

cout << "BOOK: " << endl;

cout << "Author: " << m_szAuthor << endl;

cout << "Title: " << m_szTitle << endl;

cout << "Year: " << m_nYear<< endl;

}


Файлы CJournal.h и СJournal.cpp задают класс СJournal.

// СJournal.h

#ifndef _CJOURNAL_

#define _CJOURNAL_


#include "CPublication.h"

class СJournal: public CPublication // класс СJournal — производный от CPublication

{

public:

СJournal(); // конструктор

virtual ~СJournal(); //деструктор

virtual void Display(); // вывод описания книги


protected;

int m_Number; // номер журнала

};


#endif

// СJournal.cpp


#include

#include "CJournal.h"


// Конструктор

CJournal::CJournal(): CPublication ()

{

// Ввод номера журнала

cout << "Number: ";

cin >> m_nNumber;

}


// Деструктор

CJournal::~CJournal ()

{

}


// Вывод описания журнала

void СJournal::Display()

{

cout << "Journal: " << endl;

cout << "Title: " << m_nTitle << endl;

cout << "Year: " << m_nYear << endl;

cout << "Number: " << m_nNumber << endl;

}


И, наконец, файл publications.cpp с бизнес-логикой

// Книги и журналы (объектно-ориентированное программирование)

#include

#include "CBook.h"

#include "CJournal.h"

#define MAX_ID 100 // максимальное число изданий

void main()

{

int nMenu, flag = 1;

int nNewID =0; // индекс нового издания

CPublication* aCPublication[MAX_ID]; // массив изданий

while(nNewID < MAX_ID && flag)

{


// Вывод пунктов меню

cout << "Новое издание. Введите: "<< endl;

cout << " 1 для книги,\n 2 для журнала\n 3 — выход" << endl;


// Выбор пункта меню

cin >> nMenu;

switch (nMenu)

{ case 1: aCPublication[nNewID++] = new CBook(); break;

case 2: aCPublication[nNewID++] = new CJournal(); break;

default: flag = 0; break;

}

}


// Вывод описаний изданий

for (int id = 0; id < nNewID; id++)

{

aCPublication[id]->Display();

delete aCPublication[id];

}

}

В данном примере демонстрируется использование основных принципов объектно-ориентированного программирования

• Инкапсуляция

Инкапсуляция означает сокрытие от пользователя класса его внутренней кухни. Данные и функции (методы) хранятся вместе, доступ к данным (которые следует размещать в защищенной секции protected или private) доступен только с помощью методов класса. Это позволяет разработчику класса менять код класса не изменяя код использующих его приложений (если только не были изменены сигнатуры методов).

• Наследование реализации

Цель механизма наследования — повторное использование кода. Класс в, наследующий класс а, наследует его данные и методы.

• Полиморфизм

Так как и класс CBоок, и класс CJournal происходят от одного базового класса CPublication, а метод Display () в базовом классе является виртуальным, все производные от этого базового класса классы могут переопределить этот метод. Таким образом, мы имеем возможность вызывать метод Display () для любого объекта любого класса порожденного от CPublication и не беспокоиться при этом о выяснении типа объекта.

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

• Повторное использование кода

Это одна из целей ООП. Возможны два подхода:

♦ Распространение библиотек классов в виде исходного кода (' белый ящик")

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

Но с этим способом связаны следующие проблемы:

— Многие программисты предпочитают писать свой код, а не изучать чужой.

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

— Код библиотеки включается в код всех использующих ее приложений,

♦ Упаковка класса в динамически компонуемую библиотеку (DLL)

При этом мы избавляемся от проблем, возникающих при распространении классов в виде исходного кода. Но, естественно, возникают новые проблемы:

— Отсутствие двоичного стандарта для C++

Различные компиляторы с C++ по разному решают вопросы реализации отдельных языковых особенностей языка C++ и некоторые вопросы компоновки. В связи с этим, в общем случае нельзя гарантировать, что DLL, подготовленная на одном компиляторе, будет работать с клиентом, подготовленным с помощью другого компилятора.

— Проблема версий DLL

C++ поддерживает синтаксическую инкапсуляцию, но не двоичную. Иными словами, от клиента скрыта реализация класса на уровне языка, что позволяет менять реализацию класса не меняя кода клиента. Но при этом перекомпиляция клиента в общем случае необходима. Это связано с тем, что именно клиент отводит память под все данные экземпляра класса. Это делается ради повышения эффективности. В результате, если при изменении реализации класса был изменен состав (иили порядок) данных класса, то клиент не будет работать с новей версией DLL без перекомпиляции. Установив новую версию DLL, мы погубим все приложения, которые были скомпилированы для работы со старой версией.

• Не решены вопросы разработки распределенных приложений

Собственно, такая задача и не ставилась при разработке ООП. Однако в современных условиях распределенность приложений становится необходимым требованием.


Компонентное программирование

В данном разделе будет дано только очень короткое введение в проблематику компонентного программирования. Подробнее — в последующих главах.

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

Здесь стоит отметить, что роль интерфейсов в СОМ значительно более важная, чем роль посредника. Все в СОМ начинается с интерфейсов. Они определяются первыми и задают семантику некоторого сервиса. Различные классы могут реализовывать заданный интерфейс, обеспечивая тем самым полиморфизм на новом уровне.

Имеются различные технологии, реализующие парадигму компонентного программирования. Среди них COM (DCOM, СОМ+), С ORB A, Net.

Для определенности остановимся на подходе, используемом в СОМ.

Компонент — это хранилище (в виде DLL или EXE файла) для одного или нескольких классов. Все, что знает клиент о классе, это его уникальный идентификатор и один или несколько интерфейсов, обеспечивающих доступ к реализованным данным классом методам. Допускается реализация компонента и использующего его клиента на различных языках (Visual C++, Visual Basic). В реестре системы хранится информация о местоположении компонента, содержащего данный класс (на локальном или удаленном компьютере). Это позволяет системе прозрачно для клиента перенаправлять вызовы методов к нужному компоненту и возвращать результаты.

Таким образом, обеспечивается выполнение двух важных принципов компонентного программирования:

• независимость от языка программирования,

Основные особенности компонентного программирования можно коротко охарактеризовать следующим образом:

• Инкапсуляция

В СОМ инкапсуляция находится на более высоком уровне чем в ООП. Между клиентом и реализацией класса находятся интерфейсы. Интерфейс — абстрактный базовый класс, который не имеет элементов данных и который является прямым потомком не более чем одного другого интерфейса. Реализация методов данного интерфейса выполняется в классе, который является потомком данного и, возможно, еще других интерфейсов.

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

• Наследование интерфейсов

В отличие от ООП, технология компонентного программирования не поддерживает наследование реализации класса. Наследуются только интерфейсы. Каждый интерфейс получает свой уникальный идентификатор и может реализовываться в различных классах, включаемых в различные компоненты. Новый интерфейс может наследовать ранее описанным интерфейсам. Например, в СОМ, любой интерфейс должен наследовать стандартному интерфейсу IUnknown. Наследование интерфейса в частности означает, что при реализации методов нового интерфейса будут реализованы и все методы, описанные в наследуемом интерфейсе.

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

♦ Контейнеризация

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

♦ Агрегация

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

• Полиморфизм

Если описан некоторый интерфейс, то любое число классов могут реализовывать его любым способом, на любом языке (поддерживающем СОМ). Конечно, при этом не должна меняться семантика интерфейса. Иными словами, клиенту не важно, кто и как реализовал интересующий его интерфейс. Во всех случаях он получит именно то, что ожидает.

• Бинарное представление

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

• Инфраструктура для распределенных приложений

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

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


Эволюция распределенных систем

Коротко напомним напомним эволюцию, которую претерпели распределенные системы:

• Одноуровневая система

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

• Локальная вычислительная сеть персональных компьютеров

Пользователи работают на персональных машинах и совместно используют файловый сервер, принтер.

• Архитектура клиент/сервер

Это так называемая двухзвенная архитектура. Сервер (например, баз данных) ожидает запросы клиентов (например, выраженные на SQL), обрабатывает запрос и возвращает результат клиенту. Клиенты работают с сервером независимо. Целостность данных обеспечивается механизмом транзакций.

Еще один пример системы с такой архитектурой — поисковые системы типа Alta Vista, Google, Yandex. В отличие от традиционных клиентов для работы с сервером баз данных (как правило, "толстых" клиентов, требующих специальной процедуры установки на машине пользователя), упомянутые поисковые системы работают с "тонким" клиентом, например, с браузером Internet Explorer, не зависящим от конкретного приложения.

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

• Трехзвенная архитектура клиент/бизнес-логика/данные

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

• Web сервисы

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


Технология COM (Component Object Model — компонентная объектная модель) от Microsoft

Перепишем приложение о книгах и журналах, используя идеологию модели СОМ — Component Object Model от Microsoft. Изложение основ этой модели будет проведено на примерах.


Интерфейсы

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

Часто базовую архитектуру СОМ определяют с помощью следующей формулы

Базовая архитектура СОМ = сервер/класс/интерфейс/метод

Компонент реализуется в виде сервера (одного из трех видов). Сервер является хранилищем для одного или нескольких классов. Каждый класс реализует один или несколько интерфейсов.

Каждый интерфейс определяет один или несколько методов и обязательно наследует от стандартного интерфейса IUnknown (прямо или косвенно).

Определим вначале абстрактный базовый интерфейс IPub (публикация), от которого далее будут порождены интерфейсы IBook и IJournal (книга и журнал). Здесь надо отметить, что в СОМ нет множественного наследования интерфейсов и каждый пользовательский интерфейс должен порождаться от какого-либо другого интерфейса (хотя бы от IUnknown). Определение этого интерфейса IPub в виде заголовочного файла для C++ (IPub.h) приводится ниже.

// IPub.h — Базовый интерфейс публикации IPub

#ifndef _IPub_

#define _IPub_


#include // содержит все нужное для COM

DECLARE_INT E RFAC E_(IPub, IUnknown)

{

STDMETHOD(SetTitle)(BSTR bstrTitle) PURE;

STDMETHOD(SetYear)(int nYear) PURE;

STDMETHOD(Getlnfo)(BSTR * pbstrlnfo) PURE;

};

#endif


В данном примере определен интерфейс IPub, наследуемый от стандартного интерфейса IUnknown. В интерфейсе IPub определены 3 метода:

SetTitle — задание названия публикации,

SetYear — задание года публикации

Getinfo — получение информации о публикации.

При описании как самого интерфейса, так и входящих в него методов использованы следующие макросы:

#define DECLARE_INTERFACE_(ifасе, baseiface)

interface iface: public baseiface

#define STDMETHOD(method) virtual HRESULT stdcall method

#define PURE = 0

Таким образом,

DECLARE_INTERFACE_(IPub, IUnknown)

означает, что IPub есть интерфейс (то же что и struct в С), порожденный от IUnknown, а

STDMETHOD(SetTitle)(BSTR bstrTitle) PURE

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

При определении интерфейсов всегда используются чисто виртуальные функции. Это означает, что не существует реализации интерфейса. Это только контракт, никак не ограничивающий конкретную реализацию. Для использования данного интерфейса нужно определить класс, наследующий этот интерфейс (и, возможно, другие интерфейсы), и уже реализовать этот класс.

Известно, что в разных языках программирования строки организованы различным образом. В СОМ выбрано представление строки, позволяющее работать с ним в программах, написанных на различных языках. Переменная типа BSTR (BASIC String) есть строка, представленная в формате Unicode (2 байта на один символ), с завершающим нулем и с префиксом (4 байта), хранящим длину строки (что позволяет сохранять внутри строки нулевые символы). Имеется ряд функций, облегчающих работу с такими строками (которые для C++ будут продемонстрированы в последующих примерах).

И, наконец, напомним, что в реализации данного примера средствами ООП в описании класса CPublication имелся метод Display, который обеспечивал вывод информации о публикации на терминал.

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

Теперь определим интерфейс IBook, производный от IPub.

// IBook.h — интерфейс книги IBook

#ifndef _IBook_

#define _IBook_

#include "IPub.h" // IPub

DECLARE_INTERFACE_(IBook, IPub)

{

STDMETHOD(SetAuthor)(BSTR bstrAuthor) PURE;

};

#endif


Здесь просто добавляется новый метод SetAuthor, позволяющий задавать имя автора книги, которого, в общем случае, могло и не быть у публикации.

Аналогично определяется интерфейс IJournal, также производный от IPub, расширяющий последний методом SetNumber — задание номера журнала.

// IJournal.h — интерфейс журнала IJournal

#ifndef _IJournal_

#define _IJournal_

#include "IPub.h" // IPub

DEC LARE_INT E RFAC E_(IJournal, IPub)

{

STDMETHOD(SetNumber)(int nNumber) PURE;

};

#endif

В рамках модели COM каждый интерфейс должен иметь уникальный в пространстве и времени идентификатор — IID (Interface IDentifier). Идентификатор генерируется и присваивается интерфейсу при его создании и более никогда не меняется. Все реализации данного интерфейса должны использовать этот идентификатор. Пользователи обращаются к данному интерфейсу по его идентификатору. Все это позволяет не беспокоиться по поводу присваивания одного и того же имени различными разработчиками различным интерфейсам.

Существует спецификация DCE (Distributed Computing Environment — распределенная среда вычислений) от Open Software Foundation, котрая определяет UUID — Universally Unique IDentifiers (универсально уникальные идентификаторы). Эти идентификаторы формируются на основе сетевого адреса машины и точного времени, что и обеспечивает их уникальность. В СОМ эти идентификаторы получили название GUID — Globally Unique IDentifiers (глобально уникальные идентификаторы). Каждый такой идентификатор представляется 128-битным числом. В связи с тем, что не все языки программирования, поддерживающие СОМ, могут оперировать с такими большими числами, для хранения GUID используется следующая структура

typedef struct _GUID

{ unsigned long Data1;

unsigned short Data2;

unsigned short Data3;

unsigned char Data4[8];

} GUID

Эту структуру удобно задавать с помощью следующего макроса

#define DEFINE_GUID(name, \

1, w1, w2, b1, Ь2, Ь3, Ь4, Ь5, Ь6, b7, b8) \

EXTERN_C const GUID name \

= { 1, w1, w2, { b1, Ь2, Ь3, Ь4, Ь5, Ь6, b7 b8 } }

Ниже представлен файл iid.h в котором определены GUID всех трех ранее определенных интерфейсов (позже в этот файл будут добавлены и GUID классов, реализующих указанные интерфейсы). Для получения GUID можно использовать утилиту guidgen.ехе из Visual Studio. Эта утилита позволяет получить определение нового GUID в виде макроса DEFINE GUID, который можно скопировать и вставить в файл определений GUID. В закоментированных строках этого файла содержатся значения GUID в виде, удобном для просмотра человеком. Имя идентификатора интерфейса стандартно формируется следующим образом — префикс IID_, за которым следует имя интерфейса. Например, IID_IPub.

// iid.h — GUID для интерфейсов

// {9A5DE 9А0-7225-11d5-9 8С7-000001223694}

DEFINE_GUID(IID_IPub,

0x9a5de9a0, 0x7225, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


// {9A5DE9A1-7225-11d5-98C7-000001223694 }

DEFINE_GUID(IID_IBook,

0x9a5de9a1, 0x7225, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


// {9A5DE 9A2-7225-11d5-98C7-000001223694}

DEFINE_GUID(IID_IJournal,

0x9a5de9a2, 0x7225, 0xlld5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


Реализация интерфейсов — коклассы

Теперь пора перейти к реализации наших интерфейсов. В соответствии с базовой архитектурой СОМ, интерфейсы реализуются в классах, каждый из которых может реализовать несколько интерфейсов. Далее определяются и реализуются два класса — CoBоoк и CоJournal, реализующие соответственно интерфейсы IBook и IJournal. Все названия классов в СОМ удобно начинать с префикса со, подчеркивая их принадлежность данной модели. Часто классы, определенные в СОМ, называют коклассами.

Рассмотрим вначале файл CoBоок. h

//////////////////////////////////////////////////

// СоВоок. h: заголовочный файл для класс СоВоок //

//////////////////////////////////////////////////

#ifndef _СоВоок_

#define _СоВоок_

#include "IBook.h" // определение интерфейса IBook

#include "iid.h" // GUID интерфейса IBook

class CoBook:

public IBook

{

public:

CoBook(); // конструктор

virtual ~CoBook(); // деструктор

// IUnknown

STDMETHODIMP Querylnterface(REFIID riid, void** pIFace);

STDMETHODIMP_(ULONG) AddRef();

STDMETHODIMP (ULONG) Release();


// IPub

STDMETHODIMP SetTitle(BSTR bstrTitle);

STDMETHODIMP SetYear(int nYear);

STDMETHODIMP Getlnfo(BSTR* pbstrlnfo);


// IBook

STDMETHODIMP SetAuthor(BSTR bstrAuthor);


private:

ULONG m_refCount; // Счетчик ссылок

BSTR m_bstrTitle; // Название публикации

BSTR m_bstrAuthor; // Автор публикации

int m_nYear; // Год публикации

};

#endif

Здесь определяется класс CоBоок, порожденный от интерфейса IBook. Так как интерфейс IBook был сам порожден от интерфейса IPub, а последний — от стандартного интерфейса IUnknown, то класс CоBоок должен реализовать чисто виртуальные методы всех этих интерфейсов.

Здесь при определении методов используются следующие макросы

#define STDMETHODIMP HRESULT stdcall

#define STDMETHODIMP (type) type stdcall

теперь надо поговорить об основном стандартном интерфейсе модели COM — IUknown.

Как уже не раз говорилось, любой интерфейс СОМ должен порождаться от IUnknown или от другого интерфейса. Следовательно, все интерфейсы СОМ имеют общего предка — интерфейс IUnknown. Этот интерфейс определяется в и его GUID равен

00000000-0000-0000-С000-000000000046.

Интерфейс IUnknown определяет 3 метода, и все эти методы должны быть реализованы в каждом коклассе

Querylnterfасе()

Данный метод обеспечивает клиента информацией о том, реализован ли интересующий его интерфейс в данном коклассе. В случае реализованное™ запрашиваемого интерфейса клиент получает ссылку на интерфейс. Этот подход обеспечивает необходимую гибкость, позволяя работать с одним компонентам различным клиентам, имеющим различный уровень знаний о доступных интерфейсах.

Семантика этого метода следующая. Имея указатель некоторого интерфейса pIFacel некоторого объекта и передавая в Queryinterfасе ссылку на идентификатор нового интерфейса IID_IFace2 можно получить указатель на новый интерфейс piFace2, если он реализован в данном объекте. В случае успеха возвращаемое значение S_OK, а в случае неудачи — E_NOINTERFACE

hr = pIFacel->QueryInterfасе(IID_IFace2, (void**)&pIFace2);

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

Эта реализация может обеспечиваться за счет использования таблиц виртуальных функций, которыми и являются все методы всех интерфейсов (при реализации кокласса на C++). Заметим, что если кокласс порожден от независимых друг от друга интерфейсов (т. е. один интерфейс не является потомком другого), то компилятор C++ создает виртуальных таблиц — по одной на каждый интерфейс. В каждой таблице хранятся указатели на все методы всех интерфейсов, от которых порожден данный интерфейс. Следовательно, начинается каждая таблица виртуальных функций с указателей на методы интерфейса iunknown. Обратите внимание, что реализовать каждый метод нужно только один раз, не зависимо от числа его появлений в таблицах виртуальных функций данного класса.


AddRef()

В СОМ управление временем жизни объекта выполняется при использовании функций AddRef() и Reiease(). Taк как объект может использоваться многими клиентами одновременно, ни один из клиентов не имеет права удалить объект из памяти. В СОМ каждый активизированный объект ведет подсчет сделанных на него ссылок. При обнулении этого счетчика кокласс удаляет себя из памяти. Метод AddRef() вызывается при появлении новой ссылки на объект и увеличивает значение счетчика (в нашем случае это m refcount) на 1. Возвращает данный метод текущее число ссылок.


Release ()

Этот метод вызывается при освобождении ссылки на объект и уменьшает значение счетчика на 1. Он же удаляет кокласс из памяти при обнулении счетчика ссылок. Метод возвращает текущее число ссылок.


Теперь реализация класса CoBоок

//////////////////////////////////////////////////

// СоВоок. срр: реализация класса СоВоок.

//

//////////////////////////////////////////////////

#include

#include "СоВоок. h"

extern ULONG g_objCount; // Счетчик числа объектов

//////////////////////////////////////////////////

// Конструктор и деструктор

//////////////////////////////////////////////////

СоВоок::СоВоок()

{

m_refCount = 0;

++g_obj Count;

m_bstrTitle = SysAllocString(L"");

m_bstrAuthor = SysAllocString(L"");

}


CoBook::~CoBook()

{

-- g_objCount;

if(m_bstrTitle)

SysFreeString(m_bstrTitle);

if(m_bstrAuthor)

SysFreeString(m_bstrAuthor);

}


// IUnknown

STDMETHODIMP_(ULONG) CoBook::AddRef()

{

return ++m_refCount;

}

STDMETHODIMP_(ULONG) CoBook::Release()

{

if (--m_refCount == 0)

{

delete this;

return 0;

}

else

return m_refCount;

}


STDMETHODIMP CoBook::Querylnterface(REFIID riid, void** pIFace)

{

if (riid == IID_IUnknown)

{

*pIFace = (IUnknown*)this;

}


else if (riid == IID_IPub)

}

*pIFace = (IPub*)this;

}

else if (riid == IID_IBook)

{

*pIFace = (IBook*)this;

}

else

{

*pIFace = NULL; return E_NOINTERFACE;

}


((IUnknown*)(*pIFace))->AddRef();

return S OK;

}

//IPub&IBook

STDMETHODIMP CoBook::SetTitle (BSTR bstrTitle)

{

SysReAllocString(&m_bstrTitle, bstrTitle);

return S_OK;

}


STDMETHODIMP CoBook::SetYear(int nYear)

{

m_nYear = nYear;

return S_OK;

}


STDMETHODIMP CoBook::SetAuthor(BSTR bstrAuthor)

{

SysReAllocString(&m_bstrAuthor, bstrAuthor);

return S_OK;

}


STDMETHODIMP CoBook::Getlnfo(BSTR *pbstrInfo)

{

char* pszTitle = NULL;

char* pszAuthor = NULL;

char* pszText = NULL;

int nAuthorLength, nTitleLength;

nAuthorLength = SysStringLen(m_bstrAuthor);

nTitleLength = SysStringLen(m_bstrTitle);

pszAuthor = (char*)malloc(2*nAuthorLength);

pszTitle = (char*)malloc(2*nTitleLength);

pszText = (char*)malloc(2*nAuthorLength + 2*nTitleLength + 50);


wcstombs(pszTitle, m_bstrTitle, 2*nTitleLength);

wcstombs(pszAuthor, m_bstrAuthor, 2*nAuthorLength);


sprintf(pszText,

"Book\n\nAuthor: %s\nTitle: %s\nYear: %d", pszAuthor, pszTitle, m_nYear);

*pbstrInfo = SysAllocStringLen(NULL, 2*strlen(pszText));

mbstowcs (*pbstrInfo, pszText, 2*strlen (pszText));


free(pszAuthor);

free(pszTitle);

free(pszText);


return S_OK;

}


В конструкторе CoBook () задается начальное нулевое значение для счетчика числа ссылок на данный объект и увеличивается на 1 глобальный счетчик объектов. Глобальный счетчик объектов используется при принятии решения о возможности удаления из памяти сервера, возможно содержащего не только класс CоBоок, но и какие-то другие классы. Кроме того, члены класса m_bstrTitie и m_bstrAuthor получают в качестве значения пустую строку. Префикс L перед строкой константой говорит о том, что в этой строке на один символ отводится два байта. Для ее преобразования к типу BSTR и размещения в памяти используется функция SysAllocString ().

В деструкторе CоBооk () при уничтожении объекта на 1 уменьшается счетчик объектов и освобождается память, выделенная ранее для заголовка публикации и имени автора. Для освобождения памяти, выделенной под BSTR-строку, используется функция SysFreeString ().

Реализация методов AddRef () и Release () тривиальна и полность соответствует предписанной семантике этих методов.

Метод Query inter face () обеспечивает переход между любыми двумя интерфейсами, реализованными в коклассе совоок. Заметим, что среди этих интерфейсов не только IBook, но и все интерфейсы, от которых прямо или косвенно IBook порожден — IPub и IUnknown. Если запрашиваемый интерфейс не найден, возвращается E_NOINTERFACE. В противном случае вызывается метод AddRef () интерфейса IUnknown, увеличивающий счетчик ссылок на данный объект. Вызвать функцию Release () при освобождении ссылки, полученной в результате вызова QueryInterface () обязан уже клиент.

Методы SetTitle (), SetYear () и SetAuthor () позволяют задать соответственно название, год издания и имя автора книги — нового объекта CоBооk. При передачи в качестве входного аргумента BSTR-строки происходит переразмещение в памяти соответствующей BSTR-строки — члена класса. Для этого используется функция SysReAllocstring ().

Метод GetInfо () возвращает информацию о книге в строке типа BSTR. Вся информация о книге хранится в экземпляре класса CоBооk в виде BSTR-cтрок и целого числа. В процессе формирования результирующей строки все BSTR-строки преобразуются в ANSI-строки. Для выполнения преобразования BSTR-строки в ANSI-строку применяется функция wcstombs (). Первый параметр задает буфер для ANSI-строки, второй — BSTR-строку, третий — размер буфера. Функция SysStringien () позволяет определить длину BSTR-строки в символах. Размер буфера для соответствующей ANSI-строки выбирается в два раза превосходящим длину BSTR-строки, т. к. некоторые Unicode-символы представляются двумя ANSI-символами. Далее в буфер pszText помещается вся выводимая информация, выделяется память под возвращаемую BSTR-строку *pbstrInfо и вызывается функция преобразования ANSI-строки pszText в BSTR-строку *pbstrInfо-mbstowcs().

Для выделения памяти под возвращаемую строку используется функция SysAlloSstringLen (). Первый параметр задает BSTR-строку, первые символы которой, в количестве равном второму параметру, заносятся в новую строку. Задав в качестве первого параметра null мы получаем неинициализированную BSTR-строку длины, заданной вторым параметром.

Последнее действие — освобождение памяти, выделенной под буферы.

Далее определяется и реализуется класс CоJournal, порожденный от интерфейса IJournal. Так как интерфейс IJournal был сам порожден от интерфейса IPub, а последний — от стандартного интерфейса IUnknown, то класс CоJournal должен реализовать чисто виртуальные методы всех этих интерфейсов. Данный класс мало отличается от класса CоBооk, в связи с чем комментарии минимальны.

//////////////////////////////////////////////////

// СоJournal.h: заголовочный файл для класс СоJournal.

//////////////////////////////////////////////////

#ifndef _CoJournal_

#define _CoJournal_


#include "IJournal.h"

#include "iid.h"


class CoJournal:

public IJournal

{

public:

CoJournal ();

virtual ~CoJournal();


// IUnknown

STDMETHODIMP Querylnterface(REFIID riid, void** pIFace);

STDMETHODIMP_(ULONG) AddRef();

STDMETHODIMP_(ULONG) Release();


// IPub

STDMETHOD(SetTitle)(BSTR bstrTitle);

STDMETHOD(SetYear)(int nYear);

STDMETHOD(Getlnfo)(BSTR* pbstrlnfo);


// IJournal

STDMETHOD(SetNumber)(int nNumber);


private:

ULONG m_refCount; // Счетчик ссылок

BSTR m_bstrTitle; // Название публикации

int m_nYear; // Год публикации

int m nNumber; // Номер журнала

};

#endif


И теперь реализация класса CоJournal

//////////////////////////////////////////////////

// СоJournal.cpp: реализация класса CoJournal.

//////////////////////////////////////////////////

#include

#include "CoJournal.h"


#include

#include "CoJournal.h"


extern ULONG g_objCount; // Счетчик числа объектов

//////////////////////////////////////////////////

// Конструктор и деструктор

//////////////////////////////////////////////////

CoJournal::CoJournal()

{

m_refCount = 0;

++g_obj Count;

m_bstrTitle = SysAllocString(L"");

}


CoJournal::~CoJournal()

{

-- g_objCount;

if(m_b strTitle)

SysFreeString(m_bstrTitle);

}


// IUnknown

STDMETHODIMP_(ULONG) CoJournal::AddRef()

{

return ++m_refCount;

}


STDMETHODIMP_(ULONG) CoJournal::Release()

{

if (--m_refCount == 0)

{

delete this;

return 0;

}

else

return m_refCount;

}


STDMETHODIMP CoJournal::Querylnterface(REFIID riid, void** pIFace)

{

if (riid == IID_IUnknown)

{

*pIFace = (IUnknown*)this;

}

else if (riid == IID_IJournal)

{

*pIFace = (IJournal*)this;

}


else

{

*pIFace = NULL; return E_ NOINTERFACE;

}

((IUnknown*)(*pIFace)) — > AddRef();

return S_OK;

}


// IPub & IJournal

STDMETHODIMP CoJournal::SetTitle (BSTR bstrTitle)

{

SysReAllocString(&m_bstrTitle, bstrTitle);

return S_OK;

}


STDMETHODIMP CoJournal::SetYear(int nYear)

{

m_nYear = nYear;

return S_OK;

}


STDMETHODIMP CoJournal::SetNumber(int nNumber)

{

m_nNumber = nNumber;

return S_OK;

}


STDMETHODIMP CoJournal::GetInfo(BSTR *pbstrInfo)

{

char* pszTitle = NULL;

char* pszText = NULL;

int nTitleLength;


nTitleLength = SysStringLen(m_bstrTitle);

pszTitle = (char*)malloc(2*nTitleLength);

pszText = (char*)malloc(2*nTitleLength + 50);


wcstombs(pszTitle, m_bstrTitle, 2*nTitleLength);


sprintf(pszText, "Journal\n\nTitle: %s\nYear: %d\nNumber: %d\n", pszTitle, m_nYear, m_nNumber);

*pbstrInfo = SysAllocStringLen(NULL, 2*strlen(pszText));

mbstowcs(*pbstrlnfo, pszText, 2*strlen(pszText));


free(pszTitle);

free(pszText);


return S_OK;

}


Теперь, когда коклассы CoBook и CоJournal реализованы, им следует присвовить GUID и добавить соответствующую информацию в файл iid.h. Вот последняя версия этого файла

// GUID для всех интерфейсов и классов

// {9A5DE 9А0-7225-11d5-9 8С7-000001223694}

DEFINE_GUID(IID_IPub,

0x9a5de9a0, 0x7225, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


// {9A5DE 9A1-7225-11d5-98C7-000001223694}

DEFINE_GUID(IID_IBook,

0x9a5de9al, 0x7225, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


// {9A5DE 9A2-7 22 5-11d5-9 8C7-0 000 0122 3 69 4}

DEFINE_GUID(IID_IJournal,

0x9a5de9a2, 0x7225, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


// {49F00760-7238-11d5-98C7-000001223694}

DEFINE_GUID(CLSID_CoBook,

0x4 9f007 60, 0x7238, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


// {49F00761-7238-11d5-98C7-000001223694}

DEFINE_GUID(CLSID_CoJournal,

0x4 9f007 61, 0x7238, 0x11d5,

0x98, 0xc7, 0x0, 0x0, 0x1, 0x22, 0x36, 0x94);


Обратите внимание, что имя идентификатора кокласса стандартно формируется следующим образом — префикс CLSID_, за которым следует имя кокласса. Например, CLSID_CoBook.


Фабрики классов

Теперь возникает важный вопрос — как клиент может создавать СОМ объекты CoBook и CoJournal?

Два важных принципа СОМ:

Независимость от языка

В соответствии с идеологией модели СОМ, классы СОМ (коклассы) могут реализовываться на различных языках и должны храниться в бинарном виде (dll или ехе файл). Клиент может быть реализован на любом из поддерживающих модель СОМ языке и, следовательно, должен иметь возможность создавать (активизировать) СОМ объект не используя каких-либо специфических для данного языка методов, а с помощью специального стандартного интерфейса.

Прозрачность местоположения сервера

Имеется три места, где может размещаться СОМ сервер: о В процессе клиента

Этот способ обеспечивает самую быструю связь клиента и сервера (который реализуется в виде dll). Но есть и проблемы. Например, при ошибке в сервере "вылетает" весь процесс (т. е. и клиент), о Вне процесса клиента, но на одной с ним машине

Это так называемая локальная связь (обычно используется ехе-сервер). Она обеспечивает надежность (при ошибках в сервере клиент не гибнет), но необходима организация передачи данных между клиентом и сервером через границы процессов. Это СОМ берет на себя, обеспечивая кодирование, передачу и декодирование данных. При этом создаются прокси объект в процессе клиента и заглушка в процессе сервера. Прокси имитирует для клиента сервер, а заглушка имитирует для сервера клиента. В связи с этим код клиента не зависит от того, где располагается сервер. Все преобразование данных и их пересылка осуществляется парой прокси-заглушка. Коммутация прокси и заглушки основана на протоколе упрощенного удаленного вызова процедур (LRPC–Lightweight Remote Procedure Call).

На удаленной машине

При этом связь наиболее медленная. Архитектура похожа на архитектуру, описанную в предыдущем пункте, только вместо LRPC используется RPC — протокол удаленного вызова процедур.

Заметим, что клиент не обязан знать, где именно расположен используемый сервер. Код клиента не меняется при изменении местоположения сервера. Более того, сервер может располагаться в нескольких местах, и по просьбе клиента система (менеджер управления сервисом), может устанавливать связь клиента с ближайшей к нему копией сервера.

Итак, нужен стандартный интерфейс для активации СОМ объекта. Такой интерфейс в СОМ имеется и называется IClassFactory (определен в ). Дня каждого кокласса, "живущего" в некотором сервере, в этом же сервере должен "жить" класс, реализующий так называемую фабрику класса (или объект класса). Именно фабрика класса предоставляет клиенту интерфейс IClassFactory и более никаких других интерфейсов (кроме, естественно, IUnknown, от которого IClassFactory порожден как и все остальные СОМ интерфейсы).

Интерфейс IClassFactory имеет два метода:

Createlnstance

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

LockServer

Дня активации какого-либо СОМ объекта необходимо размещение сервера, в котором "живет" данный объект, в оперативной памяти. Каждый объект ведет счетчик ссылок на себя и автоматически удаляет себя из памяти, когда ссылок больше нет. Если нет активных объектов, "живущих" в сервере, то сервер также может быть удален из памяти. Но клиент может этому восприпятствовать, заблокировав сервер в памяти для более быстрой активации объектов в будущем. Для этого он и может вызвать метод LockServer.

Далее для определенности будем рассматривать построение dll-сервера (сервера в процессе клиента). Естественно, этот выбор никак не скажется на коде клиента. Построение локального сервера (сервер вне процесса клиента, но на этой же машине) будет рассмотрено позже.

Следующий код содержит определение фабрики класса для кокласса совоок. /

/////////////////////////////////////////////////

// CoBookFactory.h: заголовочный файл для класса CoBookFactory —

// фабрика класса для класса CoBook

//////////////////////////////////////////////////


#ifndef _CoBookFactory_

#define _CoBookFactory_


#include "CoBook.h"


class CoBookFactory: public iciassFactory

{

public:

// Конструктор и деструктор

CoBookFactory ();

virtual ~CoBookFactory();


//IUnknown

STDMETHODIMP Querylnterface(REFIID riid, void** pIFace);

STDMETHODIMP_(ULONG)AddRef();

STDMETHODIMP_(ULONG)Release();


//IClassFactory

STDMETHODIMP LockServer(BOOL fLock);

STDMETHODIMP Createlnstance(LPUNKNOWN pUnkOuter,

REFIID riid, void** ppv);


private:

ULONG m_refCount; // Счетчик числа сылок на объект

};


#endif


Здесь стандартным образом объявлены конструктор, деструктор, три метода интерфейса IUnknown и два метода интерфейса IClassFactory. Семантика всех этих методов уже была изложена ранее. Дополнительно нужно пояснить смысл параметров методов LockServer и Createlnstance.

Параметр fLock принимает значение TRUE — заблокировать сервер, или FALSE — разблокировать сервер. Сервер ведет подсчет всех блокировок, установленных всеми его клиентами, и разрешает выгрузить себя из памяти, если только не осталось активных объектов и общий счет блокировок в памяти равен нулю.

Первый параметр метода CreateInstance используется только при агрегации, которая обсуждалась ранее. Именно, если класс CоBооk проектируется с возможностью его агрегации в другой СОМ класс (класс CоBооk готов предоставить возможность другому классу выдавать интерфейсы класса v за свои), то через параметр pUnkUuter (тип которого определяется в как typedef iunknown RPC_FAR * LPUKNOWN) он получает указатель интерфейса IUnknown того объекта, который его собрался агрегировать. Если CоBооk не проектировался с возможностью агрегации (так и есть в нашем случае), то он ожидает получение в этом параметре значения NULL, в противном случае тут будет выдана ошибка СLASS_E_NOAGGREGATION.

Второй параметр метода CreateInstance задает ссылку на идентификатор желаемого интерфейса, а третий возвращает запрошенный интерфейс (если он имеется).

Реализация фабрики класса CoBookFactory для класса CоBооk представлена ниже.

//////////////////////////////////////////////////

// CoBookFactory.срр: реализация класса CoBookFactory —

// фабрики класса для класса CoBook.

//////////////////////////////////////////////////

#include "CoBookFactory.h"


extern ULONG g_lockCount; // Счетчик блокировок сервера в памяти extern ULONG

g_objCount; // Счетчик активных объектов


//////////////////////////////////////////////////

// Конструктор и деструктор

//////////////////////////////////////////////////

CoBookFactory::CoBookFactory()

{

m_refCount = 0;

g_objCount++;


}

CoBookFactory::~CoBookFactory()

{

g_obj Count--;

}


//IUnknown


STDMETHODIMP_(ULONG) CoBookFactory::AddRef()

{

return ++m_refCount;

}


STDMETHODIMP_(ULONG) CoBookFactory::Release()

{

if (--m_refCount == 0)

{

delete this;

return 0;

}

else

return m_refCount;

}


STDMETHODIMP CoBookFactory::Querylnterface(REFIID riid, void** ppv)

{

if(riid == IID_IUnknown)

{

*ppv = (IUnknown*)this;

}

else if(riid == IID_IClassFactory)

{

*ppv = (IClassFactory*) this;

{

else

{

*ppv = NULL;

return E NOINTERFACE;

}


((IUnknown*)(*ppv)) — > AddRef();

return S OK;

}


//ICiassFactory

STDMETHODIMP CoBookFactory::Createlnstance(

LPUNKNOWN pUnkOuter, REFIID riid, void** ppv)

{

if(pUnkOuter!= NULL)

{

return СLASS_E_NOAGGREGATION;

}


CoBook* pBookObj = NULL;

HRESULT hr;


pBookObj = new CoBook;

hr = pBookObj —> QueryInterface (riid, ppv);


if (FAILED(hr))

delete pBookObj;

return hr;

}


STDMETHODIMP CoBookFactory::LockServer(BOOL fLock)

{

if (fLock)

++g_lockCount;

else

--g_lockCount;

return S_OK;

}


В конструкторе обнуляется счетчик ссылок на фабрику класса — член класса m ref count (как всегда при создании любого объекта СОМ) и увеличивается на единицу глобальный счетчик числа активных объектов данного сервера (g objCount).

В деструкторе на единицу уменьшается глобальный счетчик активных объектов данного сервера.

Реализация методов интерфейса IUnknown стандартная и уже обсуждалась ранее.

Перейдем теперь к обсуждению реализации методов интерфейса IClassFactory и начнем с метода CreateInstance.

Во-первых, не предполагается агрегация класса CоBооk. В связи с этим выдается ошибка CLASS_E_NOAGGREGATION, если первый параметр не равен NULL.

В следующих строках кода и проявляется связь данной фабрики класса именно с классом CоBооk. Создается экземпляр класса CоBооk и запрашивается указатель на некоторый интерфейс этого класса:

CoBook* pBookObj = NULL;

HRESULT hr;

pBookObj = new CoBook;

hr = pBookObj — > QueryInterface(riid, ppv);

При неудаче получения интерфейса, созданный объект уничтожается.

В методе LockServer глобальный счетчик блокировок сервера в памяти увеличивается или уменьшается на единицу в зависимости от значения параметра fLock.

Совершенно аналогично определяется и реализуется фабрика класса CoJournalFactory для кокласса СоJournal.

//////////////////////////////////////////////////

// СоJournalFactory.h: заголовочный файл для класса

// CoJournaiFactory — фабрики класса для класса CoJournal

//////////////////////////////////////////////////

#ifndef _CoJournalFactory_

#define _CoJournalFactory

#include "CoJournal.h"


class CoJournalFactory: public IClassFactory

{

public:

CoJournalFactory ();

virtual ~CoJournalFactory();


//IUnknown

STDMETHODIMP Querylnterface(REFIID riid, void** pIFace);

STDMETHODIMP_(ULONG)AddRef();

STDMETHODIMP_(ULONG)Release();


//IClassFactory

STDMETHODIMP LockServer(BOOL fLock);

STDMETHODIMP Createlnstance(LPUNKNOWN pUnkOuter,

REFIID riid, void** ppv);


private:

ULONG m_refCount;

};

#endif


И теперь реализация фабрики класса СоJournalFactory для класса CoJournal.

//////////////////////////////////////////////////

// СоJournalFactory.срр: реализация фабрики класса СоJournalFactory

// для кокласса CoJournal

//////////////////////////////////////////////////

#include "СоJournalFactory.h"


extern ULONG g_lockCount;

extern ULONG g_objCount;

//////////////////////////////////////////////////

// Конструктор и деструктор

//////////////////////////////////////////////////

CoJournalFactory::СоJournalFactory ()

{

m_refCount = 0;

g_objCount++;

}


CoJournalFactory::~CoJournalFactory()

{

g_obj Count-;

}


//IUnknown


STDMETHODIMP_(ULONG) CoJournalFactory::AddRef() {

return ++m_refCount;

}


STDMETHODIMP_(ULONG) CoJournalFactory::Release()

{

if (-m_refCount == 0)

{

delete this;

return 0;

}

else

return m refCount;

}


STDMETHODIMP CoJournalFactory::Querylnterface(REFIID riid, void** ppv)

{

if(riid == IID_IUnknown)

{

*ppv = (IUnknown*)this;

}

else if(riid == IID_IClassFactory)

{

*ppv = (IdassFactory*) this;

}

else

{

*ppv = NULL;

return E_NOINTERFACE;

}


(IUnknown*)(*ppv)) — >AddRef();

return S OK;

}


//IClassFactory

STDMETHODIMP CoJournaiFactory::Createlnstance(

LPUNKNOWN pUnkOuter, REFIID riid, void** ppv)

{

if(pUnkOuter!= NULL)

{

return СLASS_E_NOAGGREGATION;

}


CoJournal* pJournalObj = NULL;

HRESULT hr;


pJournalObj = new CoJournal;


hr = pJournalObj — > QueryInterface(riid, ppv);


if (FAILED(hr))

delete pJournalObj;

return hr;

}


STDMETHODIMP CoJournalFactory::LockServer(BOOL fLock)

{

if (fLock)

++g_lockCount;

else

--g_lockCount;

return S OK;

}

Заканчивая разговор о фабрике класса нужно заметить, что для нее не нужно задавать GUID.


Сервер в процессе клиента

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

Все, что достаточно знать клиенту о коклассе, это его глобальный идентификатор (GUID). Зная этот идентификатор, клиент может воспользоваться функцией CoGetClassObject, которая заставит менеджер управления сервисом найти и загрузить нужный сервер (в котором "живет" нужный кокласс), активизировать фабрику класса для этого кокласса и возвратить клиенту указатель на интерфейс IClassFactory.

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

Далее работа менеджера зависит от типа сервера.

Пока мы строим сервер в процессе клиента, реализуемый в виде dll. Для этого, при работе в Visual C++, можно создать пустое рабочее пространство проекта Win32 DLL с именем PubInPrосServer. В этом проекте нужно создать (или перенести созданные в другом месте) классы CoBook, CoJournal, CoBookFactory, СоJournalFactory. Там же ДОЛЖНЫ находиться файлы с определениями интерфейсов IPub, IBook, IJournal, файл с GUID для всех интерфейсов и коклассов iid.h и (как советует Э.Трельсен в книге [3]) следующий файл iid.cpp

// iid.cpp: гарантирует автоматический вызов

// перед "iid.h"


#include

#include

#include "iid.h"


Теперь нужно создать файл, имя которого совпадает с именем проекта — PublnProcServer.cpp:

#include "CoBookFactory.h"

#include "CoJournalFactory.h"


ULONG g_lockCount = 0;

ULONG g_objCount = 0;


STDAPI DllCanUnloadNow()

{

if (g_lockCount == 0 && g_objCount == 0)

return S_OK;

else

return S_FALSE;

}


STDAPI DllGetClassObject(REFCLSID rclsid,

REFIID riid, void** ppv)

{

HRESULT hr;

CoBookFactory* pBookFact = NULL;

CoJournaiFactory* pJournalFact = NULL;


if (rclsid == CLSID_CoBook)

{

pBookFact = new CoBookFactory;

hr = pBookFact —> QueryInterface(riid, ppv);

if (FAILED(hr))

delete pBookFact;

return hr;

}

else if (rclsid == CLSID_CoJournal)

{

pJournalFact = new CoJournaiFactory;

hr = pJournalFact->QueryInterface(riid, ppv);

if (FAILED(hr))

delete pJournalFact;

return hr;

}

else

return СLASS_E_CLASSNOTAVAILABLE;

}


В файле PubInProcServer.cpp определяются и инициализируются нулем две глобальные переменные g_lockCount, g_objCount — соответственно счетчики числа блокировок сервера в памяти и числа активных объектов.

Далее дается реализация двух экспортируемых из dll функций — DllGetClassObject и DllCanUnioadNow. Используя первую, менеджер управления сервисом (из СОМ) сможет активизировать фабрику класса запрашиваемого кокласса и получить указатель на интерфейс ICiassFactory соответствующей фабрики класса. Вторая функция используется менеджером на этапе принятия решения об удалении сервера из памяти.

Рассмотрим подробнее реализацию функции DllGetClassObject.

Эта функция получает в качестве аргументов ссылку на GUID нужного кокласса, ссылку на желаемый интерфейс фабрики класса этого кокласса (обычно IID_IClassFactory) и в качестве выходного параметра возвращает указатель на запрашиваемый интерфейс. Возвращаемое функцией значение типа HRESULT говорит об успехе или неудаче операции. В последнем случае можно выяснить причину неудачи. Дня проверки успешности операции можно использовать определенные в СОМ макроопределения SUCCEEDED() и FAILED(), анализирующие старший бит возвращенного значения типа HRESULT.

Реализация функции DllCanUnioadNow тривиальна.

Обычно dll Сервер Экспортирует еще две функции: DllRegisterServer и DllUnregisterServer. Эти функиции должны обеспечить саморегистрацию сервера в реестре. В данном курсе этот вопрос не рассматривается, и регистрация разрабатываемого сервера будет проведена вручную с помощью редактора реестра.

Далее надо включить в проект стандартный DEF-файл PubInProcServer.def, который перечисляет экспортируемые функции:

LIBRARY "PUBINPROCSERVER"

EXPORTS

DllGetClassObject @1 PRIVATE

DllCanUnloadNow @2 PRIVATE

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


Регистрация сервера в реестре

Теперь сервер готов, но для его использования необходимо внести некоторые данные в реестр системы. Сделаем это, используя редактор реестра — regedit.ехе. Для внесения минимальной информации о сервере внесем в реестр следующие данные:

CLSID и соответствующие ProgID и путь к серверу

Программный идентификатор (ProgID) используется некоторыми программами как некоторая замена CLSID (требуется только локальная уникальность). Формат для ProgID

имя_сервера. имя_кокласса. Для CoBook это PuInProcServer.CoBook.

Под ключом HKEY_CLASSES_ROOT нужно найти ключ CLSID и под ним создать раздел {49F00760-7238-11d5-98C7-000001223694}. Это CLSID для кокласса CoBook. Для задания соответствующего ProgID следует для данного раздела создать подраздел с именем ProgID и в качестве значения для параметра по умолчанию взять PubInProcServer.CoBook.

Аналогично, под ключом HKEY_CLASSES_ROOT CLSID нужно еще создать раздел {49F00761-7238-11d5-98C7-000001223694}. Это CLSID ДЛЯ кокласса CoJournal. Для задания соответствующего ProgID следует для данного раздела создать подраздел с именем ProgID и в качестве значения для параметра по умолчанию взять PubInProcServer.CoJournal.

Для задания пути к серверу (один сервер для двух коклассов), надо под ключом

НКЕY_CLASSЕS_ROOT CLSID {49F00760-7238-11d5-98С7-000001223694} создать раздел InProcServer32 и в качестве параметра по умолчанию задать полный путь к dll серверу.

Это же повторяется для CLSID кокласса CoJournal.


ProgID и соответствующий CLSID

Под ключом НКЕY_CLASSЕS_ROOT надо создать разделы PubInProcServer.CoBook и PubInProcServer.CoJournal. Для каждого из построенных разделов создать подразделы CLSID, где в качестве значения параметра по умолчанию задаются CLSID соответствующих коклассов.


Клиент

Теперь можно реализовать клиента для сервера PubInProcServer. Для этого можно в Visual C++ создать проект консольного приложения PubClient, куда перенести файлы iid.h, iid.cpp и все файлы с описаниями интерфейсов. Тем самым, клиент должен знать все GUID используемых коклассов и их интерфейсов (не обязательно всех в данном классе, но всех, используемых данным клиентом).

Сам клиент реализован в файле PubClient.cpp

// PubClient.cpp — клиент для сервера PubinProcServer

#include "IBook.h"

#include "IJournal.h"

#include "iid.h"

#include


define MAX_ID 100 // максимальное число публикаций

int main()

{

CoInitialize(NULL); // инициализация COM


IClassFactory* pBF = NULL;

IClassFactory* pJF = NULL;

IBook* pIBook = NULL;

IJournal* pIJournal = NULL;

BSTR bstr;

char* pszText;

HRESULT hr;

int nNewID = 0;

IPub* alPub[MAX_ID]; // массив указателей на публикации


bstr = SysAllocString(L"");


// Активация фабрики класса CoBookFactory и получение указателя на интерфейс IClassFactory этой фабрики (pBF)

hr = CoGetClassObject(CLSID_CoBook, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**)&pBF);

if(FAILED(hr)) // в случае неудачи — выход

return 0;


// Активация фабрики класса CoJournaiFactory и получение указателя на интерфейс IClassFactory этой фабрики (pJF)

hr = CoGetClassObject(CLSID_CoJournal, CLSCTX_INPROC_SERVER,

NULL, IID_IClassFactory, (void**)&pJF);

if(FAILED(hr)) // в случае неудачи — выход

return 0;


// Активация нового экземпляра кокласса CoBook

hr = pBF —> CreateInstance(NULL, IID_IBook, (void**)SpIBook);

// В случае успеха — ввод данных

if (SUCCEEDED(hr))

{


SysReAllocString(&bstr, L" A.W.Troelsen");

pIBook — > SetAuthor(bstr);


SysReAllocString(&bstr, L" COM and ATL 3.0");

pIBook -> SetTitle(bstr);


pIBook —> SetYear(2000);


alPub [nNewID+ +] = pIBook;

}


// Активация нового экземпляра кокласса CoJournal

hr = pJF —> CreateInstance(NULL, IID_IJournal, (void**)SpIJournal);


// В случае успеха — ввод данных

if (SUCCEEDED(hr))

{

SysReAllocString(&bstr, L" The Journal of the Graph Theory");

pIJournal —> SetTitle(bstr);

pIJournal —> SetYear(2001);

pIJournal —> SetNumber(1);


aIPub[nNewID++] = pIJournal;

}


// Активация нового экземпляра кокласса CoJournal

hr = pJF->CreateInstance(NULL, IID_IJournal, (void**)SpIJournal);


// В случае успеха — ввод данных

if (SUCCEEDED(hr))

{

SysReAllocString(&bstr, L" SIGIR");

pIJournal —> SetTitle(bstr);

pIJournal —> SetYear (1999);

pIJournal —> SetNumber(12);

aIPub[nNewID++] = pIJournal;

}


// Удаление фабрик классов

pBF —> Release ();

pJF->Release ();


// Вывод информации о всех публикациях

if(nNewID)

for(int id = 0; id < nNewID; id++)

{

SysFreeString (bstr);

aIPub[id] — > GetInfo(&bstr);

pszText = (char*)malloc(2*SysStringLen(bstr));

wcstombs(pszText, bstr, 2*SysStringLen(bstr));

MessageBox (NULL, pszText, "Publication",

MB OK|MB SETFOREGROUND);

free(pszText);

}


// Удаление всех публикаций

if(nNewID)

for(int id = 0; id < nNewID; id++)

{

aIPub[id] — > Release ();

}


SysFreeString(bstr);

CoUninitialize (); // Завершение работы с COM

return 0;

}


Несколько замечаний к приведенной программе.

СОМ функция CoGetClassObject имеет следующие параметры:

• Ссылка на идентификатор создаваемого кокласса

• Тип запрашиваемого сервера CLSCTX_INPROC_SERVER означает, что запрашивается сервер в процессе клиента. Дня локального сервера надо задать СLSСТX_LОСAL_SЕRVER, и для удаленного — CLSCTX_REMOTE_SERVER. Можно комбинировать эти флаги для автоматического выбора наиболее близкого сервера.

• В третьем параметре задается информация об удаленной машине при использовании удаленного сервера.

• Идентификатор запрашиваемого интерфейса

• Возвращаемый указатель на запрашиваемый интерфейс

Функция MessageBox() используется для вывода информации о публикации в окне сообщений. Эту информация клиент получает с сервера вызывав метод GetInfо(). Память под возвращаемую BSTR-строку сервер выделяет сам, а клиент отвечает за ее освобождение. Перед передачей информации в окно сообщений выполняется преобразование BSTR-строки в ANSI-строку.


Язык описания интерфейсов и библиотека типов

Теперь обратимся к вопросам о языковой независимости и прозрачности местоположения. Как уже ранее упоминалось, это принципиальные для СОМ требования.

Первое означает, что как компоненты, так и использующие их клиенты могут реализовываться на любом поддерживающем СОМ языке программирования (в частности, C++ и Visual Basic). Однако описанные ранее интерфейсы (файлы IPub.h, IBook.h и IJoumal.h) представлены на C++. Это означает, что любой разработчик, желающий реализовать эти интерфейсы в своем коклассе, должен использовать именно C++. Более неприятно, что и любой клиент, желающий использовать построенные таким образом компоненты, также должен быть написан на C++.

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

Что нужно для обеспечения независимости от языка и прозрачности местоположения? Нужно описать все интерфейсы и классы некоторым стандартным образом, сделав эти описания доступными компиляторам со всех языков, поддерживающих СОМ. Это и обеспечит языковую независимость. Дня обеспечения прозрачности местоположения нужно иметь возможность автоматически формировать посредников, обеспечивающих взаимодействие клиента и сервера, выполняющихся в различных процессах. В СОМ такой посредник на стороне клиента называется прокси, а посредник на стороне сервера называется заглушкой. Они реализуются в виде компонент, загружаемых в адресные пространства клиента и сервера и обеспечивают для последних иллюзию непосредственного взаимодействия друг с другом (в одном адресном пространстве). В действительности прокси и заглушка реализуют удаленный вызов процедур, используя протокол LRPC или RPC соответственно при расположении на одной или различных машинах.

Итак, C++ не годится для описания интерфейсов в связи с тем, что в этом языке много неоднозначностей, которые, конечно, успешно разрешаются компилятором с C++, но в которых не смогут разобраться компиляторы с других языков. Нужен специальный язык, на котором можно однозначно описать все интерфейсы и классы. Это язык описания интерфейсов IDL–Interface Definition Language, первоначально описанный в спецификации DCE (Distributed Computing Environment — распределенная среда вычислений) от Open Software Foundation и далее расширенный Microsoft. На нем описываются все коклассы и все реализуемые в них интерфейсы, которые будут входить в создаваемый компонент. При этом нет необходимости описывать ранее описанные интерфейсы (например, стандартные). Достаточно включить их описания с помощью ключевого слова import. Наряду с описанием коклассов и интерфейсов надо описать так называемую библиотеку типов. Именно эта библиотека и будет решать проблему языковой независимости. Она будет в бинарном виде хранить информацию о всех интерфейсах и классах данного компонента в форме, доступной для чтения компиляторами со всех поддерживающих СОМ языков.

Далее idl-файл с описанием интерфейсов, классов и библиотеки типов транслируется транслятором MILD (Microsoft IDL) в совокупность файлов, содержащих объявления на С++/С всех интерфейсов, определения GUID для всех интерфейсов и классов, коды прокси и заглушки, а также формируется в бинарном виде библиотека типов. Используя эти файлы можно построить dll для прокси и заглушки. При реализации клиента или сервера на С++/С можно использовать полученные файлы с описаниями интерфейсов и определениями GUID, при использовании других языков программирования, будет использоваться библиотека типов.

Далее приведен файл PubinProcServerTypeInfo.idi, дающий описания на IDL интерфейсов, классов и библиотеки типов для проекта PubInProcServer.

import "oaidl.idl";

//IPub

[obj ect,

uuid(9A5DE9A0-7225-11d5-98C7-000001223694),

helpstring("Base publication")]

interface IPub: IUnknown

{

HRESULT SetTitle([in] BSTR bstrTitle);

HRESULT SetYear([in] int nYear);

HRESULT Getlnfo([out, retval] BSTR* pbstrlnfo);

};


//IBook

[object,

uuid(9A5DE9A1-7225-11d5-98C7-000001223694),

helpstring("Book")]

interface IBook: IPub

{

HRESULT SetAuthor([in] BSTR bstrAuthor);

}


//IJournal

[object,

uuid(9A5DE9A2-7225-11d5-98C7-000001223694),

helpstring("Journal")]

interface IJournal: IPub

{

HRESULT SetNumber([in] int nNumber);

}

[uuid(68A702C2-8283-11d5-98C7-000001223694),

version(1.0),

helpstring("PubinProcServer with TypeLib")]

library PubinProcServer

{

importlib("stdole32.tlb");

[uuid(49F00760-7238-11d5-98C7-000001223694)]

coclass CoBook

{

[default] interface IBook;

{;

[uuid(49F00761-7238-11d5-98C7-000001223694)]

coclass CoJournal

{

[default] interface IJournal;

};

};


Видно, что данный файл весьма похож на заголовочный файл в C++. Основное отличие — наличие атрибутов в квадратных скобках.

Конструкция import "oaidi. idi"; обеспечивает импорт ряда стандартных описаний (в том числе, интерфейса IUnknown). Далее идут описания всех реализуемых в компоненте пользовательских интерфейсов: IPub, IBook и IJournal.

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

Атрибут uuid используется для задания GUID соответствующего интерфейса.

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

При описании методов интерфейсов используются атрибуты in, out, retval, позволяющие указать направление передачи значения параметра. Атрибут in назначается по умолчанию и означает, что значение данного параметра передается серверу. Напротив, атрибут out говорит о необходимости передачи значения параметра клиенту. Атрибут retval означает, что для данный параметр является возвращаемым значением для соответствующей функции при использовании, например, Visual Basic. Указание этих атрибутов (возможны их сочетания) оптимизирует трафик в сети при выполнении удаленного вызова процедур.

После описания всех интерфейсов описывается библиотека типов (ключевое слово library). В качестве атрибутов для библиотеки типов задаются уникальный идентификатор (GUID, который можно получить с помощью guidgen.exe), номер версии и helpstring.

Оператор importlib ("stdole32.tlb") должен быть первым среди всех операторов библиотеки. Он задает импорт стандартной двоичной библиотеки типов stdole32.tlb.

Далее описываются все коклассы, включаемые в данный компонент. Для каждого кокласса с помощью атрибута uuid задается его GUID и перечисляются все его интерфейсы (последние в иерархии интерфейсов). Один из интерфейсов каждого кокласса объявляется как интерфейс по умолчанию (с помощью атрибута default). Это необходимо для Visual Basic. При инициализации экземпляра данного класса именно ссылка на интерфейс по умолчанию будет возвращена клиенту. При отсутствии атрибута default, интерфейсом по умолчанию становится первый интерфейс в списке интерфейсов данного кокласса.

В данном примере отражены далеко не все возможности языка IDL и некоторые из них будут обсуждены далее. А сейчас рассмотрим использование сформированного idl-файла.

Добавим файл PubInProcServerTypeInfo.idl В проект PubInProcServer и откомпилируем. В результате будут получены несколько файлов. Для сервера в процессе клиента, который мы реализуем в данное время, прокси и заглушка не нужны. В связи с этим включим в проект из всех файлов, сгенерированных транслятором MILD, только PubInProcServerTypeInfo.h, содержащий определения всех интерфейсов на С++/С, и файл PubInProcServerTypeInfo_i.с, содержащий определения уникальных идентификаторов для всех интерфейсов и коклассов.

Далее надо модифицировать часть файлов проекта. Прежде всего из проекта удаляются все ранее использовавшиеся файлы, определявшие интерфейсы и GUID. Это файлы IPub.h, IBook,h, IJournal.h, iid.h, iid.cpp. Во всех оставшихся файлах надо удалить ссылки на удаленные файлы. В файлы CоBооk.h, CоJournal.h надо добавить строку

#include "PubInProcServerTypeInfo.h"

Аналогичные изменения необходимо сделать в клиенте CoPubClient.

Последнее действие — добавление новых данных в реестр. Именно, надо зарегистрировать библиотеку типов — PubInProcServerTypeInfo.tlb. Для этого в реестр системы необходимо внести следующую информацию:

• Под ключом HKEY_CLASSES_ROOT TypeLib надо создать раздел

{68A702C2-8283-11d5-98C7-000001223694} — GUID библиотеки ТИПОВ.

• Для нового раздела создать подраздел 1.0 — номер версии. В качестве значения параметра по умолчанию можно взять название компонента — PubInProcServer.

• Для только что созданного раздела 1.0 создать подраздел 0 — идентификатор языка (естественного).

• Для только что созданного раздела 0 создать 3 подраздела:

Win32 — в качестве значения параметра по умолчанию надо указать путь к библиотеке типов (к файлу PubInProcServerTypeInfo.tlb)

FLAGS — значение параметра по умолчанию ''0"

HELPSTRING — значение параметра по умолчанию — путь к файлу помощи (если таковой имеется)

• И последнее, ссылку на библиотеку типов надо разместить под ключами

НКЕY_CLASSЕS_ROOT CLSID {49F00760-7238-11d5-98С7-000001223694} и

НКЕY_CLASSЕS_ROOT CLSID {49F00761-7238-11d5-98C7-000001223694}.

Для этого под указанными ключами создаются разделы TypeLib, где в качестве значения параметра по умолчанию указывается GUID библиотеки типов для компонента, в котором размещены оба класса.

Теперь компонент, созданные в проекте PubInProcServer, включает библиотеку типов PubInPrосServerTуреInfj.tlb и, следовательно, может использоваться клиентами, реализованными на других языках, поддерживающих СОМ. Например, в Visual Basic достаточно установить ссылку на библиотеку типов нашего компонента и можно создавать экземпляры коклассов CоBооk и CоJournal, получая по умолчанию ссылки на интерфейсы IBook и IJournal.

Для проверки того, что сформированная библиотека типов PubInPrосServerTуреInfj.tlb действительно содержит всю информацию, содержавшуюся ранее в idl-файле PubIinProcServerTypeInfo.idl, можно воспользоваться OLE/COM Object Viewer, С ПОМОЩЬЮ которого МОЖНО увидеть все классы ИЗ компонента PubinProcServer, их интерфейсы, методы этих интерфейсов и их сигнатуры.

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

Библиотека типов доступна для чтения не только OLE/COM Object Viewer. Имеются интерфейсы ITypeLib и ITypeInfо с большим числом методов, с помощью которых можно получить любую информацию из зарегистрированной в реестре библиотеке типов.

Интерфейс ITypeLib используется при получении информации общего характера о библиотеке типов. Интерфейс ITypeInfо позволяет получить информацию о каждом отдельном объекте, описанном в библиотеке типов.

Рассмотрим следующую задачу — по заданному GUID библиотеки типов вывести описание (helpstring) для всех описанных в библиотеке объектов.

// test.срр — чтение библиотеки типов PublnProcServerTypelnfо. tlb

#include

#include


const GUID LIBID_PubInProcServer =

{0x68A7 02C2, 0x828 3, 0x11D5,

{0x98, 0xC7, 0x00, 0x00, 0x01, 0x22, 0x36, 0x94}};

int main()

{

int count;

HRESULT hr;

char* pszName, *pszDoc;

BSTR bstrName, bstrDocString;

ITypeLib* pTypeLib;


CoInitialize(NULL);


hr=LoadRegTypeLib(LIBID_PubInProcServer, 1, 0, LANG_NEUTRAL, &pTypeLib);


if(SUCCEEDED(hr))

{

count = pTypeLib —> GetTypeInfoCount();


for (int index=-1; index < count; index++)

{

hr = pTypeLib —> GetDocumentation(index,&bstrName, &bstrDocString, NULL, NULL);

if (SUCCEEDED(hr))

{

pszName = (char*)malloc(2*SysStringLen(bstrName));

wcstombs(pszName, bstrName, 2*SysStringLen(bstrName));

cout << index << ": " << pszName;

free(pszName);

if (SysStringLen(bstrDocString) > 0)

{

pszDoc = (char*)malloc(2*SysStringLen(bstrDocString));

wcstombs(pszDoc, bstrDocString, 2*SysStringLen(bstrDocString));

cout <<": " << pszDoc;

free(pszDoc);

}

cout << endl;

SysFreeString(bstrName);

SysFreeString(bstrDocString);

}

}

pTypeLib — > Release ();

}

CoUnitialize ();

return 0;

}


Для определенности рассматривается библиотека типов, сформированная по IDL-файлу PubInProcServerTypeInfo.idl. В константе LIBID_PubinProcServer задается GUID данной библиотеки типов.

Функция LoadRegTypeLib () загружает библиотеку типов с заданным GUID и, в случае успеха, в параметре pTypeLib возвращает указатель на интерфейс iTypeLib. В данном случае нам достаточно этого интерфейса, так как интересующая нас информация является информацией общего характера о библиотеке типов. Второй (старший номер версии) и третий (младший номер версии) параметры определяют ограничения на номер версии загружаемой библиотеки типов: загружается библиотека с указанной версией; если такой нет, то загружается библиотека, у которой старший номер версии равен запрашиваемому, а младший — максимальный из доступных и не меньше запрашиваемого. Если данное ограничение не выполняется, то возвращается ошибка. Четвертый параметр в данном случае указывает на то, что при построении описания библиотеки использовался нейтральный естественный язык.

В регистре всему этому соответствует ключ

НКЕ Y_CLASSЕS_ROOT TipeLib

{68A702C2-8283-22d5-98C7-000001223694} 1.0 0 Win32

значением которого является путь К файлу PubInProcServerTypeInfo.tlb.

В данной программе вызываются всего два метода из всего множества методов интерфейса ITypeLib:

GetTypeInfoCount ()

Возвращает число описаний типов в библиотеке

GetDocumentation ()

По номеру описания типа позволяет получить имя типа, его описание (helpstring), идентификатор контекста помощи и путь к файлу помощи. Подставляя NULL вместо определенного возвращаемого параметра, можно отказаться от получения соответствующей информации. Нумерация описанных в библиотеке типов начинается с нуля. Для получения информации о самой библиотеки нужно положить индекс равным — 1.

Вся строковая информация в библиотеке типов хранится в строках типа BSTR. Функция GetDocumentation сама выделяет под них память, а клиент должен ее освободить

SysFreeString(bstrName);

SysFreeString(bstrDocString);

Для перехода к ANSI строкам приходится выделять буфер подходящего размера и выполнять преобразование из BSTR в ANSI с помощью wcstombs.

Результат работы программы:

-1: PubinProcServer: PubinProcServer with TypeLib

0: CoBook

1: IBook: Book

2: IPub: Base publication

3: CoJournal

4: IJournal: Journal

Под индексом -1 приведены имя самой библиотеки типов (как она была описана в IDL) и соответствующая строка документации (helpstring).

Под индексами от 0 до 4 идут имена типов — коклассов и их интерфейсов. Строка документации приводится только для тех из них, для которых она имеется в IDL-файле.

Таким образом, имеется возможность программным путем получать доступ к документации, включенной в библиотеку типов. Следовательно, можно организовать поиск компонент с нужной функциональностью на основе задания одного или нескольких ключевых слов.


Прозрачность местоположения

DLL сервер в суррогатном процессе


Как уже не раз говорилось ранее, один из важнейших принципов СОМ — прозрачность местоположения. Иными словами, на код клиента не должно оказывать влияние конкретное местоположение сервера (конечно при условии, что этот сервер зарегистрирован в реестре системы).

Ранее мы построили и зарегистрировали сервер в процессе клиента PubInProcServer и самого клиента PubClient. Сервер, загружаемый в процесс клиента, обладает как положительными, так и отрицательными свойствами. К положительным можно прежде всего отнести быстродействие. А к отрицательным, например, неизолированность сервера от клиента, что приводит к тому, что клиент "вылетает" при сбое сервера.

В рамках архитектуры СОМ наряду с сервером в процессе клиента можно строить локальный сервер (исполняется в отдельном процессе на машине клиента) и удаленный сервер (исполняется на удаленном компьютере). При наличии уже реализованного сервера в виде сервера в процессе клиента, его можно превратить в сервер, исполняемый вне процесса клиента, загружая в специальный процесс — DLL суррогат. Имеется стандартный DLL суррогат (dllhost.ехе), который можно использовать для этой цели.

В рамках СОМ+ использование DLL суррогата наиболее предпочтительно, так как именно в этом случае сервер сможет использовать все сервисы, предоставляемые СОМ+. Однако, при этом необходимо конфигурировать сервер, определив ряд связанных с ним атрибутов в новой структуре данных — каталоге. Эти вопросы будут рассмотрены позже при изучении СОМ+.

Итак, далее мы рассмотрим размещение сервера PubInProcServer в суррогатном процессе.

Для размещения DLL сервера в суррогатном процессе достаточно сделать несколько дополнительных настроек в реестре, связанных с идентификатором приложения — AppID.

Здесь мы уже приближаемся к идеологии СОМ+. В архитектуре СОМ на верхнем уровне стоит сервер, который может содержать несколько классов, каждый из которых реализует несколько интерфейсов. При этом уникальные идентификаторы (GUID) назначаются только классам, интерфейсам, библиотекам типов, но не серверам. В СОМ+ на верхнем уровне находится приложение, которое может содержать несколько классов (размещенных, конечно, в серверах), которые, в свою очередь, реализуют несколько интерфейсов.

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

В качестве GUID для приложения можно взять GUID одного из классов, входящих в состав приложения. Например, первого класса, описанного в idl-файле.

Для занесения информации о AppID надо в реестре под ключом HKEY_CLASES_ROOT AppID завести новый раздел с GUID данного приложения. В этом разделе можно задавать различные атрибуты, управляющие доступом к приложению и т. д. Мы здесь зададим параметр с именем DllSurrogate и с пустым значением. Это означает, что приложение с данным AppID должно выполняться в стандартном суррогатном процессе (dllhost. ехе).

Теперь надо связать все входящие в приложение классы с данным AppID. Для этого под ключом HKEY_CLASES_ROOT CLSID clsid_данного_класса надо завести дополнительный строковый параметр AppID со значением равным AppID нашего приложения. Это надо повторить для всех классов нашего приложения.

Клиент PubCiient был жестко ориентирован на работу с сервером в процессе клиента. Эта ориентация задавалась при активации фабрики класса. Например,

hr = CoGetClassObject(CLSID_CoBook, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**)&pBF);

Параметр CLSCTX_INPROC_SERVER задает использование именно сервера в процессе клиента.

Для загрузки сервера в суррогатный процесс достаточно заменить этот параметр на CLSCTX_LOCAL_SERVER для обоих задействованных в приложении классов: CoBook и СоJournal.

Итак, сервер будет загружаться в DLL суррогат. Выполняется это следующим образом. При построении фабрики класса (CoGetClassObject) система обнаруживает, что клиент требует локальный сервер. Однако в реестре для данного класса имеется только путь к DLL, реализующей сервер в процессе клиента — параметр InProcServer32. Однако, для данного класса имеется и параметр AppID, задающий GUID приложения, использующего данный класс. Обращаясь к разделу реестра, описывающего данное приложение, система обнаруживает, что требуется загрузка сервера в суррогатный процесс — параметр DllSurrogate.


Стандартный маршалинг

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

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

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

Процесс упаковки, передачи и распаковки при вызове метода удаленного объекта и называется маршалингом. В СОМ имеется три типа маршалинга:

Пользовательский маршалинг

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

Стандартный маршалинг

Здесь используются прокси и заглушки, взаимодействующие друг с другом по протоколу ORPC (Object RPC). Причем, если для описания интерфейсов используется IDL, от программиста не требуется писать какой-либо код. Компилятор с IDL — Microsoft IDL (MIDL) генерирует несколько файлов с кодами прокси и заглушки. Используя их можно построить DLL, которая после регистрации и обеспечит маршалинг для описанных в IDL-файле интерфейсов.

Универсальный маршалинг

В этом случае не нужно даже строить DLL заглушки/прокси. Эта DLL строится "на лету". Необходимые условия:

♦ в методах интерфейсов используются параметры только так называемых variant-совместимых типов (некоторое подмножество типов, допустимых в IDL);

♦ в IDL файле интерфейсы описаны с атрибутом oleautomation;

♦ имеется зарегистрированная библиотека типов, в которой описаны интерфейсы;

♦ информация об интерфейсах включена в реестр под ключом НКЕY_CLASSЕS_ROOT Interface.

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

Далее приведен стандартный алгоритм действий для построения DLL прокси/заглушки, обеспечивающих стандартный маршалинг:

1. Открыть новый проект Win32 DLL — PSPubinProcServer и добавить в него сгенерированные компилятором с IDL файлы:

PubInPrосServerTуреInfo_i.с — определения GUID, упомянутых в IDL-файле,

PubInPrосServerTуреInfo_р. с, dlldata.c — код для прокси/заглушек и макросы для саморегистрации формируемой DLL.

2. Создать DEF-файл PSPubinProcServer.def

3. LIBRARY PSPubInProcServer.dll

4. EXPOTRS

5. DllGetClassObject @1 PRIVATE

6. DllCcanUnioadNow @2 PRIVATE

7. DllRegisterServer @3 PRIVATE

8. DllUnregisterServer @4 PRIVATE

9. В диалоге Project]Settings на вкладке C/C++ в поле Preprocessor definitions добавить через запятую флажки REGISTER_PROXY_DLL и _WIN32_DCOM. В результате в проект будет вставлен код саморегистрации.

10. В том же диалоге на вкладке Link в поле Object/library moduls добавить через пробел ссылки на библиотеки поддержки удаленного вызова процедур rpcndr.lib, rpcns4.lib, rpcrt4.lib.

11. Откомпилировать проект и зарегистрировать построенную DLL прокси/заглушки с помощью Tools|Register Control.

В процессе саморегистрации построенной DLL прокси/заглушки она получает CLSID равный CLSID интерфейса IPub. Под соответствующим ключом реестре имеется раздел InProcServer32 с параметром по умолчанию, задающим путь к построенной DLL. Кроме того, регистрируются все три интерфейса под ключом HKEY_CLASES_ROOT Interface. Интерфейсы регистрируются под своими собственными идентификаторами (GUID). Для каждого имеется подраздел NumMethods, в котором указывается число методов интерфейса (включая наследуемые), и подраздел ProxyStabClsid32, в котором указывается GUID для DLL прокси/заглушки.

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

Говоря про маршалинг следует остановиться на вопросе передачи данных между клиентом и сервером. В случае, когда передаются, например, массивы, необходимо следить за тем, кто выделяет и освобождает память, сколько выделяется памяти. Приведенные ниже примеры взяты из работы Richard Grimes, "Marshaling Your Data: Efficient Data Transfer Techniques Using COM and Windows 2000", MSDN Magazine, September 2000.

Предположим, что некоторый интерфейс включает методы, описания которых на IDL приведено ниже

HRESULT PassLongs([in] ULONG ulNum,

[in, size_is(ulNum)] LONG* pArrln);

HRESULT GetLongs([in] ULONG ulNum,

[out, size_is(ulNum)] LONG* pArrOut);

Здесь от клиента к объекту и обратно передаются массивы. Проще всего случай передачи массива от клиента к объекту. В этом случае клиент сам выделяет память под массив и сам ее освобождает. При передаче массива от объекта к клиенту возможны два варианта. В первом клиент заранее, до вызова метода знает размер передаваемого массива, во втором этот размер сообщает объект уже после вызова метода. Рассмотрим обе эти возможности.

Начнем со случая, когда клиент до вызова метода имеет информацию о размере массива. Код клиента

ULONG ulNum = 10;

LONG 1 [10];

hr = pArrObj — > GetLongs(ulNum, 1);

Код сервера

STDMETHODIMP CArrays::GetLongs(ULONG ulNum, LONG* pArr)

{

for (ULONG x = 0; x < ulNum; x++) pArr[x] = x;

return S_OK;

}


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

Во втором случае клиент получает размер массива уже после вызова метода. На IDL описание метода выглядит следующим образом

HRESULT GetLongsAlloc([out] ULONG* pNum, [out, size_is(, *pNum)] LONG** ppArr);

Здесь запятая в size is () говорит о том, что *pNum есть размер массива, УКАЗАТЕЛЬ на который есть ppArr.

В данном случае маршалер не может сам выделить память под массив на стороне объекта и, следовательно, это должен делать объект. Но освобождать выделенную память после передачи данных должен уже маршалер, т. к. объект к этому моменту уже завершит работу. Для выделения и освобождения памяти в данном случае следует использовать функции CoTaskMemAlloc () и CoTaskMemFree (), которые может вызывать и маршалер.

Код компонента

STDMETHODIMP CArrays::GetLongsAlloc(ULONG* pNum, LONG** ppArr);

{

*pNum = 10;

*ppArr = reinterpret_cast(CoTaskMemAlloc(*pNum * sizeof(LONG)));

for (ULONG x = 0; x < *pNum; x++) (*ppArr)[x] = x;

return S_OK;

}


Здесь reinterpret_cast используется для приведения типа указателя, возвращаемого при выделении памяти, к указателю на long. Освобождает выделенную память сам маршалер, вызывая CoTaskMemFree.

На стороне клиента память выделяет маршалер, получив информацию о размере массива — *pNum. Освобождает же эту память клиент

ULONG ulNum;

LONG* pi;

hr = pArrObj —> GetLongsAlloc(SulNum, &pl);

for (ULONG ul = 0; ul < ulNum; ul++)

printf("%ld\n", pi[ul]);

CoTaskMemFree(pi);


Потоковая модель и апартаменты

Процесс и поток

Прежде всего вспомним некоторые понятия, связанные с процессами и потоками.

Приложение — это один или несколько процессов.

Каждый процесс имеет ряд ресурсов, среди которых виртуальное адресное пространство процесса, исполняемый код, данные, дескрипторы объектов, переменные среды, базовый приоритет. Таким образом, процесс это скорее среда, в которой выполняется один или несколько потоков.

Поток — выполнение последовательности машинных команд. Потоки, живущие в одном процессе, делят все его ресурсы. Кроме того, каждый поток имеет и свои собственные, доступные только ему ресурсы-локальная память потока.

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

Очередь потоков регулируется с помощью их приоритетов. Для каждого уровня приоритета имеется своя очередь. Переключение контекста (прерывание исполняемого потока и выполнение нового) происходит при возникновении одного из трех событий:

• закончился квант времени, выделенный потоку;

• готов к выполнению поток с более высоким приоритетом;

• исполняемый поток перешел в состояние ожидания.

Базовый приоритет потока определяется двумя величинами: приоритетом процесса и приоритетом потока в рамках данного процесса. Очередность определяется не базовым, а динамическим приоритетом. Часто динамический приоритет просто совпадает с базовым, но в некоторых случаях система может приписать потоку динамический приоритет, превосходящий его базовый приоритет.

Например, если поток выполняет операции ввода/вывода, то система может приписать ему повышенный динамический приоритет. Однако, этот приоритет уменьшается на 1 после получения данным потоком очередного кванта процессорного времени и может достигнуть базового значения. Ниже базового значения приоритет не уменьшается.

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

Как порождаются потоки? Первый поток образуется при выполнении функции main. Новый поток можно запустить функцией CreateThread. В качестве одного из параметров этой функции передается адрес функции, которая будет выполняться в потоке.

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

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

Следующий стандартный псевдокод иллюстрирует использование критических секций

class CoClass { public:

CoClass::CoClass ()

{

InitializeCriticalSection(&m_cs);

}


CoClass::~CoClass()

{

DeleteCriticalSection(&m_cs);

}


HRESULT __ stdcall CoClass::Method()

{

EnterCriticalSection(&m cs);

……

LeaveCriticalSection(&m_cs);

return S_OK;

}

private:

CRITICAL_Section m_cs;

};


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

Использование критических секций позволяет запретить параллельное выполнение некоторых участков кода. Но это не решает всех проблем. Например, как с помощью критических секций обеспечить синхронизацию доступа к глобальной переменной g_objCount, используемой для подсчета числа активированных экземпляров классов, определенных в сервере PubInProcServer? Здесь удобнее применять функции

InterLockedIncrement(&g_objCount)

и

InterLockedDecrement (&g_objcount),

обеспечивающие безопасное увеличение и уменьшение на единицу значения параметра функции.

Последнее, что нужно упомянуть в связи с потоками — понятие окна и очереди сообщений. В Windows все потоки делятся на два класса: потоки с окном и рабочие потоки. В первом случае с потоком связано некоторое окно (возможно, скрытое), оконная процедура и очередь сообщений. Поток выбирает очередное сообщение из очереди сообщений и переправляет его в оконную процедуру, которая и определяет способ обработки данного сообщения. Это реализуется с помощью следующего цикла

MSG msg;

while(GetMessage(&msg, 0, 0, 0))

DispatchMessage(&msg);

Во втором случае окно, оконная процедура и очередь сообщений не используются.

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


Апартаменты

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

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

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

Итак, начнем с иерархии процессов, апартаментов, потоков и объектов:

• В каждом процессе имеется один или несколько апартаментов.

• Каждый поток живет в определенном апартаменте.

• Каждый объект живет в определенном апартаменте.

Имеется три типа апартаментов:

STA (Single-Threaded Apartment)

В одном процессе может иметься несколько апартаментов этого типа. Один из них (созданный первым) носит название главного STA. В STA живут объекты, не поддерживающие параллельный вызов своих методов. В каждом STA живет ровно один поток, причем это поток с окном. Именно этот поток выполняет все вызовы методов объектов, живущих в данном апартаменте.

МТА (MultiThreaded Apartment)

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

NA (Neutral apartment)

В одном процессе может иметься только один NA. В NA живут объекты, поддерживающие параллельный вызов своих методов. Нет потоков, связанных с NA навечно. Любой поток из STA или МТА может временно покинуть свой апартамент и заняться выполнением некоторого метода некоторого объекта, живущего в NA.

Теперь рассмотрим процесс создания апартаментов, связь с ними потоков и объектов. Ограничимся случаем сервера, исполняющегося в процессе клиента.

Потоки создаются клиентом. Клиент имеет возможность связать данный поток с STA или МТА апартаментами. При этом либо создается новый апартамент, либо поток связывается с уже существующим апартаментом.

Если поток собирается работать с СОМ, он должен вызвать

CoInitialize(NULL), CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)

или

CoInitializeEx(NULL, COINIT_MULTITHREADED).

В первых двух случаях создается НОВЫЙ STA и данный поток связывается с этим STA. Первый из созданных STA объявляется главным. В третьем случае создается МТА, если он не был создан ранее. Данный поток связывается с МТА.

При создании апартамента в куче создается специальный объект апартамента, содержащий, в частности, такую информацию как идентификатор апартамента и его тип. Связь потока с апартаментом обеспечивается тем, что идентификатор апартамента сохраняется в локальной памяти потока (TLS — Thread Local Storage).

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

Мы уже знаем, что если некоторый сервер предназначен для выполнения в процессе клиента, то для каждого его класса в реестре системы имеется раздел InProcServer32. В этом разделе имеется параметр по умолчанию, значением которого является путь к DLL, содержащей код данного сервера. В этом же разделе может иметься именованный параметр — ThreadingModel. Именно этот параметр определяет тип апартамента, в котором может жить экземпляр данного класса. В таблице 2.1 перечислены возможные значения данного параметра и допустимые типы апартаментов.


Table: Соответствие потоковой модели класса и допустимого типа апартамента

Потоковая модель ∙ Тип апартамента

Нет ∙ Главный STA

Apartment ∙ Любой STA

Free ∙ МТА

Both ∙ Любой STA или МТА

Neutral ∙ NA


Отметим несколько моментов, связанных с этой таблицей.

Значение Нет соответствует случаю класса, который создавался до внедрения многопоточности в Windows. Все экземпляры таких классов помещаются в главный STA. В связи с тем, что с этим апартаментом связан только один поток, методы всех объектов, живущих в данном апартаменте, не могут выполняться параллельно. Это гарантирует безопасность работы с такими объектами в многопотоковой среде. Но эта безопасность достигается за счет неизбежного снижения производительности системы. Чем больше объектов живет в главном STA, тем дольше приходится клиенту ожидать выполнения вызова метода живущего в этом апартаменте объекта.

Значение Apartment означает, что экземпляр данного класса не рассчитывает на параллельный вызов его методов, хотя сам обращается к глобальным переменным (например, счетчикам числа активированных объектов) безопасным образом. Объкт, поддерживающий данную модель, будет размещен в STA. Если создавший данный объект поток сам живет в STA, то именно в этом STA и будет жить новый объект. Если же этот поток принадлежит МТА, то будет создан новый STA (и новый связанный с ним поток), в который и будет помещен созданный объект. Данная модель позволяет параллельно обращаться к объектам, живущим в различных STA. Это существенное улучшение по сравнению с использованием одного главного STA.

Значение Free означает, что экземпляр данного класса знает о потоках все и готов к получению параллельных вызовов своих методов. Такой объект помещается в МТА (МТА создается, если его ранее не было).

Значение Both означает, что экземпляр данного класса не только готов к получению параллельных вызовов своих методов, но и осторожен в обращении со своими клиентами. При взаимодействии двух объектов разделение их по признаку клиент/сервер достаточно условно. Вызываемый объект в свою очередь может вызвать какие-то методы вызывающего объекта. Разделяя потоко-безопасные и потоко-опасные объекты по различным апартаментам (МТА и STA) мы обеспечиваем, кроме всего прочего, защиту вызывающего объекта от вызываемого.

Но для общения через границы апартаментов нужны дополнительные ресурсы. Было бы оптимально поместить вызываемый объект в апартамент вызывающего объекта, что обеспечит прямой доступ к его методам. Если потоковая модель класса объявлена как Both, это означает гарантию со стороны разработчика класса того, что экземпляр данного класса будет корректно работать как с потоко-безопасными объектами в МТА, так и с объектами не поддерживающими параллельный доступ, в STA. Это позволяет разместить данный объект в том апартаменте, в котором живет создавший его поток. Тем самым обеспечивается максимальная эффективность доступа к методам данного объекта из создавшего его потока.

Значение Neutral требует размещения экземпляра класса в апартаменте NA.

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



Итак, мы рассмотрели типы апартаментов и условия, определяющие привязку потоков к апартаментам и размещение создаваемых объектов в апартаментах. Теперь надо разобраться с тем, как можно вызывать объекты из потоков, которые могут принадлежать как одному, так и различным апартаментам.

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

В случае STA

поток имеет прямой указатель на некоторый интерфейс объекта (т. к. именно этот поток и создал этот объект) и может делать прямые вызовы его методов.

В случае МТА

♦ либо данный поток создал данный объект и, следовательно, имеет на него прямой указатель,

♦ либо объект создан каким-то другим потоком из МТА.

В этом случае поток, создавший объект, может сохранить указатель на его интерфейс в глобальной переменной. Все остальные потоки из МТА (и только из МТА!) могут использовать этот указатель для прямого вызова методов данного интерфейса.

Напрямую можно обращаться и к объектам из NA, независимо от того, поток из какого апартамента создал этот объект. Эта новая технология появилась в Windows 2000. Рекомендуется все вновь создаваемые СОМ+ объекты создавать для размещения их в NA. Преимуществом данной технологии является отсутствие переключения потоков. Вызывающий поток независимо от апартамента покидает свой апартамент и выполняет код метода объекта из NA. В данном разделе мы более не будем обсуждать эти новые возможности и продолжим изучать проблемы, связанные с классическими STA и МТА.

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

В чем причина? Вызов через границы апартаментов выполняется через пару прокси/стаб. Прокси находится в апартаменте вызывающего процесса, а стаб в апартаменте вызываемого объекта.

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

Здесь уместно заметить, что прокси и стаб должны быть зарегистрированы в реестре как поддерживающие потоковую модель Both. Это позволит загружать прокси и стаб в любой апартамент. Такая регистрация выполняется автоматически, если используется idl для описания интерфейсов и компилятор MIDL.

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

Данный поток сам инициировал порождение данного объекта

В этом случае автоматически выполняется маршалинг запрошенного интерфейса, который сводится к получению некоторого нейтрального к апартаменту представления указателя на интерфейс, передачи его в апартамент вызывающего потока и к построению (с помощью proxy/stab DLL, которая должна быть построена для данного сервера и зарегистрирована на данной машине) прокси в вызывающем апартаменте и стаба в вызываемом апартаменте. Таким образом, вызывающий поток получает указатель на прокси, имитирующий для него объект из другого апартамента. Если в данном апартаменте кроме вызывающего потока имеются другие потоки (случай МТА), то и эти другие потоки могут использовать построенный указатель на прокси.

Данный поток не инициировал порождение данного объекта

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


Процедура состоит из двух шагов

♦ Маршализация указателя на интерфейс

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

Для этого вызывается функция библиотеки СОМ

♦ WINOLEAPI CoMarshallnterThreadlnterfacelnStream(

♦ [in] REFIID riid,

♦ [in] LPUNKNOWN pUnk,

♦ [out] LPSTREAM *ppStm);

В данную функцию передаются сылка на GUID интерфейса и указатель на этот интерфейс. В качестве результата получаем указатель на доступный для обоих потоков объект-поток (stream object), который поддерживает стандартный интерфейс istream и используется для временного хранения нейтрального представления указателя на запрошенный интерфейс.

♦ Демаршализация указателя на интерфейс

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

♦ WINOLEAPI CoGetlnterfaceAndReleaseStream(

♦ [in] LPSTREAM pStm,

♦ [in] REFIID riid,

♦ [out] LPVOID FAR* ppv);

Выходной параметр дает указатель на желаемый интерфейс, который на самом деле является указателем на прокси, через который поток может делать вызов через границу между апартаментами. Как и при автоматическом маршалинге интерфейсов здесь используется proxy/stab DLL, содержащая код маршалинга для интересующего нас интерфейса.


Заметим, что описанный прием можно применить и в том случае, когда вызывающий поток и вызываемый объект находятся в одном апартаменте, но никто в данном апартаменте не имеет прямого указателя на этот объект. Такая ситуация может возникнуть, например, когда поток из STA создает объект с потоковой моделью Free, а потом некоторый поток из МТА желает вызвать метод этого объекта. В этом случае никто в МТА не имеет прямого указателя на построенный объект. Только поток в STA имеет указатель на прокси для этого объекта. Необходимо выполнить маршализацию указателя на интерфейс в STA и его демаршализацию в МТА. Система сама обнаружит, что вызывающий поток и вызываемый объект находятся в одном апартаменте и вместо прокси предоставит прямой указатель на объект.

Описанный способ маршалинга указателя на интерфейс имеет определенные недостатки:

Неоптимальность вызова объекта с потоковой моделью Both

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

Дня использования прямого указателя при вызове из другого апартамента необходимо обмануть СОМ. Этот прием называется Free-Threaded Marshaler (FTM). Вот его суть.

При проектировании объекта с потоковой моделью Both нужно предусмотреть агрегацию этим объектом одного специального уже реализованного в СОМ объекта, поддерживающего специальным образом реализованный стандартный интерфейс IMarshaler.

При использовании пары функций

CoMarshaIinterThreadInterfaceInStream

и

CoGetInterfaceAndReieaseStream

система вначале проверяет — не поддерживает ли объект, указатель на который маршалируется, интерфейс IMarshal. Если такой интерфейс не поддерживается, то используется стандартный маршалинг и мы получаем прокси для доступа к объекту, так как было описано ранее. Если же интерфейс IMarshal реализован в данном объекте, то именно он определяет способ маршалинга.

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

Создать агрегируемый объект, реализующий marshal, можно с помощью функции

CoCreateFreeThreadedMarshaleг.

При использовании описанного приема нужно соблюдать определенные ограничения. Объект с FTM не должен использовать прямые указатели на другие объекты, которые не используют FTM. Причина понятна. При нарушении указанного ограничения некоторый поток может получить косвенным образом прямой указатель на объект из другого апартамента, чья потоковая модель может отличаться от Both. Использование этого указателя может привести к краху всего приложения.

• При демаршализации указателя на интерфейс уничтожается объект-поток

Это означает, что если указатель на некоторый объект нужен потокам в нескольких различных апартаментах, то мы должны несколько раз вызывать пару функций

CoMarshaInterThreadInterfaceInStream

и

CoGetInterfaceAndReleaseStream.

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

Такая технология реализована в СОМ и называется Global Interface Table (GIT). В классе с GUID CLSID_StdGiobalInterfасеTаЫе реализован интерфейс IGlobalInterfасеаЫе, методы которого позволяют зарегистрировать новый указатель на некоторый интерфейс в этой таблице, получить его из таблицы, удалить указатель из таблицы. При этом автоматически происходит маршализация указателя на интерфейс при его регистрации в таблице и демаршализация при получении из таблицы.

А теперь в заключении рассмотрим механизм вызова метода через границу апартамента.

Начнем с механизма вызова объекта в STA.

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

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

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

В случае вызова объекта из МТА все значительно проще.

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


Технология СОМ+ от Microsoft

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

Обеспечивают надежность приложения:

Безопасность

В отличие от локального приложения, в работу с распределенным приложением вовлечены многие конечные пользователи, которые, с точки зрения администратора системы, должны иметь различные права доступа к данному приложению. В СОМ+ решаются следующие вопросы:

— Аутентификация клиента

Тот ли он, за кого себя выдает?

— Авторизация клиента

Какие операции может выполнять данный клиент?

— Передача полномочий

Часто в распределенной системе вызов некоторого метода, выполненный конечным пользователем, приводит к формированию цепочки вызовов одними объектами других объектов. В начале цепочки используются полномочия конечного пользователя. Но чьи полномочия будут использованы далее? о Транзакции

Транзакции обеспечивают целостность данных в распределенной системе. Заметим, что СУБД обеспечивают механизм транзакций, но только в рамках одной базы данных. Здесь же в одну распределенную транзакцию могут быть вовлечены различные независимые хранилища данных,

Синхронизация

В СОМ синхронизация доступа к объектам из разных потоков осуществляется с помощью использования механизма апартаментов. Практика программирования показывает, что редкие программисты проектируют потоко-безопасные компоненты, которые могут жить в таких апартаментах как МТА и NA. Как правило, используется STA, и доступ ко всем объектам, живущим в одном STA, синхронизируется самой системой посредством очереди сообщений. СОМ+ идет на встречу реальным предпочтениям программистов, предлагая возможность задавать синхронизацию доступа к объектам декларативным образом. При этом, даже объекты, живущие в МТА или NA, могут быть защищены от параллельного обращения,

Очереди (асинхронная коммуникация)

В СОМ вызовы всех объектов синхронны. Иными словами, поток, сделавший вызов некоторого метода некоторого объекта, продолжает работу только после получения ответа. Исключением является поток в STA, который может выйти из состояния ожидания ответа для обработки вызова, пришедшего извне и принадлежащего к той же цепочке вызовов, что и вызов, ответ на который ожидается. Такая синхронная модель очевидно не сработает при временном выходе из строя сервера, в котором живет вызываемый объект. Решением указанной проблемы является асинхронная коммуникация. Вызов ставится в некоторую очередь вызовов, а вызывающий поток продолжает свою работу. Вызов из очереди извлекается и передается для выполнения серверу тогда, когда он станет доступным.

Обеспечивают масштабируемость приложения:

Свободно связанные события

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

Пул объектов и активация по необходимости

В СОМ+ различают создание/удаление объекта и его активацию/деактивацию. В ряде случаев выгодно создать некоторое число объектов нужных типов, а затем выполнять активацию/деактивацию объектов, помещая деактивированный объект в так называемый пул объектов, и извлекая объект нужного типа из пула объектов при активации,

Базы данных в памяти

Некоторые таблицы из баз данных, которые часто используются только для чтения, могут размещаться в оперативной памяти машин среднего уровня (уровень бизнес-логики в Windows DNA архитектуре),

Динамическое выравнивание нагрузки

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

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

Использование декларативного подхода в данном случае имеет ряд преимуществ по сравнению с процедурным:

• Уменьшается время на разработку приложения

• Повышается надежность кода

Автоматически сгенерированный код надежнее созданного программистом, т. к. он прошел более объемное тестирование

• Уменьшается степень зависимости приложения от конкретной версии платформы (СОМ+)

Пока не изменена семантика, приписанная атрибутам, данное приложение будет работать со всеми последующими версиями платформы.

Атрибуты фактически уже приписывались компонентам и приложению уже в СОМ. Например, каждому классу приписывался его уникальный CLSID, путь к серверу (DLL или EXE файлу), потоковая модель (ThreadingModel). Однако возможности реестра ограничены, и для хранения большого числа дополнительных атрибутов, появившихся в СОМ+, используется дополнительная база данных — так называемый СОМ+ каталог. Имеется менеджер каталога, у которого есть доступ как к реестру системы, так и к каталогу. Разработчик и администратор получают доступ к каталогу через утилиту Component Services, или через иерархию предоставляемых системой объектов.

В данном курсе вопросы администрирования специально рассматриваться не будут. Используя Component Services несложно задать необходимый набор атрибутов для нового приложения. Единственное, что для этого необходимо — понимание имеющихся в СОМ+ сервисов. Именно их изучению и будет посвящена основная часть данной главы.


Архитектура СОМ+

Начнем со структуры приложения.

Понятия приложение, класс, интерфейс, метод образуют следующую иерархию:

приложение / класс / интерфейс / метод.

Иными словами, приложение содержит один или несколько классов (компонентов), каждый класс реализует один или несколько интерфейсов, и каждый интерфейс описывает один или несколько методов. Причем каждый класс может входить не более чем в одно приложение.

Имеется два типа приложений: библиотечные и серверные. Библиотечное оформляется в виде DLL и загружается в адресное пространство клиента. Серверное также оформляется в виде DLL, но при его активации запускается суррогатный процесс (dllhost.exe), в адресное пространство которого и загружается эта DLL. Тип приложения определяется при его создании. Например, при использовании Component Services задается приписываемый всему приложению атрибут, задающий его тип активации — библиотечное или серверное приложение.

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

Теперь рассмотрим, как выглядит приложение во время выполнения. Все, что говорилось ранее о процессах, потоках, апартаментах имеет место и в СОМ+. Но эта архитектура усложняется введением новых элементов: контекст и активность.

Начнем с контекста. Каждый объект инкапсулирует данные и методы. Обычно говорят, что данные описывают состояние объекта. Однако в СОМ+ данные, инкапсулированные в объекте,

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

WINOLEAPI CoGetObjectContext

{

[in] REFIID riid,

[out] LPVOID **ppv

};

Первый параметр задает GUID запрашиваемого интерфейса, реализованного объектом контекста(IID_IObectContext, IID_IObjectContextlnfо, IID_IObjectContextActivity, IID IContextState). Во втором параметре возвращается адрес указателя на запрошенный интерфейс. Используя эти интерфейсы объект может не только узнать свое текущее состояние, но и изменить его. Рассматривать данные интерфейсы здесь мы не будем, т. к. первоначально необходимо изучить сервисы, для использования которых эти интерфейсы и разработаны.

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

Итак, каждый объект в СОМ+ живет в некотором контексте. Различные контексты не пересекаются друг с другом и не пересекают границы апартаментов.

Понятие контекста тесно связано с понятием перехвата. Именно механизм перехвата обеспечивает учет семантики, определенной при задании атрибутов компонента. Don Box в статье "Windows 2000 Brings Significant Refinements to the COM(+) Programming Model", Microsoft System Journal, May 1999, так описывает схему перехвата

1. Компонент описывает свои требования используя атрибуты.

2. Во время создания объекта система проверяет — выполняется ли активатор (код, вызвавший CoCreateInstance) в среде, совместимой с конфигурацией класса?

3. Если ответ на предыдущий вопрос положителен, то перехват не нужен, и CoCreateInstance возвращает прямой указатель на объект.

4. В противном случае CoCreateInstance передает управление среде, совместимой с требованиями класса, создает там объект и возвращает прокси.

5. Этот прокси обеспечивает совместимость среды выполнения с требованиями класса, выполняя определенные действия до и после каждого вызова метода.

Фактически понятие перехват появилось даже ранее MTS (Microsoft Transaction Server). В приведенную выше схему полностью укладывается процесс создания нового экземпляра класса с заданной потоковой моделью в рамках СОМ. Вообще, Don Box называет принцип перехвата краеугольным камнем современного СОМ программирования.

В СОМ маршализация указателей на интерфейс требовалась при вызове через границу апартамента. В СОМ+ такая маршализация требуется при вызове через границу контекста. В соответствии с принципом перехвата только в рамках одного контекста можно использовать прямые указатели на интерфейс.

Как и в СОМ маршализация и демаршализация указателей на интерфейс выполняется автоматически при создании, активации объекта и при вызове функций, возвращающих указатели на интерфейс. В остальных случаях, для получения указателя на интерфейс объекта из другого контекста необходимо явным образом выполнить процедуры маршализации и демаршализации указателя на интерфейс. Дня этого можно использовать функции CoMarshalInterfасе и CoUnmarshalInterfасе. Для некоторой оптимизации этого процесса можно проектировать объекты с FTM и использовать GIT, как это было в СОМ.


Синхронизация

Ранее уже упоминалаось, что при программировании в СОМ+ рекомендуется выбирать для новых классов потоковыю модель ThreadingModei = Neutral. Все экземпляры такого класса будут размещаться в одном апартаменте NA. Основное преимущество этого апартамента состоит в том, что он не имеет связанных с ним потоков, и поток из любого другого апартамента, сделавший вызов метода объекта из NA, временно покидает свой апартамент и выполняет код вызванного метода. Отсутствие переключения потоков существенно снижает затраты на вызов. Однако, необходимо побеспокоиться о синхронизации. Сам апартамент NA никак не ограничивает возможность параллельного вызова одного и того же метода одного и того же объекта из NA.

В рамках СОМ синхронизация обеспечивалась либо написанием потоко-безопасного кода (например, путем использования критических секций), либо объект помещался в STA апартамент. В СОМ+ появляется новая возможность — декларация необходимости синхронизации путем задания нужного значения для соответствующего атрибута. Сама синхронизация обеспечивается через механизм, основанный на понятии активность.

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

Включение контекста в некоторую активность определяется атрибутом Synchronization входящих в контекст объектов. В таблице 3.1 приведены возможные значения этого атрибута и их влияние на вхождение контекста, которому будет принадлежать активируемый объект, в ту или иную активность.



При назначении значения атрибуту Synchronization необходимо учитывать некоторые ограничения. Именно, если потоковая модель класса ThreadIngModel=Apartment, или данный класс поддерживает активацию по необходимости, или он использует сервис транзакций, необходимо выбрать для значения атрибута синхронизации либо REQUIRED, либо REQUIRES_NEW.

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

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

К сожалению, описанный механизм синхронизации не решает проблему синхронизации полностью. Блокировка при вызове объекта из некоторой активности устанавливается на уровне процесса. Это означает, что в случае, когда активность включает контексты из нескольких процессов, вызовы, направленные в контексты данной активности, но принадлежащие различным процессам, могут выполняться параллельно. Это может привести к deadlock. Пример приведен в упомянутой выше статье Don Box. Пусть поток X вызывает объект А, поток Y вызывает объект В, объекты А и В принадлежат одной активности, но разным процессам. В этом случае вызовы могут выполняться параллельно. Пусть теперь объект А вызвал объект В, а объект В вызвал объект А. Возникает deadlock, так как эти вызовы принадлежат разным цепочкам вызовов и блокируют друг друга.

Рецепт следующий. Не следует различным клиентам пользоваться одними и теми же объектами-серверами. Каждый клиент должен активировать для себя новый объект-сервер. Разделять следует только данные. Их согласованность будет обеспечиваться сервисом транзакций.

Рассмотрев понятие синхронизации можно поставить вопрос о роли апартаментов в СОМ+. Как говорит Don Box, их роль резко ограничена. Это привязка потоков к контекстам и только. Действительно, к каждому апартаменту кроме NA привязан один (в случае STA) или несколько (в случае МТА) потоков. Эти потоки могут вызвать любой метод любого объекта из соответствующего апартамента. Методы объекта из NA вообще могут быть вызваны из любого потока. Механизм синхронизации в СОМ+ определяет когда поток из апартамента может вызвать метод объекта, живущего в некотором контексте этого апартамента.


Распределенные транзакции

Основные понятия

Начнем с некоторой теории, точнее, с некоторого набора понятий, общих для различных моделей и стандартов, связанных с самим понятием распределенной транзакции.

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

В распределенную транзакцию вовлечены следующий объекты:

Прикладные компоненты

Число и функции прикладных компонентов определяются бизнес-логикой приложения.

Менеджеры ресурсов

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

Менеджер транзакций

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

Транзакция — это некоторая единица работы, которую выполняют вышеописанные объекты в течении относительно короткого временного интервала (в СОМ+ по умолчанию транзакция длится не более 60 секунд). Транзакция должна обладать следующими ACID свойствами:

Atomicity — атомарность транзакции

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

Consistency — целостность распределенных данных

В предположении целостности данных до начала транзакции, целостность данных должна иметь место и по завершении транзакции. Здесь под данными понимаются данные, хранимые во всех вовлеченных в транзакцию ресурсах данных. Целостность данных означает то, что данные удовлетворяют ряду определяемых приложением ограничений.

Isolation — изоляция транзакции

В процессе выполнения транзакции модифицируется часть данных, хранимых в ресурсах данных. Доступ к таким данным извне транзакции должен быть заблокирован по двум причинам.

Во-первых, во время выполнения транзакции обычно не известен конечный результат — будут ли результаты транзакции сохранены или будет выполнен откат назад. В связи с этим использование модифицируемых данных извне транзакции запрещается.

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

Durability — сохранность результатов транзакции

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

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

Демаркация транзакции

Данное понятие означает способ определения единицы работы, составляющей одну атомарную транзакцию. Работа состоит из некоторой совокупности операций (транзакционных операций), выполняемых вовлеченными в транзакцию объектами. Имеется два способа демаркации транзакции:

Программная демаркация

Этот способ обычно применяется при следовании процедурной парадигме программирования. Некоторому потоку приписывается метка транзакции, и все операции, выполняемые в этом потоке до момента выполнения команды успешного завершения или прерывания транзакции, принадлежат данной транзакции. Транзакцию можно временно приостанавливать, снимая метку с потока и вновь помечивая поток.

Декларативная демаркация

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

Контекст транзакции

Транзакция порождается менеджером транзакций путем создания контекста транзакции. Контекст транзакции сохраняет состояние транзакции, в частности, ее идентификатор. Все объекты, вовлеченные в транзакцию, имеет доступ к контексту транзакции.

Регистрация ресурсов

Каждый вовлекаемый в транзакцию менеджер ресурсов регистрируется у менеджера транзакций. Эта регистрация снимается по завершении транзакции.

Двух-фазный протокол завершения транзакции

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

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

Во второй фазе менеджер транзакций принимает окончательное решение об успешном завершении транзакции или об откате назад.

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

Если кто-то из менеджеров ресурсов не смог завершить успешно (или вовремя) фазу подготовки, менеджер транзакций выдает команду на откат назад.

Теперь опишем процесс порождения, распространения и завершения распределенной транзакции в целом. Остановимся на случае декларативной демаркации транзакции, т. к. именно этот способ используется в СОМ+.

1. Порождение транзакции

Новая транзакция порождается при активации некоторого прикладного компонента. Данный прикладной компонент либо требует порождения новой транзакции не зависимо от наличия транзакции, в которой выполняется его активатор, либо ему требуется какая-либо транзакция, но активатор выполняется вне транзакций.

Порождается транзакция менеджером транзакций, который формирует контекст транзакции, делая его доступным для данного прикладного компонента.

В соответствии с декларативным принципом демаркации транзакции, все операции, выполняемые данным прикладным компонентом, принадлежат этой транзакции.

2. Распространение транзакции

Если прикладной компонент, выполняющийся в некоторой транзакции, инициирует активацию нового прикладного компонента, то этот компонент помещается в текущую транзакцию если он не требует явно выполнения в новой транзакции или выполнения вне всякой транзакции. Контекст текущей транзакции становится доступным вновь включенному в транзакцию компоненту.

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

Прикладные компоненты не заботятся о том, чтобы транзакция удовлетворяла ACID свойствам. Эта ответственность возложена на менеджера транзакций и менеджеров ресурсов, вовлеченных в текущую транзакцию.

3. Завершение транзакции

Каждый прикладной компонент по завершении своей доли работ в рамках текущей транзакции информирует систему о своем согласии на успешное завершение транзакции либо о своем требовании выполнить откат назад.

Если хотя бы один прикладной компонент потребовал отката назад, менеджер транзакций выдает соответствующую команду всем зарегистрированным менеджерам ресурсов.

В противном случае, если все прикладные компоненты удовлетворены результатами транзакции, выполняется двух-фазный протокол завершения транзакции.


Далее мы перейдем к рассмотрению того, как распределенные транзакции реализованы в СОМ+. Но предварительно уместно остановиться на вопросе совместимости менеджеров транзакций и менеджеров ресурсов от различных поставщиков. Очевидно, что при работе на платформе СОМ+ разработчик будет использовать менеджер транзакций этой платформы, который будет без проблем понимать и прикладные компоненты, созданные на этой платформе. Несомненно, этот менеджер транзакций способен регистрировать для участия в транзакции менеджеров ресурсов, разработанных в Microsoft. Это Microsoft SQL Server, Microsoft Message Queue Server. Но как быть с менеджерами транзакций от других поставщиков?

Имеется стандарт X/Open Distributed Transaction Model от консорциума Open Group. Эта модель специфицирует несколько интерфейсов, среди которых два описывают взаимодействие между прикладными компонентами, менеджером транзакций и менеджерами ресурсов:

ТХ интерфейс

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

ХА интерфейс

Этот двунаправленный интерфейс обеспечивает взаимодействие менеджера транзакций и менеджера ресурсов. Часть функций данного интерфейса реализуется менеджером ресурсов, а часть — менеджером транзакций. В частности, менеджер транзакций реализует функции регистрации и дерегистрации менеджера ресурсов для участия в транзакции.

Для совместимости менеджера транзакций из СОМ+ с менеджерами ресурсов от других поставщиков в СОМ+ предусмотрена поддержка ХА интерфейса. В результате в распределенную транзакцию, порожденную прикладными компонентами выполняющимися под СОМ+, могут быть вовлечены менеджеры ресурсов для таких систем хранения данных как Oracle, Informix, IBM DB2, Sybase SQL Server, Ingres, выполняющихся на различных платформах.

Теперь перейдем непосредственно к транзакционной модели, реализованной в СОМ+.


Модель транзакций в СОМ+

Прикладные компоненты

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

Прежде всего участие или неучастие объекта в транзакции определяется его транзакционным атрибутом. Значение этого атрибута может быть задано как разработчиком класса (в IDL файле, содержащем описание данного класса), так и администратором (посредством использования Component Services).

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

Указанный атрибут после компиляции IDL файла будет храниться в библиотеке типов.

При использовании Component Services необходимое значение атрибута задается на вкладке Transactions.

Транзакционный атрибут может принимать одно из пяти значений (ниже эти значения приведены в той форме, которая используется в Components Services):

Disabled

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

Not Supported

Объект, которому приписано это значение, не только сам отказывается участвовать в каких-либо транзакциях, но и не распространяет транзакцию на объекты, активацию которых он инициирует. Иными словами, пусть объект А выполняется в транзакции Т1, объект В активируется объектом А и имеет Not supported в качестве значения транзакционного атрибута. Пусть, наконец, объект С активируется объектом В. Тогда, не зависимо от значения транзакционного параметра, приписанного объекту С, данный объект не будет выполняться в транзакции Т1.

Supported

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

Required

При выборе этого значения мы получаем объект, который всегда выполняется в контексте некоторой транзакции. Если его активатор уже принадлежит некоторой транзакции, то и он принадлежит той же транзакции. В противном случае при активации объекта порождается новая транзакция, в контексте которой и будет выполняться данный объект. Заметим, что в последнем случае этот объект называется корнем транзакции, в контексте которой он выполняется.

Requires New

В этом случае при активации объекта всегда порождается новая транзакция, корнем которой является данный объект.

Имеется еще один атрибут, который необходимо приписать классу, собирающемуся участвовать в транзакциях. Это атрибут активация по необходимости (Just-In-Time Activation), имеющий два значения (on/off). В транзакционной модели СОМ+ все классы, которые могут выполняться в контексте какой-либо транзакции, должны быть активируемыми по необходимости (тут надо заметить, что и объекты, не собирающиеся участвовать в транзакциях, могут активироваться по необходимости).

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

При первичной активации объекта (например, клиент использует фабрику класса для построения экземпляра класса) формируется сам объект, а также прокси на стороне клиента, стаб на стороне сервера и соединяющий их канал. Пока клиент имеет хотя бы одну ссылку на данный объект, система прокси/канал/стаб продолжает существовать. Циклом же активации/деактивации объекта на стороне сервера управляет СОМ+. Объект деактивируется, если он не нужен, и активируется по необходимости. Точнее говоря, любой объект, выполняющийся в контексте некоторой транзакции, деактивируется не позднее завершения этой транзакции. Более подробно это обсуждается далее. Вторичную активацию объекта инициирует его стаб, получивший вызов от клиента.

Механизм активации по необходимости очевидно помогает решить проблему создания масштабируемого приложения. Но необходимость его использования именно в случае транзакционных объектов связана вовсе не с заботой о масштабируемости. Основная причина — повышение надежности программирования транзакционных приложений. Дело в том, что по завершении транзакции в рамках модели СОМ+ прикладные компоненты ничего не знают об окончательном ее результате — была ли транзакция успешной, либо завершилась откатом. В этих условиях опасно оставлять у прикладных компонентов, участвовавших в транзакции, какую-либо память о завершившейся транзакции. Эта память может противоречить реальному положению дел. Например, некоторый объект помнит, что счет некоторого клиента был увеличен на 100 долларов, но реально транзакция была прервана и был выполнен откат назад. В результате имеется опасность нарушения целостности данных, если упомянутый объект будет действовать далее с учетом имеющейся у него информации. Принудительная деактивация всех прикладных компонентов, участвовавших в транзакции, очищает их память, сохраняя тем самым целостность данных.

Говоря про активацию по необходимости, следует упомянуть про пул объектов. Это еще один сервис, предоставляемый СОМ+ как средство решения проблемы масштабируемости приложения. При активации объект может либо формироваться заново, либо извлекаться из пула ранее построенных объектов данного типа. При деактивизации вместо уничтожения некоторого объекта его можно вернуть в пул для последующего использования. Выбор за разработчиком.

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

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

Далее приведены требования к классу, экземпляры которого можно помещать в пул:

• Класс должен реализовать стандартный интерфейс IObjectControl

Данный интерфейс имеет три метода, которые автоматически вызываются СОМ+.

Activate

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

Deactivate

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

CanBePooled

Этот метод вызывается автоматически на последнем этапе деактивации. Тут объект может отказаться по каким-либо причинам от помещения в пул. В этом случае он просто будет уничтожен.

• Допустимые потоковые модели: Free, Both, Neutral

Иными словами, объект, допускающий помещение в пул, должен жить в МТА или NA апартаменте. Это связано с тем, что один поток сформирует данный объект и поместит его в пул, и совсем другому потоку понадобится извлечь его из пула и вызвать какой-либо из его методов.

• Класс должен допускать агрегацию, но сам не должен агрегировать другие помещаемые в пул объекты.

• Класс не должен использовать FTM для оптимизации маршализации указателя на интерфейс.


Распространение транзакции

Менеджер транзакций в СОМ+ имеет имя координатор распределенных транзакций — DTC (Distributed Transaction Coordinator). При активации объекта, который должен быть корнем некоторой новой транзакции, DTC порождает новую транзакцию, которая получает уникальный идентификатор (GUID). Этот идентификатор хранится к объекте контекста данного объекта. Кроме того, объект контекста хранит два бита, модифицируя которые объект информирует DTC о своем отношении к текущей транзакции. Эти биты иногда называют бит голосования и бит деактивации по возврату.

Бит голосования

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

По умолчанию именно это значение выбирается при активации объекта.

Принимает значение 0, если объект в данный момент не удовлетворен результатами и собирается голосовать за откат назад.

Бит деактивации по возврату

Если данный бит установлен в 1, то сразу же после возврата из выполняемого метода данный объект будет деактивирован, а значение его бита голосования будет учтено DTC при принятии решения об успешном завершении транзакции или об откате назад.

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

Для модификации указанных битов имеется несколько возможностей. Рассмотрим одну из них. Интерфейс IObjectContext объекта контекста имеет четыре метода, которые можно использовать для задания значений этих битов (см. таблицу 3.2)



Заметим, что интерфейс IContextState объекта контекста имеет методы, позволяющие узнать текущее значение указанных битов и задать их новые значения не парами, а по одному.

Наконец, используя Components Services, можно пометить некоторый метод как Auto-Done. В этом случае при возврате из этого метода произойдет деактивация объекта и он будет голосовать за успешное завершение текущей транзакции или за откат в зависимости от того, завершился ли вызов данного метода нормально или с ошибкой. При использовании указанных выше интерфейсов этот режим перекрывается явным заданием битов голосования и деактивизации.

Распространение транзакции происходит как обычно путем активации новых объектов и путем обращения к системам хранения данных. В последнем случае соответствующий менеджер ресурса регистрируется у DTC для участия в текущей транзакции.


Завершение транзакции

Завершается транзакция в следующих случаях

• Корневой объект транзакции деактивирован после возврата из метода, где был установлен бит деактивации

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

Если хотя бы для одного объекта бит голосования равен 0, выполняется откат назад без выполнения двух-фазного протокола.

• Корневой объект был уничтожен в связи с тем, что клиент освободил все указывающие на объект ссылки

В этом случае DTC завершает транзакцию и выполняется откат назад без выполнения двух-фазного протокола.

• Закончился временной интервал, выделенный на текущую транзакцию

По умолчанию длительность транзакции не превышает 60 секунд. По их истечении DTC завершает транзакцию и выполняется откат назад без выполнения двух-фазного протокола.


Безопасность

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

Различные протоколы безопасности могут быть реализованы в виде динамически компонуемых библиотек, так называемых провайдеров сервиса безопасности (SSP — Security Service Provider). Каждый SSP должен реализовать стандартный API — интерфейс провайдера поддержки безопасности (SSPI — Security Support Provider Interface), который изолирует распределенное приложение от конкретного протокола безопасности и позволяет повышать уровень безопасности используя более совершенные протоколы.

В следующем разделе рассматривается протокол Kerberos, который является протоколом по умолчанию для Windows 2000 и наилучшим на сегодня образом удовлетворяет потребности в обеспечении безопасности распределенных СОМ+ приложений. Заметим, что в СОМ+ может использоваться также протокол NT LAN Manager, который использовался ранее в СОМ приложениях, исполняемых под Windows NT. Однако, данный протокол не обеспечивает всех возможностей, предоставляемых протоколом Kerberos, в том числе тех, которые особенно важны для распределенных приложений с трех-уровневой архитектурой (взаимная аутентификация клиента и сервера, делегирование).


Kerberos

Kerberos является протоколом, обеспечивающим безопасность функционирования распределенного приложения. Разработан этот протокол в начале 80-х годов в Массачусетском технологическом институте и в настоящее время принят как стандарт, поддерживаемый различными независимыми производителями программного обеспечения. С этим стандартом можно познакомиться по RFC 1510. Windows 2000 реализует этот стандарт, и далее рассматривается именно эта реализация — Windows 2000 Kerberos. Основой для данного раздела является статья David Chappel "Exploring Kerberos, the Protocol for Distributed Security in Windows 2000", Microsoft System Journal, August 1999.


Основные сервисы

Любая реализация Kerberos обеспечивает следующие сервисы:

Аутентификация

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

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

Реализация Kerberos в Windows 2000 поддерживает два алгоритма шифрования:

DES — Data Encription Standard

Это первый официально предложенный правительством США стандарт в области шифрования, предназначенный для коммерческого использования. Длина ключа равна 64 битам, из которых 8 используются для контроля ошибок, и, следовательно, эффективная длина ключа равна 56 битам.

RC4

Более быстрый алгоритм шифрования потока данных с ключом переменной длины. Именно этот алгоритм используется по умолчанию в Windows 2000 Kerberos. Длина ключа в версиях, используемых на территории США, равна 128 бит, а за пределами США — 56 бит.

Проверка целостности данных

Здесь имеется ввиду, что получатель данных должен быть уверен, что в процессе передачи данные не были изменены. Реализация данного требования основана на вычислении контрольной суммы для каждого передаваемого пакета данных, которая в зашифрованном виде передается вместе с ним. Контрольная сумма есть значение некоторой функции от передаваемых данных, имеющей различные значения на различных данных. В Kerberos для вычисления контрольной суммы используется НМАС — Hash-based Message Authentication Code. Передача зашифрованной контрольной суммы гарантирует, что искажение (случайное или умышленное) передаваемых данных будет обнаружено на принимающей стороне.

Шифрование трафика

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

Упомянутые выше сервисы требуют определенных накладных расходов. Наиболее накладно шифрование трафика. Проверка целостности данных дороже аутентификации. При изучении собственно модели безопасности в СОМ+ мы увидим, что клиент и сервер совместно выбирают тот уровень безопасности, который удовлетворит их требования.


Основные сценарии использования сервисов Kerberos

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

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

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

Рассмотрим основные поля данных билета:

Нешифруемые поля

Домен

Домен, где был выдан этот билет. Предполагается, что на контроллере домена запущен сервер Kerberos — KDS (Key Distribution Server), который и выдает все билеты в данном домене,

Имя принципала

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

Шифруемые поля

Флаги билета

Ключ сессии

Билет выдается клиенту для предъявления некоторому серверу. Именно этим ключом будут шифроваться данные, передаваемые между данными клиентом и сервером в текущей сессии (или пока не истекло время действия билета)

♦ Домен

♦ Имя принципала

Клиент не имеет доступа к зашифрованным полям билета. Сопоставляя Домен и Имя приципала в шифруемых и нешифруемых полях билета, сервер может заметить ситуацию, когда предъявляемый билет был выдан другому клиенту,

Время выдачи билета

Время прекращения действия билета

Адреса хоста

Список IP адресов, с которых клиент может вызывать сервер

Авторизационные данные

Эти данные используются сервером для определения прав данного клиента при вызове методов данного сервера.

Для дальнейшего изложения удобно использовать следующие обозначения:

• — секретный ключ принципала, полученный хешированием его пароля. В случае клиента, в случае сервера, в случае KDS

• — ключ сессии между и

• — данные, зашифрованные ключом


Вход пользователя в систему

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

1. Ввод имени, домена и пароля

2. Запрос TGT (ticket-granting ticket) у KDS

Специальный билет TGT можно рассматривать как паспорт данного пользователя в данном домене (но принимаемый только KDS). При всех последующих обращениях к KDS в рамках данного сеанса клиентское приложение предъявляет KDS этот билет, доказывая идентичность пользователя. Запрос на получение TGT формируется автоматически и включает в себя

♦ Имя пользователя

♦ Временную метку — момент выдачи запроса, шифрованный ключом

3. Получение и ключа сессии между пользователем и KDS-

Получив запрос, KDC использует известные ему из Active Directory секретный ключ пользователя для расшифровки временной метки. Если расхождение между этой временной меткой и текущим временем по часам KDS не превышает 5 минут, делается вывод о том, что клиент аутентифицирован, т. к. только он мог воспользоваться секретным ключом для кодирования свежей временной метки.

Использование в протоколе Kerberos временной метки для аутентификации пользователя опционно и используется именно в реализации данного протокола в Windows 2000. При отсутствии временной метки аутентификация пользователя реально выполняется при попытке клиента расшифровать полученные от KDS данные, зашифрованные его секретным ключом, и получить ключ сессии между данным пользователем и KDS.

Билет TGT имеет структуру обычного билета. Заметим, что TGT зашифрован секретным ключом самого KDS. Никто кроме KDS, включая самого пользователя, не может прочитать шифрованные поля. В поле Ключ сессии этого билета содержится, что позволяет KDS не хранить ключи сессий со всеми своими клиентами. В поле Авторизационные данные содержатся взятые из Active Directory права данного пользователя и идентификаторы безопасности (SID) этого пользователя и всех групп, в которые он входит.

4. Запрос билета для локальных сервисов

Для обращения к локальным сервисам на машине пользователя последнему также необходим билет для локальных сервисов, который автоматически и запрашивается у KDS с предъявлением TGT.

5. Получение билета для локальных сервисов

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


Аутентификация клиента удаленным серверным приложением

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

Процесс аутентификации клиента С сервером S при первичном вызове состоит из следующих шагов:

• Клиент С посылает KDS запрос на получение билета для сервера S Этот запрос включает

♦ — билет TGT клиента С, зашифрованный ключом KDS

♦ имя сервера S

♦ — аутентификатор, зашифрованный ключом сессии для С и S

Аунтетификатор содержит текущее время и имя пользователя. Так как он зашифрован ключом сессии, никто кроме самого клиента С не может его сформировать. Временная метка усложняет попытку перехватить и послать этот запрос повторно от другого клиента (кроме того, KDS будет еще анализировать и IP адрес, с которого пришел запрос)

• KDS аутентифицирует клиента

Используя свой собственный секретный ключ KDS расшифровывает TGT клиента и извлекает из него ключ сессии. Используя ключ сессии, KDS расшифровывает аутентификатор. Аутентификация клиента считается успешной если

♦ Удалось расшифровать аунтетификатор

♦ Имя клиента из TGT совпадает с именем, указанным в аутентификаторе

♦ Временная метка из аутентификатора отличается от текущего времени по часам KDS не более чем на 5 минут

♦ Запрос пришел с одного из указанных в TGT IP адрессов

• При успешной аутентификации KDS высылает клиенту — билет для сервера S и — ключ сессии между клиентом и сервером S. Заметим, что билет Т зашифрован секретным ключом сервера S и, следовательно, клиент не сможет узнать и модифицировать его поля. В этот билет KDS включает некоторые данные из TGT (имя клиента, авторизационные данные). Кроме того, задаются флаги, срок действия билета, генерируется (случайный) и помещается в билет ключ сессии

• Клиент предъявляет серверу S билет и новый аутентификатор

• Сервер S аутентифицирует клиента, используя тот же алгоритм, что и KDS.

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

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

Возможность взаимной аутентификации появилась именно в Kerberos (в NT LAN Manager она отсутствовала). Механизм очень прост — сервер S после аутентификации клиента С высылает последнему временную метку из аутентификатора этого самого клиента, зашифрованную ключом сессии. Это доказывает клиенту, что данный сервер действительно является сервером S, т. к. для расшифровки аутентификатора ему был необходим ключ, который он мог извлечь только из билета Т, зашифрованного секретным ключом сервера S.


Реализация сервисов проверки целостности данных и шифрования трафика

Смысл и алгоритм реализации данных сервисов уже описан выше. Осталось заметить, что в качестве секретного ключа, известного только передающей и принимающей сторонам, можно использовать соответствующий ключ сессии.


Делегирование

Подробно понятие делегирования (как и понятие имперсонализация) будут рассматриваться далее. Здесь будет объяснена только его суть и описан механизм реализации делегирования в Windows 2000 Kerberos.

При имперсонализации клиента (с его согласия) сервер получает доступ к локальным ресурсам от имени данного клиента (уровень передаваемых прав доступа определяется клиентом). Однако имперсонализация не позволяет серверу делать удаленные вызовы других серверов от имени клиента. Конечно, выполняя некоторый вызов клиента С сервер S может послать удаленный вызов на другой сервер Q, но с точки зрения сервера Q вызов получен именно от сервера S, а не клиента С. Следовательно, даже если клиент С имеет особые права доступа к серверу Q, сервер S не сможет ими воспользоваться, выполнив простую имперсонализацию клиента С.

В чем причина ограниченности возможностей имперсонализации? Имперсонализация реализуется средствами конкретной операционной системы. При имперсонализации в Windows потоку в серверном процессе, выполняющему вызов пользователя, приписывается маркер доступа (access token), в котором записаны права данного пользователя, а не серверного процесса (как обычно бывает по умолчанию). Это позволяет получить от имени пользователя доступ к локальным ресурсам, т. к. при этом просто сопоставляются права доступа из маркера доступа со списком принципалов, которым разрешен или запрещен доступ к конкретному локальному ресурсу.

При удаленном доступе необходимо обеспечить такие сервисы как аутентификация, контроль целостности данных, шифрование трафика. Очевидно, что маркер доступа не содержит необходимых для этого данных. Фактически, говоря в терминах протокола Kerberos, Сервер S должен обратиться к KDS с запросом на получение билета для сервера Q, причем сервер Q при получении этого билета должен быть уверен, что получил он его от клиента С, а не от сервера S.

Данная возможность реализована в Kerberos и называется делегированием. Отметим, что делегирование отсутствует в NT LAN Manager.

Делегирование прав от клиента С серверу S осуществляется следующим образом:

• Клиент С посылает серверу S (которым он уже аутентифицирован) свой билет для KDS — и ключ сессии с KDS —

Сервер S должен иметь эти данные для того, чтобы получить от KDC билет для какого-либо другого сервера (например, Q), выданный как бы клиенту С. В отличие от сервера Q, KDS обмануть очень сложно (да и сервер Q можно обмануть только с помощью самого KDS). Поэтому от KDS и не скрывается то, что запрашивает билет сервер S, но выписать его необходимо на имя клиента С. Среди флагов билета TGT должен быть флаг forwardable, указывающий на то, что клиент С еще при первом обращении к KDS уведомил его о том, что в будущем он будет делегировать свои полномочия другим серверам (кстати, не любому серверу, а только тому, который зарегистрирован в домене как заслуживающий доверия (trusted) сервер).

• Сервер S посылает (от лица клиента С) запрос KDS на получение билета для сервера Q, в который включается, как обычно, имя сервера Q, билет и аутентификатор

Зметим, что в аунтетификатор включается имя клиента и он кодируется ключом сессии клиента С и KDS.

• KDS аутентифицирует клиента

Конечно, KDS замечает, что билет запрашивает не клиент С (вызов пришел не с того IP адреса, который указан в TGT). Однако он закрывает на это глаза в связи с тем, что в

TGT имеется флаг forwardable, и отправитель запроса знает ключ, который он мог получить только от клиента

• KDS посылает серверу S запрошенный билет, зашифрованный ключом сервера Q, и ключ сессии сервера S и сервера Q -

• Далее общение сервера S и сервера Q проходит как обычное общение клиента и сервера. Заметим, что с точки зрения сервера Q он работает с клиентом С. Это позволяет серверу S делегировать полученные от клиента полномочия серверу Q и т. д. Для этого у него есть все необходимое: TGT, закодированный ключом KDS, и ключ сессии


Удаленный вызов за пределы домена

Проблема удаленного вызова за пределы домена состоит в том, что клиент должен получить билет для сервера из другого домена у KDS этого другого домена. Но наш клиент не зарегистрирован в этом новом домене и, следовательно, не может получить там TGT.

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

• Клиент С из первого домена, желающий получить доступ к серверу R из второго домена обращается прежде всего к KDS своего домена. Первый KDS возвращает клиенту новую версию его TGT, зашифрованную паролем и ключ сессии для общения с KDS второго домена (это ключ хранится и в новой версии TGT).

• Клиент отправляет KDS второго домена новый TGT, зашифрованный ключом и аутентификатор, зашифрованный ключом сессии клиента и второго KDS.

• Второй KDS расшифровывает TGT и, доверяя первому KDS, возвращает клиенту билет для запрашиваемого сервера из своего домена.


Модель безопасности в СОМ+

Как уже говорилось ранее, определенная независимость модели безопасности, используемой в СОМ+, достигается за счет использования интерфейса SSPI, через который СОМ+ может пользоваться услугами различных провайдеров сервиса безопасности SSP. Для определенности остановимся на SSP, представленном реализацией в Windows 2000 протокола Kerberos. Будем полагать, что все клиенты и серверы в данном домене используют этот SSP.

Как и другие сервисы СОМ+, сервис безопасности может быть использован без написания какого-либо кода, за счет администрирования. Однако возможен (и в некоторых случаях очень полезен) программный доступ к этому сервису.

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


Активация серверного приложения клиентским приложением

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

• CLSID нужного класса

• Тип сервера (сервер в процессе клиента, локальный или удаленный)

• Указатель на структуру COSERVERINFO, содержащей некоторые данные об удаленной машине, о клиенте, о требуемом уровне безопасности

• IID запрашиваемого интерфейса (обычно IID_IClassFactory)

Единственный выходной параметр при успешном выполнении вызова возвращает указатель на запрашиваемый интерфейс.

Самый интересный для нас параметр — указатель на структуру COSERVERINFO. Предположим для начала, что этот указатель равен NULL.

В этом случае вся связанная с данным вызовом информация на стороне клиента определяется по умолчанию:

• Имя машины

В реестре машины клиента под ключом CLSID должен иметься раздел для нужного класса (CLSID класса), а в нем подключен RemoteServerName, определяющий имя удаленной машины, на которой установлено приложение, содержащее этот класс.

• Уровень аутентификации

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

1. None

Аутентификация не выполняется. В этом случае клиент анонимен для сервера и никакого контроля за передаваемыми данными не проводится.

2. Connect

Именно этот уровень обычно выбирается администратором как уровень по умолчанию. Аутентификация клиента выполняется при первом соединении клиента с сервером. Защиты передаваемых данных нет.

3. Call

Аутентификация клиента выполняется при каждом вызове метода сервера. Выполняется защита от перехвата и прочтения передаваемых данных стандартными средствами. Для этого выполняется шифрование последовательных номеров передаваемых пакетов.

4. Packet

Аутентификация выполняется при пересылке каждого пакета. Шифруются номера передаваемых пакетов.

5. Packet lntegrity

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

6. Packet Privacy

В дополнение к функциональности предыдущего уровня выполняется шифрование всех передаваемых данных.

Выбор того или иного уровня аутентификации определяется соотношением степени секретности данных и расходами на поддержку того или иного уровня безопасности. СОМ+ предоставляет возможность задавать разные уровни безопасности динамически, что позволяет гибко учитывать требования приложения к безопасности.


• Уровень имперсонализации

Этот уровень определяется только клиентским приложением (и серверным, когда оно выступает клиентом по отношению к другому серверному приложению). Уровень имперсонализации определяет объем прав доступа, которые клиентское приложение желает передать серверному.

Рассмотрим прежде всего сам механизм имперсонализации. После входа пользователя в систему все запускаемые (прямо и косвенно) им процессы имеют маркер доступа (access token), содержащий, в частности, такую информацию как SID (Security IDentifier) пользователя и SID всех груп, в которых зарегистрирован данный пользователь, а также различные его привилегии. Каждому локальному ресурсу приписан список ACL (Access Control List), содержащий элементы АСЕ (Access Control Entry), в каждом из которых разрешается или запрещается определенный набор способов доступа к данному ресурсу для некоторого SID. При попытке обращения некоторого потока к некоторому локальному ресурсу происходит последовательный просмотр ACL этого ресурса и все SID из маркера доступа процесса, содержащего данный поток, сопоставляются с SID из очередного АСЕ. Процесс просмотра ACL прекращается, как только выясняется, что данный поток обладает достаточными правами для доступа к ресурсу, либо, напротив, что доступ запрещается.

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

Объем передаваемых прав определяет сам клиент. В нашем случае (все параметры заданы по умолчанию) уровень имперсонализации для всех вызовов с данной машины на на удаленные машины определяет администратор. Вот возможные значения для этого уровня:

1. Anonimous

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

2. Identity

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

3. Impersonate

Данный уровень имперсонализации достаточен для того, чтобы сервер мог получать доступ ко всем локальным ресурсам от лица клиента. Это наивысший уровень имперсонализации для SSP NT LAN Manager.

4. Delegate

При этом уровне сервер может делать удаленные вызовы от лица клиента. Этот уровень появился в SSP Kerberos.

Что делать, если уровни аутентификации и имперсонализации, установленные по умолчанию администратором данной конкретной машины, не устраивают клиентское приложение? Имеются два выхода:

• Клиентское приложение задает собственный уровень безопасности по умолчанию в рамках своего процесса

• Клиентское приложение может динамически менять свои требования к уровню безопасности в процессе выполнения.

Для задания собственного уровня безопасности по умолчанию в рамках своего процесса клиентское приложение может вызвать функцию CoInitializeSecurity. Серверное COM+ приложение не должно вызывать эту функцию, т. к. она вызывается автоматически и вторичный ее вызов завершится ошибкой.

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

• Уровень аутентификации, который будет использован в данном клиентском приложении по умолчанию

• Уровень имперсонализации, который будет использован в данном клиентском приложении по умолчанию

• Указатель на структуру, где для каждого из поддерживаемых данным приложением сервисов аутентификации (Kerberos, NT LAN Manager….), задается сервис авторизации (NULL для Kerberos) и аутентификационная информация. Для Kerberos это:

♦ Имя пользователя

♦ Пароль

Возможность задания аутентификационной информации позволяет клиентскому приложению пользоваться правами не того пользователя, который запустил это приложение, а пользователя, чьи данные указаны при вызове CoInitializeSecurity.

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

Значение уровня аутентификации определяется в результате переговоров клиентского и серверного приложений. Именно, итоговый уровень аутентификации равен максимальному из уровней аутентификации по умолчанию, определенными клиентским и серверным приложениями. Уровень имперсонализации определяется исключительно клиентским приложением.

Возможность динамического изменения установок уровня безопасности основана на использовании интерфейса IClientSecurity. Если разработка серверного приложения сопровождалась подготовкой IDL файла с описаниями всех интерфейсов всех классов, включенных в приложение, то прокси каждого интерфейса (код которого сгенерирован Microsoft IDL), реализует интерфейс IClientSecurity. Таким образом, получив указатель на любой интерфейс некоторого объекта серверного приложения, клиентское приложение может (используя QueryInterfасе) получить указатель на IClientSecurity. Данный интерфейс имеет следующие методы:

Query Blanket

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

SetBlanket

Используется для задания нового уровня безопасности, который будет обеспечиваться для всех вызовов через данный прокси

СоруРгоху

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

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

Желая использовать иные, чем по умолчанию, требования к уровню безопасности, клиентское приложение может вызвать функцию CoGetClassObject с указателем на структуру COSERVERINFO, содержащую, в частности, следующие данные:

• Используемый при работе с данным объектом сервис аутентификации (например, Kerberos)

• Сервис авторизации (NULL в случае Kerberos)

• Уровни аутентификации и имперсонализации

• Указатель на структуру с аутентификационными данными:

♦ Имя пользователя

♦ Пароль

♦ Домен

В результате, при построении нового серверного объекта клиентское приложение получит прокси настроенный на специфицированный в COSERVERINFO уровень безопасности. Конечно, как и раньше, его можно динамически менять через интерфейс IClientSecurity.


Задание уровня безопасности на стороне сервера

Конфигурируя серверное приложение, администратор должен задать (например, с помощью Component Services) ряд параметров, определяющих требования к уровню безопасности со стороны данного приложения:

• Будет ли контролироваться доступ к приложению? Если будет, то на каком уровне: только на уровне процесса или также и на уровне компонент?

При контроле доступа к приложению только на уровне процесса никакая информация об используемом уровне безопасности не будет включена в контекст объекта. Это делает невозможным наиболее тонкий программный контроль безопасности. Стандартным для СОМ+ является второй вариант, когда уровень доступа контролируется и на уровне отдельных компонент, интерфейсов и методов. В этом случае в контекст объекта включается информация, которая делает возможным программное управление безопасностью.

• Уровни аутентификации и имперсонализации

Заданный уровень аутентификации будет использоваться при переговорах между клиентским и серверным приложениями относительно реально используемого уровня аутентификации (используется максимальный из уровней аутентификации, заданных для клиента и сервера). Уровень имперсонализации используется в тех случаях, когда какой-либо поток данного приложения будет вызывать другое серверное приложение, выступая в роли клиента.

• Идентификационные данные

Исполняемое серверное приложение выступает в роли принципала со своими именем, паролем, SID. Ранее (в СОМ) серверное приложение могло запускаться под идентификацонными данными запустившего его клиента, но такое решение было признано немасштабируемым. В СОМ+ администратор выбирает один из двух вариантов:

♦ Интерактивный пользователь

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

♦ Заданный пользователь

В этом случае администратор задает для конкретного серверного приложения уникальные имя и пароль, регистрируя приложение как нового пользователя в системе с определенными правами. Передача прав клиента серверному приложению возможна через механизм имперсонализации.

• Роли

В СОМ+ механизм ролей используется при авторизации клиентов. Авторизация клиента означает выяснение его прав, связанных с запуском серверного приложения, с вызовом того или иного метода серверного объекта. При конфигурировании серверного приложения администратор определяет список ролей, которым могут принадлежать клиенты. Далее к каждой роли приписываются те или иные пользователи, зарегистрированные в домене. Удобно приписывать к одной роли целую группу пользователей (с заданным SID). Одной роли можно приписать многих пользователей, и один пользователь может исполнять несколько ролей. Классические примеры ролей: клерк, менеджер.

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

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

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

С каждым вызовом некоторого метода серверного объекта связан так называемый контекст безопасности вызова (конечно, для этого должны быть заданы роли, и некоторая роль должна быть приписана этому методу или содержащему его интерфейсу, компоненту, но не всему приложению). Имеется интерфейс ISecurityCallContext, через который можно получить доступ к этому контексту. Для получения указателя на этот интерфейс можно вызвать из потока, исполняющего данный метод, функцию CoGetCallContext. В качестве входного параметра задается идентификатор запрашиваемого интерфейса (ID_ISecurityCallContext), через выходной параметр возвращается указатель на этот интерфейс.

Для нашей задачи контроля доступа к методу в зависимости от параметров можно использовать следующие методы данного интерфейса:

IsSecurity Enabled возвращает как выходной параметр логическое значение: TRUE, если основанная на ролях система безопасности задействована, и FALSE в противном случае.

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

Вообще, интерфейс ISecurityCallContext позволяет получить следующую информацию об исполняемом вызове:

• Длина цепи вызовов

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

• Минимальный уровень аутентификации, использующийся в данной цепи вызовов

• Идентификационная информация для всех клиентов, сделавших включенные в данную цепь вызовы:

♦ SID

♦ Имя

♦ Сервис аутентификации (например, Kerberos)

♦ Уровни аутентификации и имперсонализации

• Идентификационные данные клиента, сделавшего первый вызов в цепи

• Идентификационные данные клиента, сделавшего последний вызов в цепи

Теперь рассмотрим, как на стороне сервера решается вопрос имперсонализации клиента и делегирование его прав при удаленном вызове.

С помощью функции CoGetCallContext можно получить также указатель на интерфейс IServerSecurity. Именно этот интерфейс предоставляет метод без параметров ImpersonateClient, который при вызове в потоке, выполняющем вызов клиента, имперсонирует этого клиента, приписывая потоку маркер доступа с аутентификационными данными этого клиента. Вызов другого метода без параметров этого же интерфейса — RevertToSeif позволяет вернуть потоку исходный маркер доступа.

Как уже отмечалось ранее, если уровень аутентификации клиента был установлен не ниже чем в Impersonate, то после имперсонализации клиента сервер получает доступ (на время выполнения этого метода или до вызова RevertToSelf) ко всем локальным ресурсам от имени клиента пользуясь всеми его правами. Однако, если уровень имперсонализации установлен в Delegate, можно было бы ожидать, что данный поток получает право делать от лица клиента и удаленные вызовы. Однако, это не так. Ради совместимости с предыдущими версиями (СОМ под Windows NT), делегирование будет иметь место только после дополнительной настройки. Именно, после имперсонализации клиента С поток сервера S должен выполнить cloakig, т. е. скрыть свою сущность от вызываемого сервера Q под маской имперсонированного клиента С. Один из способов состоит в задании специального флага для прокси, который поток сервера S получил, выполнив вызов сервера Q. Для этого можно получить текущие установки уровня безопасности для данного прокси используя IClientSecurity::GetBlanket, добавить один из следующих флагов:

• EOAC_STATIC_CLOAKING

• EOAC_DYNAMIC_CLOAKING

и задать новые установки для прокси с помощью IClientSecurity::SetBlanket.

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

IServerSecurity::ImpersonateClient и IServerSecurity::RevertToSeif, текущий поток будет делать удаленные вызовы то от имени клиента С, то от имени сервера S.


Асинхронные компоненты (Queued Components)

Вначале несколько слов о терминологии. Термин Queued Components сложно перевести на русский язык, сохраняя стоящий за ним (в рамках СОМ+) смысл. Дословный перевод "организованные в очередь компоненты", который иногда используется в русскоязычной литературе, заставляет предполагать, что имеется в виду некоторая очередь различных компонентов, тогда как все совершенно иначе — имеется очередь вызовов к одному компоненту. В данном курсе выбран термин "асинхронные компоненты", который должен подчеркивать возможность коммуникации клиента и сервера, функционирующих в различные, непересекающиеся интервалы времени.

Здесь же необходимо отметить, что в СОМ+ появилась новая возможность объявить некоторый интерфейс как асинхронный. Асинхронные интерфейсы и асинхронные компоненты (Queued Components) — это разные технологии. Использование асинхронного интерфейса позволяет клиенту сделать вызов некоторого метода асинхронного интерфейса сервера и сразу же заняться чем-нибудь еще (при использовании обычного синхронного интерфейса клиент блокируется до получения ответа от сервера). Но при этом и клиент и сервер должны функционировать одновременно. В случае вызова, направленного асинхронному компоненту, клиент также не блокируется, как и при вызове асинхронного интерфейса. И, кроме того, клиент может делать вызов сервера в тот момент, когда сервер еще не запущен.

Теперь перейдем к более подробному рассмотрению именно технологии асинхронных компонент.

До сих пор мы рассматривали только синхронную коммуникацию клиента и сервера — клиент делает вызов и ждет ответа сервера. Только после получения ответа от сервера продолжается выполнение клиента. Иначе такая коммуникация называется коммуникацией в режиме реального времени.

В случае вызова асинхронного компонента

• Клиент может не дожидаться ответа сервера и продолжать свою работу.

• Вызовы от клиента к серверу могут накапливаться на стороне клиента и отправляться серверу все за один раз.

• Клиент может вызывать метод сервера даже в тот момент, когда сервер еще не запущен.

Рассмотрим ситуации, когда предпочтительно использовать асинхронные компоненты:

• В рамках бизнес-логики данного приложения в одних случаях коммуникация клиент/сервер должна выполняться в режиме реального времени, а в других допустима асинхронная коммуникация.

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

• Возникает проблема масштабируемости приложения

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

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

• Клиентское приложение работает на устройстве, не подключенном к сети

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

Позже он может подключить свой компьютер к сети, и все аккумулированные вызовы поступят к удаленным асинхронным компонентам.

Технология асинхронных компонент основана на технологии MSMQ (Microsoft Message Queuing), которая для полноты изложения и рассматривается кратко в следующем разделе (изложение основано на статье David Chappell "Microsoft Message Queue Is a Fast, Efficient Choice for Your Distributed Applications" в MSJ, July 1998 и материалах из MSDN).


MSMQ

Синхронная коммуникация удаленных клиента и сервера в СОМ+ основана на использовании протокола DCOM — объектно-ориентированной версии RPC. MSMQ предоставляет новую возможность — обмен сообщениями. Эта технология имеет следующие преимущества:

• Отправитель сообщения не блокируется в ожидании ответа от получателя.

• Отправитель и получатель могут работать в различные непересекающиеся интервалы времени.

• Отправитель может направить сообщение сразу же целой группе получателей.

• Сообщение можно сохранить на диске и повторить его обработку при сбое.

• Возможна пересылка сообщений по цепочке (аналог делегирования).

Таким образом, MSMQ (вместе с другими подобными технологиями) представляет особую парадигму проектирования распределенных систем. В качестве примера можно напомнить, что обмен сообщениями используется в распределенных вычислениях (MPI в кластерах ЭВМ). Можно предположить, что основанные на сообщениях технологии будут особенно популярны при проектировании распределенных систем на уровне выше предприятия, т. е. систем, отдельные части которых слабо или вообще не связаны друг с другом организационно.

Основными элементами технологии MSMQ являются:

• API, используемый приложением для отправки и получения сообщений

Данный API имеется в двух видах: множество С функций и множество СОМ объектов. MSMQ API обеспечивает следующие возможности:

♦ Создание и уничтожение очереди сообщений

♦ Открытие и закрытие существующей очереди сообщений

♦ Получение сообщения из очереди синхронно, асинхронно, с удалением из очереди, чтение некоторых элементов сообщения без удаления его из очереди и т. п.

• Сообщение

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

Body

Здесь размещается основное содержание сообщения (объемом до 4 МЬ). Передавать можно даже СОМ объект, поддерживающий интерфейс IPersistStream, методы которого позволяют записать состояние объекта в некоторый поток, а затем восстановить объект, инициировав его данными из потока,

Арр Specific

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

Delivery

Данное свойство определяет, где очередь должна хранить данное сообщение — на диске (значение Express), либо в оперативной памяти (значение Recoverable). В последнем случае сообщение не пропадет если, например, произойдет отключение питания. Но в первом случае минимизируются накладные расходы. Time То Be Received

Это похоже на параметр "время жизни" пакета в IP протоколе. Данный параметр задает число секунд, в течении которых сообщение должно быть получено адресатом. По истечении заданного времени MSMQ автоматически уничтожит сообщение.

Time То Reach Queue

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

Acknowledge

Задавая подходящее значение для этого параметра, отправитель может потребовать от MSMQ автоматически уведомить его о получении сообщения, о записи сообщения в очередь назначения, об уничтожении сообщения по той или иной причине.

Admin Queue

Здесь можно задать так называемую административную очередь, в которую MSMQ будет направлять уведомления, тип которых задан в свойстве Acknowledge.

Arrived Time

В этом свойстве MSMQ сохраняет момент времени, в который сообщение достигло очереди назначения.

Journal

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

Priority

Приоритет сообщения учитывается при его маршрутизации и при размещении в очереди.

Response Queue

Здесь можно указать имя очереди на стороне отправителя, в которую он хотел бы получить ответ от получателя сообщения (если, конечно, получатель пожелает отправить ответ).

Message ID

Данный идентификатор формируется автоматически. Получатель сообщения, желающий послать ответ, может указать этот идентификатор в свойстве Correlation ID, связывая тем самым ответ с полученным сообщением.

Correlation ID

При отправке ответа на полученное сообщение здесь следует указать значение свойства Message ID полученного сообщения.

Ряд свойств, приписываемых сообщению, связан с аутентификацией и шифрованием сообщений. Эти вопросы излагаются далее без упоминания имен соответствующих свойств. Предварительно стоить заметить, что эти же вопросы решаются системой безопасности СОМ+, но только для случая синхронных вызовов, т. к. вся система аутентификации, основанная на Kerberos или NT LAN Manager, основана на синхронной коммуникации клиента и сервера. Технология асинхронных компонент требует использования новой системы аутентификации.

Аутентификация позволяет получателю сообщения быть уверенным в том, что данное сообщение послано именно данным отправителем и в процессе передачи в сообщение не были внесены никакие искажения (целостность сообщения). Шифрование сообщения (его тела — Body) позволяет защитить передаваемую информацию от несанкционированного просмотра.

Аутентификация выполняется по запросу отправителя. Соответствующее требование задается в свойстве Auth Level, где определяется и тип электронной подписи, которую следует использовать при аутентификации в зависимости от типа сообщения.

Что делает MSMQ на стороне отправителя при запросе:

♦ Получает сертификат от приложения-отправителя

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

Сертификат прикрепляется к сообщению и используется менеджером очереди назначения при аутентификации полученного сообщения. До использования сертификата он должен быть зарегистрирован его владельцем в Active Directory данного домена.

♦ Получает личный ключ отправителя

Этот ключ, конечно, не будет отправляться в сообщении. Он будет использоваться при генерации цифровой подписи, которой будет подписано отправляемое сообщение.

♦ Вычисляет хешированное значение сообщения

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

♦ Формирует цифровую подпись для данного сообщения

Ранее полученное хешированное значение шифруется личным ключом отправителя.

♦ Сообщение с прикрепленными сертификатом и цифровой подписью отправляется в очередь назначения.

Что делает менеджер очереди назначения по получении сообщения:

♦ Вычисляет хешированное значение определенных полей полученного сообщения Используется указанный в этом же сообщении алгоритм,

♦ Извлекает публичный ключ из сертификата, полученного с сообщением, о

♦ Расшифровывает цифровую подпись

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

♦ Сравнивает хешированные значения отправленного и полученного сообщений:

• Хешированные значения равны

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

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

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

• Хешированные значения различны

Сообщение уничтожается и отправителю посылается соответствующее уведомление (при наличие его просьбы)

♦ Если сообщение еще не уничтожено, проверяется право отправителя на включение сообщения в данную очередь

При формировании очереди ей приписывается свой ACL — Access Control List. Если известен SID отправителя, то выполняется просмотр списка ACL до тех пор, пока не выяснится, что данный отправитель имеет или не имеет право на включение сообщения в данную очередь. Если SID отправителя не задан, то сообщение будет включено в очередь только при отсутствии каких-либо ограничений на эту операцию в ACL.

♦ Сообщение включается в очередь, если успешно завершилась проверка в ACL. В противном случае сообщение уничтожается. Отправителю посылается соответствующее уведомление (по его просьбе).

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

• Очередь

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

♦ Очереди, создаваемые приложением

При создании новой очереди необходимо определить, будет ли создаваемая очередь публичной (public) или личной (private), и будет ли она транзакционной (transactional) или нетранзакционной (non-transactional):

— Публичная очередь

Регистрируется в Active Directory и, следовательно, может быть обнаружена любым приложением. Интересно отметить, что среди параметров, приписываемых очереди при ее создании, имеется параметр, задающий тип очереди в виде GUID. Это позволяет приложениям искать публичные очереди нужного типа (например, очередь печати).

— Личная очередь

Регистрируется только на локальной машине. Другие приложения могут узнать об этой очереди от создавшего ее приложения (например, через свойство Response Queue полученного сообщения).

— Транзакционная очередь и нетранзакционная очередь

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

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

♦ Очереди приложения делятся на

— Очередь назначения

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

— Административная очередь

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

— Очередь ответов

В эту очередь приложения-получатели сообщений, отправляемых данным приложением, отправляют свои ответы,

♦ Системные очереди

Среди системных очередей, создаваемых MSMQ, отметим

— Журнал

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

— Dead-letter

Сообщения, которые не удалось отправить.

Завершая краткий обзор MSMQ, осталось рассмотреть типы MSMQ приложений:

• MSMQ сервер

Поддерживаются и используются очереди, менеджер очередей, MSMQ API, маршрутизация сообщений между очередями. Хотя бы один такой сервер должен иметься в любом распределенном приложении, поддерживающем обмен сообщениями.

• Независимый клиент

Поддерживаются и используются очереди, менеджер очередей, MSMQ API. Благодаря наличию собственных очередей независимый клиент может отправлять сообщения даже будучи отключенным от сети. Отправляемые сообщения накапливаются в некоторой очереди и передаются в очередь назначения автоматически при подключении к сети.

• Зависимый клиент

Поддерживается и используется только MSMQ API. Такой клиент может работать только при подключении к сети.


Архитектура асинхронных компонент

Вновь обратимся к рассмотрению технологии асинхронных компонент. Основными элементами ее архитектуры являются:

• Клиент

Интересно отметить, что клиент может вызывать экземпляр асинхронного компонента либо обычным, синхронным образом, либо асинхронно. Все зависит от способа активации объекта.

При явном использовании стандартного для СОМ способа (функции CoGetClassObject или CoCreateInstance [Ex]) будет создан экземпляр асинхронного компонента, пригодный только для обычных синхронных вызовов. Точнее, на стороне клиента будет сформирован обычный прокси, а на стороне сервера — стаб, которые и обеспечат вызовы методов экземпляра асинхронного компонента в реальном времени.

Для использования возможностей технологии асинхронных компонент клиент должен инициировать формирование экземпляра асинхронного компонента посредством использования моникеров ТИПОВ queue, new И функции CoGetObject. Например,

hr = CoGetObject(L" queue:/new: My_App.My_Class",

NULL, IID_My_interface, (void**)&ppv);

Моникер иногда называют интеллектуальным именем. Моникер — это объект, который знает как найти, активировать, инициализировать другой СОМ объект. Моникеры бывают как встроенные (нескольких типов), так и пользовательские. Используя встроенные моникеры и их композицию можно получить бесконечно много новых моникеров, что практически позволяет обойтись без разработки и реализации пользовательских моникеров, которые могут понадобиться только в весьма частных случаях.

Указанные выше моникеры типов queue и new являются встроенными, т. е. мы можем использовать их не заботясь о их реализации. Моникер освобождает клиента от участия в процессе активации объекта. Этот процесс зависит от типа объекта, и без использования моникеров клиент должен входить во все детали (например, как активировать объект, состояние которого хранится в заданном файле). Каждый моникер реализует интерфейс IMoniker, скрывающий за стандартным фасадом все детали конкретного алгоритма активации.

Функция CoGetObject в нашем случае делает следующее:

1. Создает контекст связывания.

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

2. Создает моникер по его строке инициализации (текстовое представление моникера, display name).

Технология моникеров позволяет построить единое иерархическое пространство имен для объектов различных типов. Строка инициализации моникера, связанного с данным объектом, и может рассматриваться как его имя.

Вызывается функция MkParseDispiayName, которая получает на входе

— Указатель на контекст связывания (полученный в предыдущем пункте)

— Строку инициализации формируемого моникера

Выходными параметрами являются

— Длина начальной части строки инициализации, распознанная данной функцией

— Указатель на построенный моникер

В нашем случае строкой инициализации моникера является строка "queue: /new: Mу_Арр. My_Class". Слева от первого разделителя "стоит префикс — тип моникера (в нашем случае queue), который должен быть зарегистрирован как ProgID в реестре. Справа от этого разделителя стоит специфичное для данного типа моникера строка, которая хранит некоторую информацию об объекте, с которым должен быть связан этот моникер. В нашем случае эта строка является строкой инициализации еще одного моникера, тип которого new. Таким образом, в данном случае мы имеем композицию двух моникеров.

Алгоритм работы функции MkParseDispiayName:

5. Сканируется строка инициализации моникера и выделяется ее префикс — queue.

6. В реестре для ProgID queue ищется путь к реализации соответствующего класса моникера типа queue. Либо объект этого класса, либо его экземпляр реализует интерфейс IParseDisplayName.

7. В функцию IParseDisplayName::ParseDisplayName (с такими же параметрами, как и у функции MkParseDispiayName) передается еще не обработанный остаток строки инициализации — "new: Му_Арр. Му_Сlass".

8. Функция IParseDisplayName:: ParseDisplayName сканирует эту строку и выделяет тип нового моникера new и строку, описывающую объект, с которым должен связываться моникер типа new — "Mу_Арр. My_ciass". Ну и, конечно, формируется моникер типа queue.

9. В реестре для ProgID new ищется путь к реализации соответствующего класса моникера типа new. Либо объект этого класса, либо его экземпляр реализует интерфейс IParseDisplayName.

10. В функцию IParseDisplayName::ParseDisplayName передается еще не обработанный остаток строки инициализации — "Mу_Арр. My_ciass", который уже не содержит префикса - типа еще одного моникера. Формируется моникер типа new, который должен будет связываться с СОМ объектом с ProgID My_App.My_ciass.

11. В результате вызова функции CreateGenericComposite формируется моникер, являющийся композицией двух построенных выше моникеров типов queue и new. Указатель на этот моникер возвращается как результат работы функции MkParseDispiayName. Возвращается и число обработанных символов в строке инициализации (все символы обработаны).

3. Выполняет связывание построенного в предыдущем пункте моникера — композиции моникеров queue и new с СОМ объектом.

Для композиции моникеров вызывается функция IMoniker::BindToObject. Входными параметрами этой функции являются:

— Указатель на контекст связывания

— Указатель на моникер, стоящий в композиции слева от данного (в нашем случае такого нет)

— GUID запрашиваемого интерфейса объекта, с которым выполняется связывание.

Единственным выходным параметром является указатель на запрошенный интерфейс.

Алгоритм работы функции IMoniker::BindToObject в случае композиции моникеров queue и new:

3. Вызывается функция IMoniker::BindToObject для ранее построенного (ссылка хранится в контексте связывания) моникера new.

При вызове задается ненулевой указатель на моникер, стоящий в композиции слева — моникер queue.

Если бы этот указатель был нулевым, то вызов функции IMoniker::BindToObject для МОНИКера new привел бы К построению нового экземпляра класса с ProgID Mу_Арр. Mу_сlass" (этот класс обязательно должен иметь фабрику класса С реализованным интерфейсом IClassFactory).

В нашем случае вызов функции IMoniker::BindToObject для моникера new сводится к преобразованию ProgID Mу_Арр. Mу_Class" в соответствующий CLSID, который передается МОНИКеру queue.

4. Вызывается функция IMoniker::BindToObject для ранее построенного (ссылка хранится в контексте связывания) моникера queue.

Моникер queue всегда используется в композиции со стоящим справа от него моникером типа new. Получив от последнего CLSID асинхронного компонента с ProgID "My_App.My_ciass", моникер queue формирует на стороне клиента прокси особого вида — Recorder, о котором речь пойдет в следующем пункте и который и обеспечивает асинхронность работы клиента и асинхронного компонента. Указатель на Recoder возвращается клиенту как указатель на запрошенный интерфейс асинхронного компонента.

Здесь надо еще упомянуть о параметрах, которые можно передавать моникеру типа queue. При обсуждении технологии MSMQ уже приводился некоторый (неполный) список свойств, которые можно приписывать передаваемому сообщению. Выбор значений этих свойств определяет время жизни сообщения до момента получения, необходимость аутентификации на отправителя на стороне получателя, использованные алгоритмы хеширования и шифрования, имя очереди назначения и т. п. Ясно, что упомянутый выше Recoder должен формироваться с учетом этих требований. Собственно, именно Recoder и будет формировать сообщения на стороне клиента. В связи с этим, уже при вызове функции coGetobject нужно иметь возможность задавать свойства отправляемых сообщений (в противном случае, они будут определены по умолчанию). Эти свойства можно указать в строке инициализации композитного моникера между "queue: " и "/" и в виде списка пар свойство = значение.

Recorder

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

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

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

Recorder. Клиент потенциально может сфабриковать сообщение с чужим SID, однако, он не может изменить SID отправителя. На принимающей стороне эти два SID сравниваются и при их равенстве аутентификация считается успешной.

Listener

Запускается при запуске приложения на принимающей стороне. Извлекает сообщение из очереди, анализирует переданный в нем CLSID асинхронного компонента и запускает соответствующий Player, которому и передается данное сообщение для последующей обработки.

Player

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

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

• Экземпляр асинхронного компонента

Очередь (транзакционная) создается автоматически при создании приложения с атрибутом queued. Имя очереди совпадает с именем приложения.

Приложение запускается либо администратором либо программно. Если в приложении одновременно имеются обычные (синхронные) и асинхронные компоненты, то при вызове любого синхронного компонента приложение запустится, запустится Listener и станут возможными вызовы асинхронных компонент. Рекомендуется не смешивать в одном приложение компоненты этих двух типов. В этом случае запуск приложения с асинхронными компонентами будет находиться под контролем администратора, что и соответствует духу технологии асинхронных компонент — доступ к асинхронным компонентам открывается в удобное для них время.

Асинхронный компонент должен содержать интерфейсы помеченные как QUEUEABLE. Все методы таких интерфейсов должны иметь только входные ([in]) параметры и не должны возвращать ничего специфического для данного метода. При вызове такого метода клиенту возвращается значение типа HRESULT не сервером, a Recoder. Это значение только уведомляет об успехе либо неудаче записи вызова в буфер. Параметры передаются по значению, либо это может быть указатель на СОМ объект, поддерживающий интерфейс IPersistStream, либо указатель на асинхронный компонент. Последняя опция позволяет клиенту отправить серверу свой объект, который поможет серверу возвратить ответ клиенту (например, через электронную почту).

В асинхронных компонентах можно использовать основанную на ролях систему контроля доступа к его функциональности. Однако, не поддерживается имперсонализация. Аутентификация уже обсуждалась ранее.


Модель событий

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

Рассмотрим прежде всего две ранее использовавшиеся модели событий, недостатки которых устранены в более совершенной модели событий СОМ+:

• Регулярный опрос издателя

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

Данная модель имеет следующие основные недостатки:

♦ Огромная доля запросов подписчиков к издателю завершается отрицательным ответом (ожидаемое событие еще не произошло), и ресурсы и подписчика и издателя тратятся впустую,

♦ От момента реального возникновения события до того момента, как об этом событии узнает подписчик, проходит определенное время (в среднем — половина длины временного интервала между двумя последовательными опросами издателя данным подписчиком).

• Тесно-связанные события

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

Модель тесно связанных событий устраняет упомянутые недостатки предыдущей модели, но не все. Остаются следующие вопросы:

♦ Реализация

Организация подписки, рассылка уведомлений большому числу подписчиков, контроль ошибок и т. п. — сложная задача, которую следует решать не разработчику приложения. Он должен сконцентрироваться на бизнес-логике, а не на построении инфраструктуры, общей для многих приложений. В рамках СОМ была выполнена стандартизация данной модели — описаны интерфейс IConnectionPoint и некоторые другие, но их реализация остается за разработчиком,

♦ Фильтрация уведомлений

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

♦ Активация подписчика

Издатель получает только указатель на интерфейс подписчика, но не знает его CLSID. Это означает, что подписчик все время должен находиться в активном состоянии. Издатель не сможет создать подписчика при появлении интересующего данного подписчика события.

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

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


Событийный класс

В данной модели, как и в модели тесно связанных событий, определяется интерфейс (событийный интерфейс), который должен реализовать каждый подписчик на события, представленные данным интерфейсом. Каждый метод этого интерфейса соответствует некоторому событию, входные [in] параметры метода конкретизируют событие, выходных параметров быть не должно. Уведомление подписчика о некотором событии достигается вызовом соответствующего метода событийного интерфейса, реализованного данным подписчиком.

Однако, если в модели тесно-связанных событий при возникновении некоторого события издатель самостоятельно вызывает соответствующий метод для каждого подписчика, то в данной модели все эти вызовы выполняются автоматически подсистемой событий. Точнее, издатель направляет вызов подсистеме событий, которая транслирует его всем подписчикам.

Событийный класс является основным элементом в данном механизме. Он представляет в подсистеме событий событийный интерфейс, который должны реализовать подписчики.

Рассмотрим все этапы описания и использования событийного класса:

• Разработка событийного класса

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

• Регистрация событийного класса в подсистеме событий

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

При регистрации событийного класса можно использовать и

Component Services и стандартные интерфейсы для работы с подсистемой событий. Рассмотрим вначале использование стандартных интерфейсов для работы с подсистемой событий:

♦ Прежде всего формируется экземпляр реализованного в системе класса с идентификатором CLSID_CEventClass и запрашивается указатель на интерфейс IEventClass этого класса.

♦ Интерфейс IEventClass используется для задания свойств класса CLSID_CEventClass, которые должны определить регистрируемый событийный класс:

— CLSID регистрируемого событийного класса задается вызовом метода IEventClass:: put_EventClassID (), где параметром является BSTR строка, задающая CLSID событийного класса.

— ProgID регистрируемого событийного класса задается вызовом метода IEventClass:: put_EventClassName (), где параметром является BSTR строка, задающая ProgID событийного класса.

— IID событийного интерфейса регистрируемого событийного класса задается вызовом метода IEventClass::put_FiringInterfaceID (), где параметром является BSTR строка, задающая IID событийного интерфейса.

Для преобразования GUID в BSTR строку можно использовать функцию StringFromIID.

♦ Формируется экземпляр реализованного в системе класса с идентификатором CLSID_CEventSystem и запрашивается указатель на его интерфейс IEventSystem. Данный интерфейс предоставляет методы для добавления, удаления и извлечения событийных классов и подписок из базы данных подсистемы событий.

♦ Вызывается метод store этого интерфейса, которому в качестве первого параметра передается PROGID_EventClass, что указывает на то, что сохраняется именно событийный класс, а в качестве второго параметра передается указатель на интерфейс IEventClass экземпляра класса с идентификатором CLSID_CEventClass, параметры которого настроены на регистрируемый событийный класс.

Событийный класс может конфигурироваться и с помощью Component Services. При этом можно задавать как специфические именно для событийного класса настройки, так и настройки, применимые ко всем СОМ+ компонентам:

♦ Асинхронность событийного класса

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

♦ Поддержка транзакций событийным классом

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

♦ Безопасность

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

Издатель, событийный класс и подписчик могут использовать все возможности, предлагаемые системой безопасности в СОМ+. Например, подписчик может выяснить, является ли издатель события тем, за кого себя выдает.

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

♦ Параллельная рассылка уведомлений

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

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

Более кардинальное решение проблемы связано с использованием фильтра на стороне издателя, что будет рассмотрено позднее.

♦ Идентификатор издателя

Событийному классу можно приписать уникальный идентификатор издателя. Это позволяет подписываться на события, распространение которых инициируется конкретным издателем.

Имеется еще один важный параметр класса событий, задание которого возможно только программным путем (используя административные компоненты для работы с СОМ+ каталогом) после регистрации событийного класса. Это свойство событийного класса MuitiInterfacePubiisherFilterCLSID. Данное свойство задает CLSID специального класса — фильтра, который на стороне издателя может регулировать рассылку уведомлений подписчикам. Данный фильтр будет обсуждаться позднее.

• Активация экземпляра событийного класса и инициализация рассылки уведомлений Активация экземпляра событийного класса выполняется издателем, желающим инициировать рассылку уведомлений о наступлении некоторого события. Издатель формирует экземпляр событийного класса, вызывая, например, функцию CoCreateInstance. Далее, получив указатель на событийный интерфейс данного событийного класса, он вызывает некоторый метод этого интерфейса, инициируя тем самым рассылку уведомлений всем подписчикам. Особенностью процесса активации экземпляра событийного класса является то, что подсистема событий автоматически синтезирует реализацию событийного интерфейса, обеспечивая активацию подписчиков и вызов соответствующего реализованного каждым подписчиком метода событийного интерфейса. Заметим, что в процесс рассылки уведомлений могут вмешаться два фильтра — фильтр со стороны издателя и фильтр со стороны подписчика.

Если не использовалась технология асинхронных компонент, и издатель дождался конца процесса рассылки уведомлений, то он может проанализировать код возврата типа hresult, который может принимать одно из следующих значений:

S_SUCCESS — уведомления были доставлены всем подписчикам,

EVENT_S_SOME_SUBSCRIBERS_FAILD — код успешного в целом завершения рассылки, однако доставка уведомлений некоторым подписчикам завершилась неудачей. Заметим, что на этом этапе невозможно определить подписчиков, не получивших уведомлений,

EVENT_E_ALL_SUBSCRIBERS_FAILD — неудача, никто из подписчиков не получил уведомления.

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


Издатель, подписчик и подписка

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

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

По поводу подписки следует заметить, что существует три ее типа:

Постоянная

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

Временная

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

PerUser

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

Как и в случае регистрации событийного класса, зарегистрировать подписку может только администратор, используя программирование и/или Component Services. Каждая подписка на некоторое событие является объектом, хранимым в подсистеме событий. Подписаться можно на событийный интерфейс в целом (на все его методы) или на некоторый метод событийного интерфейса. Если необходимо подписаться на несколько методов событийного интерфейса (но не на все), нужно отдельно оформить подписку для каждого метода. Заметим, что подписчик должен реализовать все методы событийного интерфейса.

Начнем с программной регистрации подписки:

• Прежде всего формируется экземпляр реализованного в системе класса с идентификатором CLSID_CEventSubscription (объект подписки) и запрашивается указатель на интерфейс IEventSubscription этого класса.

• Интерфейс IEventSubscription используется для задания свойств объекта подписки:

♦ GUID регистрируемой подписки задается вызовом метода IEventSubscription:: put_SubscriptioniD (), где параметром является BSTR строка, задающая GUID подписки. Этот идентификатор должен быть уникален в данном сервисе событий,

♦ Имя регистрируемой подписки задается вызовом метода IEventSubscription:: put_SubscriptionName (), где параметром является BSTR строка, задающая название подписки. Это читаемая строка, предназначенная для администраторов,

♦ Уникальный идентификатор издателя задается вызовом метода IEventSubscription:: put_Publisherid (), где параметром является BSTR строка, задающая GUID издателя. Если эта информация не задана, то подписчик будет получать уведомления вне зависимости от того, какой издатель инициировал рассылку уведомлений через данный событийный класс,

♦ CLSID событийного класса задается вызовом метода IEventsSbscription:: put_EventClassiD (), где параметром является BSTR строка, задающая CLSID событийного класса, для получения уведомлений от которого и регистрируется данная подписка.

♦ Идентификатор событийного интерфейса, на который регистрируется подписка (а событийный класс может реализовывать несколько событийных интерфейсов) задается вызовом метода IEventsSbscription::putInterfaceiD, параметр типа BSTR задает GUID этого интерфейса.

♦ Метод iEventSubscription::putMethodName вызывается для задания метода событийного интерфейса, на который регистрируется подписка. Параметром является BSTR строка, задающая название метода,

♦ Вызов метода IEventSubscription::putSubscriberCLSID с параметром типа BSTR строка, задающим CLSID подписчика, используется в случае постоянной подписки. Данный CLSID будет использоваться подсистемой подписки для активации подписчика,

♦ Вызов метода IEventSubscription::putMashineName позволяет В виде BSTR строки задать имя машины, на которой следует в случае постоянной подписки активировать подписчика,

♦ Вызов метода IEventSubscription::putSubscriberInterfасе используется для организации временной подписки. В качестве параметра передается маршалированный указатель (типа IUnknown*) на интерфейс уже активированного подписчика, на который следует посылать уведомления,

♦ Вызов метода IEventВubscription::putPerUser позволяет задать подписку третьего типа, когда подписка действует только при входе в систему определенного пользователя — владельца подписки. Этот вариант используется только в том случае, когда подписчик активируется на на той же машине, где зарегистрирован событийный класс. Параметр типа BOOL включает или выключает это ограничение на подписку,

♦ Задать владельца подписки можно вызывая метод iEventSuscription::putOwnerSID, параметр типа BSTR задает Security ID владельца подписки.

• Формируется экземпляр реализованного в системе класса с идентификатором CLSID_CEventSystem и запрашивается указатель на его интерфейс IEventSystem.

• Вызывается метод Store этого интерфейса, которому в качестве первого параметра передается PROGID_EventSubscription, что указывает на то, что сохраняется именно объект-подписка, а в качестве второго параметра передается указатель на интерфейс IEventClass экземпляра класса с идентификатором CLSID_CEventSubscription, параметры которого настроены на регистрируемую подписку.


Фильтр на стороне издателя

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

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

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

Initialize

Когда издатель инициирует активацию экземпляра событийного класса, подсистема событий автоматически активирует издательский фильтр (если свойство MuitiInterfacePublisherFilterCLSID было задано). В случае какой-либо ошибки при активации фильтра не будет активирован и событийный объект. После активации фильтра подсистема событий сразу же вызывает метод initialize, которому в качестве единственного параметра (входного) передается указатель на реализованный в системе интерфейс IMuitiInterfaceEventControl. Через этот интерфейс фильтр может получить доступ к коллекции подписчиков на произвольный событийный интерфейс и метод в событийном классе, для которого и подготовлен данный фильтр. Достаточно вызвать метод IMuitiInterfaceEventControi::GetSubscriptions, среди параметров которого:

[in] Идентификатор событийного интерфейса.

[in] Метод событийного интерфейса. Если название метода не существенно, то значение параметра NULL,

[in] Критерий отбора объектов-подписок для включения в коллекцию подписок. Здесь задается BSTR сторка, представляющая логическое выражение, в которое входят свойства подписки, операторы отношения, константы, логические операторы и скобки. Если параметр равен NULL, то используется некоторый критерий отбора по умолчанию,

[out] Указатель на IEventObjectCollection. Через этот интерфейс фильтр получает доступ к коллекции отобранных объектов-подписок и может в дальнейшем определять порядок их оповещения.

Заметим, что построенная при вызове IMuitiInterfaceEventControl::GetSubscriptions коллекция автоматически обновляется при добавлении/удалении подписок на данный событийный интерфейс (метод) в подсистеме событий.

PrepareToFire

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

[in] Идентификатор событийного интерфейса

♦ [in] Метод событийного интерфейса

Эти два параметра определяют тип события, рассылку уведомлений о котором инициирует издатель,

[out] Указатель на интерфейс IFiringControl

Это интерфейс реализован в системе. Фильтр использует его для вызова метода IFiringControl::FireSubscription. Этот метод принимает В качестве единственного (входного) параметра указатель на интерфейс IEventSubscription объекта-подписки, которому следует отправить уведомление о событии. Именно здесь фильтр может определить подписчиков и порядок рассылки им уведомлений.


Фильтр на стороне подписчика

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

Среди свойств объекта-подписки имеется свойство FilterCriteria. Это свойство администратор может задавать при регистрации подписки с помощью Component Services. Критерий фильтрации задается логическим выражением, содержащим параметры событийного метода, операторы отношений, константы, логические операторы и скобки. Таким образом, задав критерий фильтрации, администратор устанавливает так называемое параметрическую фильтрацию, результат которой определяется значениями параметров вызываемого метода событийного интерфейса. Данная фильтрация выполняется естественно после фильтрации, выполненной на стороне издателя. В связи с тем, что критерий параметрической фильтрации хранится в подписке на той же машине, где зарегистрирован событийный класс, никакие излишние вызовы на машину, где обычно активируется подписчик, не выполняются. Естественно, и сам подписчик активируется если только направляемое ему уведомление преодолело параметрический фильтр.


.NET Framework от Microsoft

Введение

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

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

В последнее десятилетие 20 века огромное влияние на определение путей дальнейшего развития технологии программирования оказывает Интернет. Глобальная сеть предстает перед программистами как новая вычислительная система, для программирования которой нужны новые технологии. Эти новые технологии должны учитывать децентрализованность на всех уровнях. Физически сеть состоит из множества узлов, соединенных каналами ограниченной пропускной способности. На логическом уровне единая система может создаваться независимо работающими, не знающими друг о друге разработчиками, использующими различные платформы и языки программирования. Организационно различные части системы могут управляться различными лицами и организациями. Как следствие особое внимание должно уделяться вопросам безопасности, надежности и масштабируемости проектируемых приложений.

Ранее в данном курсе упомянутые вопросы уже рассматривались при рассмотрении технологии COM (СОМ+). Новая технология (.NET) во многом основана на идеях СОМ. Пришло время, когда все программисты, работающие на платформе Windows, должны будут познакомиться с этими идеями.

Данный раздел посвящен введению в .NET. Обычно такое введение посвящено беглому рассказу о всех технологиях .NET, включая и работу с базами данных, и с Web, и методы построения пользовательского интерфейса. Данный курс посвящен компонентному программированию и упор будет сделан именно на компоненты.

В .NET о компонентах говорится особо. Имеется пространство имен system. componentModel, в котором определяются классы и интерфейсы, поддерживающие некоторую модель компонентного программирования. В рамках этой модели компонентами называются классы (производные от некоторых специальных классов или реализующие некоторые специальные интерфейсы), используемые прежде всего при разработке пользовательского интерфейса. Но уже в СОМ подход к понятию компонента был значительно шире. В данном курсе понятие компонента не определяется строго. Неформально понятие компонент связывается с технологией, ориентированной на распределенное программирование распределенных систем. Базы данных, интерфейс пользователя — это важные темы, но они не относятся исключительно к распределенным системам и, в связи с этим, не затрагиваются в данном курсе.

Можно выделить две компонентные модели, поддерживаемые технологией .NET. Во-первых, это уровень CLR (Common Language Runtime) — основа всей технологии .NET. Данная модель наиболее близка к СОМ. Во-вторых, это очень популярная сегодня модель XML Web-сервисов, в рамках которой возможно взаимодействие программ, исполняющихся на разных платформах при условии, что взаимодействующие программы понимают протокол SOAP.

Здесь мы будем говорить только о первой модели. Это представляется вполне естественным при переходе от таких тем как СОМ и СОМ+. И изложение будет основано на сопоставлении компонентной модели СОМ и модели, поддерживаемой в CLR.


Интерфейсы

Интерфейсы являются основой СОМ. Там их роль состоит в изоляции клиента от компонента, благодаря чему клиент без перекомпиляции может работать с разными версиями компонента. Кроме того, интерфейсы обеспечивают в СОМ независимость от языка программирования, на котором написаны клиент и компонент.

В СОМ и в СОМ+ мы видим огромное множество интерфейсов, совокупность которых можно рассматривать как некоторое компонентно — ориентированное расширение базового языка программирования (например, C++). Стоит отметить, что изучать эти интерфейсы весьма не просто в силу их многочисленности и отсутствия единой систематизирующей структуры.

Роль интерфейсов в .NET резко сокращена. На взгляд автора, теперь интерфейсы прежде всего служат для расширения множества типов, приписанных данному классу. В отличие от СОМ, где не было наследования реализации, в .NET класс может наследовать одному классу (по умолчанию классу System.Object) и любому числу интерфейсов, которые не имеют реализации и не наследуют корневому классу System.Object.

Относительное снижение значимости интерфейсов в .NET по сравнению с СОМ в .NET компенсируется следующим:

• Клиент больше не изолируется от компонента

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

• Многие интерфейсы более не нужны

Многие интерфейсы, играющие важную роль в СОМ, более не нужны в .NET. С помощью представленного ниже примера будет объяснено, почему, например, стал не нужен базовый интерфейс для СОМ — IUnknown. Те возможности, которые ранее программисты получали за счет реализации и использования большого числа интерфейсов, теперь можно получить за счет использования среды разработки и исполнения, предоставляемой .NET. Это и CLR, и общая система типов CTS, и промежуточный язык MSIL, на который транслируются программы со всех других языков, поддержанных .NET.


Сервер в процессе клиента

Рассмотрим в качестве примера процесс построения и использования сервера, который в рамках COM-терминологии получил бы классификацию "сервер в процессе клиента", т. к. он будет исполняться в адресном процессе клиента. Код представлен на С#.

Сервер

using System;

namespace MyServer {

public interface IAccumulator {

void Add(int sum);

}


public interface IAudit {

int Total();

}


public class Account: IAccumulator, IAudit {

protected int _sum = 0;


public void Add(int sum) {

_sum += sum;

}


public int Total() {

Console.WriteLine("Server AppDomain = {0}",

AppDomain.CurrentDomain.FriendlyName);

return _sum;

}

}

}


Клиент

using System;

using System.Reflection;

using MyServer;


public class MyApp {

public static void Main() {

Account a = new Account();

IAccumulator iAcc = a as IAccumulator;

if (iAcc!= null) {

iAcc.Add(3);

iAcc.Add(5);

}


IAudit iAud = iAcc as IAudit;

if (iAud!= null)

Console.WriteLine("Total = {0}", iAud.Total());


Console.WriteLine("Client AppDomain = {0}\n",

AppDomain.CurrentDomain.FriendlyName);


Type iAuditType = iAud.GetType();

Methodlnfо[] Methods = iAuditType.GetMethods();

foreach (MethodInfо Method in Methods)!

Console.WriteLine(Method.ToString());

}

}

}


В данном примере сервер реализован классом Account. Этот класс реализует два интерфейса:

IAccumulator

Этот интерфейс позволяет клиенту отправлять на счет, поддерживаемый сервером, некоторую сумму (метод Add)

IAudit

Данный интерфейс позволяет клиенту узнать текущее состояние счета (метод Total)

И интерфейсы, и реализующий их класс Account определяются в рамках одного пространства имен MyServer.

Стоит еще обратить внимание на то, что метод Total не только возвращает текущую сумму на счете, но и выводит на консоль имя домена приложения, в котором выполняется сервер. Тут необходимы дополнительные пояснения. Управляемый код, порождаемый компилятором с C# и использующий все сервисы CLR, является типо-безопасным кодом, который в процессе своего выполнения не может повредить данные, к которым он не должен иметь доступа. Это позволяет в рамках одного процесса организовать несколько логических процессов — доменов приложений (Application Domain), которые делят одно адресное пространство и границу между которыми может пересекать поток. Каждое приложение работает в рамках одного домена приложения. За счет "легковестности" доменов приложений, их использование более эффективно, чем использование для каждого приложения отдельного процесса. Выводимая на консоль информация об имени домена приложения позволит нам проверить, что и клиент и сервер действительно выполняются в рамках одного домена приложения.

Теперь обратимся к клиенту. Используется пространство имен MyServer, в котором определен класс Account. Кроме того, используется пространство имен System. Reflection. В этом пространстве определены классы, обеспечивающие механизм рефлексии, т. е. доступ к метаданным.

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

Во-первых, теперь нет нужды в методе QueryInterface. Оператор as позволяет безопасно привести ссылку на объект к ссылке на любой реализуемый им интерфейс. Если полученная ссылка на интерфейс IAccumulator не равна null, то это означает, что класс Account действительно реализует интерфейс IAccumulator и клиент может отправить на счет две суммы (3 и 5), используя метод Add.

Теперь, имея ссылку на интерфейс IAccumulator, можно перейти к ссылке на интерфейс IAudit. В случае реализации этого интерфейса вызывается его метод Total и на консоль выводится сумма счета и имя домена приложения, в котором выполняется клиент.

Во-вторых, кардинально меняется подход к управлению памятью. Заметим, что мы не освободили память, отведенную под экземпляр класса Account. В СОМ управление временем жизни объекта выполняется с помощью подсчета числа сделанных на него ссылок. Когда число ссылок становится равным 0, объект вызывает собственный деструктор.

Этот механизм имеет много недостатков. Тут и ошибки при подсчете сделанных ссылок (клиент должен вызывать методы AddRef и Release интерфейса IUnknown соответственно при получении новой ссылки на объект и при ее освобождении), и возможность появления циклических ссылок, при наличии которых счетчик числа ссылок на некоторые объекты никогда не обратится в 0. Во всех случаях возникает утечка памяти, весьма нежелательная в случае долгоживущих серверных приложений.

В.NET используется автоматическое управление выделением и освобождением памяти. Менеджер кучи быстро выделяет память для вновь создаваемых объектов благодаря наличию указателя на начало непрерывного свободного пространства в куче. Если этот указатель приближается к границе кучи, и менеджер не может выделить память по очередному запросу, выполняется дорогостоящий процесс освобождения более не используемой памяти. Сборщик мусора находит ссылки на все используемые объекты, и перемещает их образы в куче к ее началу, модифицируя все адреса перемещаемых объектов в данных и в коде. Вся остальная часть кучи после последнего используемого объекта считается свободной.

Конечно, со сборкой мусора связаны свои проблемы:

Недетерминированность

Момент начала сборки мусора заранее как правило не известен. Если объект связывает ценные ресурсы (открытые файлы, соединения с базой данных), то клиент должен явно вызвать метод Dispose, в котором объект должен реализовать освобождение упомянутых и других ценных ресурсов не дожидаясь начала работы сборщика мусора.

Если надежд на клиента мало, класс должен реализовать метод Finalize, в котором также должно выполняться освобождение ресурсов. В отличие от метода Dispose, метод Finalize вызывается автоматически во время сборки мусора до уничтожения объекта.

Распределенная сборка мусора

Сборщик мусора не имеет доступа к удаленным объектам. Их освобождение регулируется другим механизмом, т. н. лизингом (leasing).

Последняя часть кода клиента демонстрирует применение механизма рефлексии. Цель — вывести на консоль сигнатуры всех методов, которые доступны через интерфейс IAudit. Заметим, что это будут не только все методы, реализованные в классе Account, но и все методы, реализованные в корневом классе System.Object, от которого неявно наследуют все классы в .NET, в том числе и класс Account.

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

Таким образом, механизм рефлексии предоставляет ту информацию, которая в рамках СОМ хранилась в IDL-файлах, в заголовочных файлах, в библиотеке типов.

Теперь рассмотрим вопросы компиляции и развертывания построенного приложения.

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

Предположим, что код клиента содержится в файле MуАрр. cs рабочего каталога, а код сервера в подкаталоге MyServer в файле MyServer.cs. Следующие команды обеспечивают компиляцию и сборку клиентского и серверного приложений:

>csc /t: library MyServer.cs

>csc /r: MyServer\MyServer.dll MyApp.cs

В первой строке вызывается компилятор с C# и код сервера MyServer.cs компилируется в сборку MyServer.dll. Ключ /t: library говорит именно о том, что нужна сборка типа DLL, т. е. ее можно загружать в домен клиентского приложения.

Во второй строке компилируется код клиента MуАрр. cs. По умолчанию получаем сборку МуАрр. ехе. Ключ /r: MyServer MyServer.dll дает статическую ссылку на сборку, содержащую компонент Account.

Отметим, что в данном случае сборка с сервером находится в одном каталоге с клиентом (точнее, в его подкаталоге, имя которого совпадает с именем сборки). Такой способ развертывания является самым простым. Он допускает перенос приложения в другой каталог, на другую машину простым копированием. Нет никакой регистрации.

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

Теперь запустим клиентское приложение

>csc МуАрр. ехе

Server AppDomain = МуАрр. ехе

Total = 8

Client AppDomain = МуАрр. ехе

Int32 Total()

Void Add(Int32)

Int32 GetHashCode()

Boolean Equals(System.Object)

System.String ToString()

System.Type GetType()

Мы видим, что и сервер и клиент выполняются в одном домене приложения. Его имя совпадает с именем клиентского приложения. Это связано с тем, что клиентская сборка загружается при запуске в домен приложения по умолчанию, который потом и получает свое имя, совпадающее с именем загруженной сборки. Серверная сборка загружается туда же.

Ниже выводятся сигнатуры всех методов, доступных через ссылку на любой интерфейс класса Account. Это и оба метода, реализованные в классе Account, и методы, унаследованные от System.Object.


Регистрация сборки в Global Assembly Cache

GAC — Global Assembly Cache, это то место, где можно развертывать сборки, которые должны быть доступны многим клиентам. Для развертывания сборки в GAC ее нужно подписать. Точнее, сборке присваивается версия, ее содержимое хешируется и хешированное значение шифруется личным ключом разработчика. В сборку (в ее манифест), включаются версия сборки, шифрованный хеш — электронная подпись, публичный ключ. В результате, благодаря наличию публичного ключа, можно проверить сохранность сборки, т. е. перед запуском клиентского приложения удостовериться, что именно с этой сборкой данное клиентское приложение было откомпилировано.

Ниже приведены все необходимые команды, обеспечивающие подписание сборки и ее регистрацию в GAC:

>csc /t: module MyServer.cs

>sn — k my.snk

>al /out: MyServer.dll MyServer.netmodule /keyfile: my.snk

>gacutil — i MyServer.dll


>sn — Tp MyServer.dll


Public key is

002400000480….

Public key token is 047772996d01a6d4

Для получения подписанной сборки (сборки со строгим именем) нужно откомпилировать код серверас параметром /t: module. В результате получим модуль MyServer.netmodule, содержащий (как и сборка) код на MSIL, но не содержащий (в отличие от сборки) манифеста.

Далее генерируется (если не была сгенерирована ранее) пара ключей — личный и публичный ключи, с помощью утилиты sn (strong name). Эта пара записывается в файл my. snk.

На следующем этапе компоновщик сборки al (assembly linker) формирует подписанную сборку MyServer. dll, используя модуль MyServer.netmodule и файл с ключами my.snk.

Для регистрации сборки в GAC можно использовать утилиту gacutil.

Для работы с подписанной сборкой клиент должен знать ее версию и публичный ключ (или его хешированное значение — токен публичного ключа). Последнее можно получить с помощью утилиты sn с параметром —Tр, указав подписанную сборку.

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

В случае продемонстрированной в предыдущем примере статической ссылки, которую можно было бы применить и в этом случае (в случае сборки зарегистрированной в GAC), клиент связывается с нужной сборкой на этапе компиляции. В этом случае в манифест сборки клиента записывается вся нужная информация об используемых им сборках. Это позволяет еще до запуска клиента узнать — имеются ли в наличии нужные клиенту компоненты. Благодаря использованию подписанной сборки при ее наличии можно гарантировать ее совместимость с клиентом.

Но иногда нужно связаться со сборкой динамически, в процессе исполнения клиента. В этом случае на этапе компиляции наличие нужной сборки (и нужного типа в данной сборке) не проверяется.

using System;

using System.Reflection;


public class MyApp {

public static void Main() {


Assembly assem = Assembly.Load("MyServer, " +

"Version=0.0.0.0, " +

"Culture=neutral, " +

"PublicKeyToken=047772996d01a6d4");


Type accountType = assem.GetType("MyServer.Account");

if (accountType!= null) {

MethodInfо addMethod = accountType.GetMethod("Add");

MethodInfo totalMethod =

accountType.GetMethod("Total");

Object obj = Activator.CreateInstance(accountType);

Object [] args = new Object[1];

args [0] = 3;

if (addMethod!= null){

addMethod.Invoke(obj, args);

args [0] = 5;

addMethod.Invoke(obj, args);

}

if (totalMethod!= null)

Console.WriteLine("Total = {0}",

totalMethod.Invoke(obj, null));

}

}

}


В данном примере статический метод Load класса Assembly загружает из GAC сборку с заданным именем, версией (по умолчанию 0.0.0.0), культурой (по умолчанию neutral) и токеном публичного ключа. Далее, используя механизм рефлексии, получаем ссылку на объект типа Tуре, содержащий всю информацию о классе Account из данной сборки. Если информация об этом типе имеется в загруженной сборке, получаем информацию о методах Add и Total. Потом создаем экземпляр класса Account и формируем массив аргументов. В данном случае массив состоит из одного элемента со значением 3.

Если метод Add реализован в классе Account, вызываем этот метод на построенном объекте, передавая ему сформированный массив аргументов. Модифицируем этот массив, занося в него 5, и еще раз вызываем метод Add.

И, наконец, если метод Total также реализован, вызываем его без аргументов и выводим полученное значение на консоль.

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


Удаленный сервер

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

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

Сервер

//MyServer.cs

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;


namespace MyServer {


public interface IAccumulator {

void Add(int sum);

}

public interface IAudit {

int Total();

}


public class Account: MarshalByRefObject, IAccumulator, IAudit {


protected int _sum = 0;


public void Add(int sum) {

_sum += sum;

}


public int Total () {


Console.WriteLine("Server AppDomain = {0}",

AppDomain.CurrentDomain.FriendlyName);

return _sum;

}

}

public class AccountApp {

public static void Main() {


HttpChannel myChannel = new HttpChannel (8080);

ChannelServices.RegisterChannel(myChannel);


RemotingConfiguration.RegisterWellKnownServiceType (

typeof(Account),

"Account",

WellKnownObjectMode.Singleton);


Console.WriteLine("Server is listening");

Console.ReadLine();

Console.WriteLine("Bye");

}

}

}


Клиент

//МуАрр. сs

using System; using MyServer;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;


public class MyApp {

public static void Main() {

HttpChannel с = new HttpChannel();

ChannelServices.RegisterChannel(c);


Account a = (Account)Activator.GetObject (

typeof(Account), "http://localhost:8080/Account",

WellKnownObjectMode.Singleton);


a. Add(5);

Console.WriteLine("Total = {0}", a.Total());

Console.WriteLine("Client AppDomain = {0}",

AppDomain.CurrentDomain.FriendlyName);

}

}


Для использования каналов включаем в код сервера ссылки на пространства имен, содержащие нужные классы:

System.Runtime.Remoting,

System.Runtime.Remoting.Channels,

System.Runtime.Remoting.Channels.Http.

Последнее пространство имен необходимо для работы с каналом, использующим http. Есть возможность работы с tcp каналом (что в несколько раз быстрее), но http канал по умолчанию передает сообщения по протоколу SOAP в стандартизованном XML формате, a tcp по умолчанию передает SOAP сообщения в нестандартизованном бинарном формате. Иными словами, при использовании html канала потенциальный клиент может вообще не принадлежать миру .NET и платформе Windows.

Необходимо обратить внимание на то, что класс Account теперь наследует классу MarshalByRefObject. Все объекты в .NET делятся на три типа:

1. передаваемые по ссылке

2. передаваемые по значению

3. не передаваемые за пределы своего домена приложения

Наш класс должен быть передаваем по ссылке. Это обеспечивается тем, что он наследует классу MarshalByRefObject. При работе с таким объектом клиент реально работает не с самим объектом, а с его прокси. Сам же объект формируется удаленно.

Наш удаленный объект должен жить в некотором приложении. Здесь мы описываем консольное приложение, функция Main (в коде сервера) — его точка входа.

В данном приложении формируется объект — http-канал (с указанием произвольного номера порта) и этот канал регистрируется в системе Remoting.

После этого в этой же системе регистрируется класс, к которому будет обеспечен доступ удаленных клиентов. Задается его тип, его URI — определяемый разработчиком идентификатор, и режим работы объекта. В данном случае это Singleton. Это означает, что такой объект создается после получения первого запроса от какого-либо клиента и далее живет (сохраняя состояние), отвечая на запросы как этого, так и других объектов.

После запуска серверного приложения на консоль сервера выдается уведомление о том, что сервер готов к работе. С этого момента сервер ожидает получения сообщений по каналу http на порт 8080. Остановить работу сервера можно нажав на клавишу .

Теперь обратимся к клиенту. Он регистрирует один из каналов, зарегистрированных сервером (но не указывает номер порта).

Метод Activator.Getobject возвращает ссылку на прокси, сам же объект при этом не создается. Если он не был создан ранее, то он будет создан при получении первого запроса от какого-либо клиента. Среди параметров задаются тип компонента, путь к нему и режим его работы. Все остальное не изменилось по сравнению с клиентом, предназначенным для работы с сервером в домене клиентского приложения.

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

Консоль сервера

>csc /t: exe MyServer.cs

>MyServer.ехе

Server is listening

Server AppDomain = MyServer.exe

Server AppDomain = MyServer.exe


Bye

>


Консоль клиента

>csc /r: MyServer\MyServer.ехе МуАрр. cs

>MyApp.ехе

Total = 5

Client AppDomain = MyApp.exe

>MyApp.ехе

Total = 10

Client AppDomain = MyApp.exe

>


Несколько комментариев, касающихся удаленных компонентов.

В.NET определены три типа режимов работы удаленных объектов:

1. SingleCall

Удаленный объект активируется на стороне сервера только при получении вызова какого-либо его метода от клиента и по выполнении этого метода сразу же уничтожается.

2. Singleton

Удаленный объект совместно используется несколькими клиентами, сохраняя состояние между вызовами. Для контроля за его жизненным циклом используется распределенная сборка мусора — лизинг. При этом объект получает некоторое время жизни, продлеваемое автоматически после каждого нового вызова. После исчерпания этого времени следует запрос к спонсору объекта (обычно к самому клиенту) о продлении жизни компонента. При отсутствии ответа объект уничтожается. Такой механизм позволяет экономить на коммуникации между клиентом и удаленным сервером.

3. Активируемый клиентом

Такой объект доступен только одному клиенту и сохраняет состояние между вызовами. Для контроля за жизненным циклом объекта используется лизинг.

Говоря об удаленных компонентах и сопоставляя .NET с СОМ нельзя не вспомнить об апартаментах. В одном процессе может быть несколько апартаментов, и ссылка на объект, полученная в одном апартаменте, не может непосредственно использоваться в другом апартаменте. Необходимо выполнить ее маршалинг между апартаментами. В.NET такой проблемы нет. Любая объектная ссылка (прокси на удаленный компонент) используется глобально по всему домену приложения.

И наконец необходимо упомянуть о передаче объектов по значению. Это возможно и полезно если объект небольшой. В этом случае клиент получает не прокси на объект, а его копию. Для передачи объекта по значению необходимо, чтобы соответствующий класс был определен с пользовательским атрибутом [Serializable] для использования стандартного метода сериализации, либо можно самостоятельно реализовать интерфейс ISeriaiizabie для задания собственного способа сериализации. Пользовательские атрибуты и вопросы их использования будут рассмотрены далее.


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

До сих пор мы не рассматривали обработку ошибок. В СОМ каждый метод каждого интерфейса (за исключением методов AddRef и Release интерфейса IUnknown) должен возвращать значение типа HRESULT, говорящее об успехе или неудаче вызова метода и о причине неудачи.

Получатель этого значения должен его обработать. Но у него не всегда есть возможность сделать это.

В.NET используется технология обработки исключений — блоки try, catch, finally, инициализация исключения — throw.

В нашем распределенном примере при запуске клиента без запуска сервера возникает никем не перехваченная ошибка. Включим часть кода клиента, где происходит работа с сервером, в блок try и добавим блоки catch и finally для задания реакции на ошибки и на выход из блока try.

using System;

using MyServer;

using System. Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Net;


public class MyApp {

public static void Main() {


HttpChannel с = new HttpChannel();

ChannelServices.RegisterChannel(c);

try {

Account a = (Account)Activator.GetObject(typeof(Account),

"http://localhost:8080/Account",

WellKnownObjectMode.Singleton);

a. Add(5);

Console.WriteLine("Total = {0}", a.Totalf));

}

catch(WebException e) {

Console.WriteLine(e.Message);

}

catch(Exception e){

Console.WriteLine(e.Message);

}

finally {

Console.WriteLine("Bye");

}

}

}

Первый блок catch перехватывает специальное исключение WebException, связанное именно с работой http канала, а второй блок catch перехватывает все остальные исключения. Независимо от наличия ошибки и ее типа всегда отрабатывает блок finally.

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

Клиент запущен до запуска сервера

>МуАрр. ехе

The underlying connection was closed: Unable to connect

to the remote server

Bye

>

Клиент запущен после запуска сервера

>МуАрр. ехе

Total = 5

Bye

>

Необходимо еще одно дополнительное замечание. Если имеет место цепочка вызовов, то необработанное исключение поднимается вверх по цепочке вызовов до блока catch, способного его обработать. Это позволяет сделать обработку в наиболее удобном месте. Возможна и частичная обработка ошибки на каждом уровне при ее передаче вверх по стеку вызовов.


Синхронизация

Теперь рассмотрим случай двух клиентов, параллельно посылающих некоторые суммы на один и тот же счет, поддерживаемый нашим сервером.

Вспомним, что в СОМ использовались апартаменты типа STA для объектов, не допускающих параллельный вызов своих методов, и типа МTА для потоко-безопасных объектов. В.NET по умолчанию считается, что все объекты являются потоко-безопасными.

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

Сервер

……

using System.Threading;

……

public class Account: MarshalByRefObject, IAccumulator, IAudit {

……

public void Add(int sum) {

int s = _sum;

Thread.Sleep(1);

_sum = s + sum;

}

…..


Клиент посылает на сервер 1000 раз по 5 условных единиц и после этого выводит на свою консоль общую отправленную сумму.

Клиент

…..

int sentTotal = 0;

for (int i = 0; i < 1000; i++) {

a. Add(5);

sentTotal +=5;

}

Console.WriteLine("Sent totally by this client = {0}",

sentTotal);

…..

Запустим сервер и затем с небольшим временным интервалом двух клиентов. Каждый из запущенных клиентов по завершении своей работы выведет на свою консоль следующие строки:

>МуАрр. ехе

Sent totally by this client = 5000 Bye

>

Несложно написать клиентскую программу, которая обращается к работающему серверу и выводит на свою консоль текущую величину счета. Здесь для краткости эта программа (total.ехе) не приведена, но для примера приводится результат ее работы (программа total была запущена после завершения работы обоих клиентов)

>total.ехе

Received by server from all clients = 7240

Bye

>

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

Простое решение проблемы — создать контекст синхронизации, в который и поместить серверный компонент. В этом случае новый поток блокируется при входе этот контекст, если в нем исполняется какой-либо другой поток. Для этого достаточно пометить наш класс Account атрибутом [Synchronization ()] и привязать его к контексту, в котором он будет создаваться. Для этого класс Account должен наследовать классу ContextBoundObject, который в свою очередь является производным от класса MarshalByRefObject.

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

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Threading;

using System.Runtime.Remoting.Contexts;


namespace MyServer {


public interface IAccumulator {

void Add(int sum);

}


public interface IAudit {

int Total();

}


[Synchronization()]

public class Account: ContextBoundObject, IAccumulator, IAudit {

protected int _sum = 0;


public void Add(int sum) {

int s = _sum;

Thread.Sleep(1);

_sum = s + sum;

}


public int Total() {

return _sum;

}

}

public class AccountApp {

public static void Main() {

HttpChannel myChannel = new HttpChannel(8080);

ChannelServices.RegisterChannel(myChannel);

RemotingConfiguration.RegisterWellKnownServiceType {

typeof(Account),

"Account",

WellKnownObjectMode.Singleton);

Console.WriteLine("Server is listening");

Console.ReadLine();

Console.WriteLine("Bye");

}

}

}


И несколько комментариев. В.NET синхронизацию можно обеспечить либо с помощью критических секций, встраиваемых в код компонента, либо декларативно с помощью контекста синхронизации. Последнее похоже на использование понятия активности в СОМ+.

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

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


NET и аспектно-ориентированное программирование

Введение

Говоря про компонентное программирование нельзя не упомянуть про парадигму аспектно-ориентированного программирования (АОП). Элементы этой парадигмы встречаются в области технологии программирования уже достаточно давно. Это субъектно — ориентированное программирование (subject — oriented programming) [SOP1, SOP2], композиционные фильтры (composition filters) [CF1, CF2], адаптивное программирование (adaptive programming) [AP1, АР2]. В наиболее явной форме формулировка данной парадигмы представлена в работе [АОР1]. Хороший обзор по АОП представлен в диссертации [АОР2]. И, наконец, связи между АОП и .NET отражены в статье [АОР3].

С точки зрения АОП в процессе разработки достаточно сложной системы программист решает две ортогональные задачи:

• Разработка компонентов

• Разработка сервисов, поддерживающих взаимодействие компонентов

Такие языки программирования как, например, C++, VB и т. п. ориентированы прежде всего на решение первой задачи. Код компонента представляется в виде класса, т. е. он хорошо локализован и, следовательно, его легко просматривать, изучать, модифицировать, повторно использовать. С другой стороны, при программировании процессов, в которые вовлечены различные объекты, мы получаем код, в котором элементы, связанные с поддержкой такого процесса, распределены по коду всей системы. Эти элементы встречаются в коде множества классов, их совокупность в целом не локализована в обозримом сегменте кода. В результате мы сталкиваемся с проблемой "запутанного" кода (code tangling).

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

Понятие аспект в рамках АОП в диссертации [АОР2] определено так: "Некоторая модель является аспектом другой модели, если она пересекает (crosscuts) ее структуру". Иными словами понятие аспекта относительно. Если, например, в качестве модели некоторой системы мы рассматриваем совокупность компонентов, представляющих такие сущности как вкладчик, счет, банк, то аспектами являются сервисы, обеспечивающие синхронизацию доступа к счету, распределенные транзакции, безопасность. То есть автоматически выполняемые сервисы, обеспечивающие слаженную, надежную, безопасную работу компонентов.

Итак, согласно парадигме АОП, для программирования компонентов и аспектов нужны различные, специфические языки программирования. После этапа кодирования компонентов и аспектов на соответствующих языках выполняется автоматическое построение оптимизированного для выполнения (но не для просмотра и модификации) кода (например, на С). Этот финальный процесс называется слиянием (weaving).

В рамках СОМ+ элементы идей АОП присутствуют в виде использования декларативного программирования для задания сервисов, услугами которых будут пользоваться компоненты. Сами компоненты разрабатываются на языках, ориентированных на разработку компонентов (C++, VB). При конфигурировании компонента в СОМ+ приложении задается некоторый набор атрибутов, определяющий тот набор сервисов, которыми будет пользоваться данный компонент.

В СОМ+ набор возможных сервисов и, соответственно, задающих их атрибутов, фиксирован. Программист не может разработать новый сервис, который можно было бы декларативно связать с некоторым компонентом, приписав последнему соответствующий атрибут. Ситуация изменилась в .NET. Хотя и осталась возможность использовать все сервисы из СОМ+, появилась новая возможность разработки новых сервисов, подключаемых к компонентам декларативно, посредством определения новых атрибутов. Весь этот механизм основан на таких понятиях как контекст и пользовательский атрибут.

Прежде чем мы перейдем к рассмотрению контекстов и атрибутов необходимо заметить, что в документации к .NET отсутствует информация о ряде важнейших классов и интерфейсов, которые нам предстоит использовать. Указывается, что эти классы и интерфейсы предназначены для использования самой системой (CLR), и что не предполагается их использование разработчиками приложений. Тем не менее получить информацию об этих классах и интерфейсах возможно. Имеются статьи (например, [АОРЗ]), код, в которых демонстрируется использование данных классов и интерфейсов.

Важнейшим новым источником информации является опубликованный Microsoft весной 2002 года код объемом около 1.9 млн строк и документация к нему — Shared Source Common Language Infrastructure (SSCLI). Этот код является одной из возможных реализаций спецификации языка C# и Common Language Infrastructure (CLI), принятых европейской организацией стандартизации ЕСМА в конце 2001 года .NET Framework является коммерческой реализацией этой же спецификации.

Несмотря на определенные различия между CLR из .NET и SSCLI, код из CLI является полезным источником информации при изучении .NET. В частности, изучение кода атрибута SynchronizationAttribute из SSCLI позволяет глубже понять его семантику, что необходимо для правильного и эффективного использования данного атрибута в .NET.

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


Описание примера

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

Как и в предыдущем разделе консольное клиентское приложение делает вклад на счет, поддерживаемый другим ранее запущенным консольным серверным приложением. Серверное приложение содержит три компонента, представляемые классами Account, Tax и News.

Компонент Account поддерживает счет, позволяя клиентам сделать вклад (метод Add) и узнать величину текущего счета (метод Total).

Компоненты Tax и News представляют соответственно налоговую службу и агентство новостей. Оба компонента получают уведомления о вкладах на счет (метод Notify) и выводят соответствующую информацию на консоль.

Компонент Tах активируется компонентом Account, который добровольно информирует Tax о каждом сделанном вкладе в процессе выполнения вызова метода Add. В свою очередь компонент Tах активирует компонент News, которому и передает полученную от Account информацию для придания ее гласности. Не полагаясь исключительно на компонент Tах, компонент Account получает от Tах ссылку на компонент News и самостоятельно напрямую посылает уведомление этому компоненту.

Для демонстрации пользовательских атрибутов, позволяющих задавать набор сервисов, доступных компонентам серверного приложения, мы будем использовать два атрибута:

SynchronizationAttribute

Этот атрибут синхронизации реализован в .NET и уже применялся в примере предыдущей главы. Как уже отмечалось выше, семантика этого атрибута весьма не проста, и для ее полного понимания полезно познакомиться с кодом этого атрибута, представленным в SSCLI (файл sscli clr scr bcl system runtime remoting synchronizeddispatch.es). Этот код содержит 1010 строк (вместе с комментариями). Мы не будем здесь его разбирать полностью, однако основные сведения, полученные при его изучении, будут в данной главе изложены и продемонстрированы в экспериментах.

MyCallTraceAttribute

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

Весь код примера содержится в трех файлах:

MyApp.cs

Этот файл содержит код консольного клиентского приложения.

MyServer.cs

Файл содержит код консольного серверного приложения и трех компонентов (Account, Tax, News).

MyCallTrace.cs

Этот файл содержит код атрибута трассировки вызовов.

Ниже приводится makefile, который можно использовать для компиляции и сборки клиентского и серверного приложений

all: МуАрр MyServer

clean:

@del МуАрр. exe MyServer.exe


МуАрр: МуАрр. ехе

МуАрр. exe: MyApp.cs MyServer.exe

csc /r: MyServer.exe MyApp.cs


MyServer: MyServer.exe

MyServer.exe: MyServer.cs MyCallTrace.cs

csc MyServer.cs MyCallTrace.cs

Из этого кода видно, что и клиентское, и серверное приложение являются приложениями типа .ехе и запускаются в различных доменах приложений. Для использования этого makefile достаточно запустить nmake, в результате чего будут получены файлы МуАрр. ехе и MyServer.ехе.


Серверное приложение

Ниже приводится код из файла MyServer.cs, который является некоторым расширением одноименного файла, рассмотренного в предыдущей главе.

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Threading;

using System.Runtime.Remoting.Contexts;

namespace SPbU.AOP_NET {

public interface IAccumulator {

void Add(int sum);

}

public interface IAudit {

int Total();

}


[Synchronization()]

[MyCallTrace("LogFile")]

public class Account: ContextBoundObject,

IAccumulator, IAudit {


private Tax _tax;

private int _sum = 0;


public Account() {

_tax = new Tax ();


Console.WriteLine("Account context = " +

Thread.CurrentContext.ContextID + "\n" +

"Account constructor thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);

}


public void Add(int sum) {

_sum += sum;

_tax.Notify("new Account operation: +" + sum); _

_tax.news.Notify("direct notification from Account");


Console.WriteLine("Account Add thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);

}


public int Total() {

return _sum;

}

}

[Synchronization()]

[MyCallTrace("LogFile")]

public class Tax: ContextBoundObject {


private News _news;


public Tax() {

_news = new News();


Console.WriteLine("Tax context = " +

Thread.CurrentContext.ContextID + "\n" +

"Tax constructor thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);

}


public void Notify(String msg) {

Console.WriteLine("Tax notification: " + msg);

Console.WriteLine("Tax Notify thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);


_news.Notify(msg);

}


public News news {


get {

return _news;

}

}

}


[Synchronization()]

[MyCallTrace("LogFile")]

public class News: ContextBoundObject {


public News(){


Console.WriteLine("News context = " +

Thread.CurrentContext.ContextID + "\n" +

"News constructor thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);

}


public void Notify(String msg) {

Console.WriteLine("News notification: " + msg);

Console.WriteLine("News Notify thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);

}

}


public class AccountApp {

public static void Main(){

HttpChannel myChannel = new HttpChannel(8080);

ChannelServices.RegisterChannel(myChannel);

RemotingConfiguration.RegisterWellKnownServiceType (

typeof(Account), "Account",

WellKnownObjectMode.Singleton);

Console.WriteLine("Server is listening");

Console.ReadLine();

Console.WriteLine("Bye");

}

}

}


Некоторые комментарии:

1. Определяемые в этом коде классы включаются в новое пространство имен — SPBU. AOP_NET. В этом же пространстве имен будет определен далее и атрибут трассировки вызовов MyCallTraceAttribute. При выборе имени пространства имен использовалась следующая рекомендация — префикс имени должен определять организацию, в которой работает разработчик. Попутно стоит заметить, что атрибут SynchronizationAttribute принадлежит пространству имен System.Runtime.Remoting.Contexts.

2. Классу Account наряду с атрибутом синхронизации (можно опустить часть "Attribute" при задании имени атрибута) приписан атрибут трассировки вызовов — [MyCallTrace ("LogFile")]. Здесь аргумент задает имя файла в рабочем каталоге, в конец которого будут записываться данные о вызовах методов этого класса. Однако трассировка вызовов будет обеспечиваться не всегда. Это касается только вызовов, сделанных извне контекста, в котором живет объект — экземпляр данного класса. Трассировка вызовов внутри данного контекста не производится. Понятие контекста и семантика данного атрибута будут рассмотрены далее.

3. Код класса Account претерпел некоторые изменения по сравнению с предыдущей главой:

♦ Появилось поле _tax — ссылка на экземпляр класса Tах. Новый экземпляр этого класса активируется в конструкторе класса Account с помощью оператора new. В результате при построении на стороне сервера экземпляра класса Account в этом же домене приложения формируется новый экземпляр класса Tах.

♦ В конструкторе класса Account выполняется вывод на консоль сервера идентификатора текущего контекста, т. е. контекста, в котором создается экземпляр класса Account. Кроме того на консоль выводится хеш потока, в котором выполняется конструктор, и логическое значение, равное true если этот поток выбран системой из пула потоков,

♦ При вызове метода Add происходит не только увеличение счета на величину нового вклада, но и вызывается метод Notify на объекте Tax, которому В качестве аргумента передается строка, сигнализирующая о поступлении нового вклада на счет.

Кроме того в этом же методе выполняется прямое уведомление компонента News. Для этого используется ссылка _tax.news на Экземпляр класса News, активированного к этому моменту экземпляром класса Tах. И здесь же на консоль выводится хеш потока, выполняющего метод Add, и информация о том, является ли этот поток потоком из пула потоков. Заметим, что хеш потока является уникальным в системе и может использоваться для идентификации потоков.

4. Прежде чем продолжить обсуждение кода сервера, необходимо остановиться на понятии контекста. Это важнейшее понятие данной главы. Именно механизм контекстов обеспечивает некоторый уровень реализации парадигмы аспектно-ориентированного программирования в рамках .NET. Смысл понятия контекста будет разъясняться последовательно в процессе разбора кода рассматриваемого здесь примера и обсуждения результатов экспериментов с этим кодом. Здесь обсудим связь между контекстом и доменом приложения.

5. Все объекты, живущие в некотором домене приложения, разбиваются на непересекающиеся группы. Грубо говоря, в одну группу попадают объекты, имеющие сходные запросы на использование сервисов. Все объекты одной группы живут в одном контексте, а объекты из разных групп живут в разных контекстах.

6. В каждом домене приложения имеется контекст по умолчанию, в который попадают все объекты, классы которых не являются производными от класса ContextBoundObject. Таким классам нельзя приписать какой-либо пользовательский атрибут. Точнее, экземпляры таких классов не могут воспользоваться связанными с этими атрибутами сервисами. Напротив, экземпляры классов, производных от класса ContextBoundObject, привязаны к конкретным контекстам (кроме контекста по умолчанию) и могут пользоваться связанными с такими контекстами сервисами.

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

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

9. Теперь продолжим обсуждение кода сервера.

10. Класс Tах (как и класс Account) привязан к контексту (он наследует классу ContextBoundObject). Это дает возможность экземплярам данного класса использовать сервисы синхронизации и трассировки вызовов (последнее верно только для вызовов, приходящих извне данного контекста). Именно для этого классу Tах приписаны атрибуты синхронизации и трассировки вызовов.

Дополнительные комментарии, касающиеся класса Tах:

♦ В конструкторе класса Tах активируется новый экземпляр класса News, ссылка на который запоминается в поле _news. Для доступа к этой ссылке предусмотрено публичное свойство news (только для чтения),

♦ При выполнении конструктора класса Tах на консоль сервера выводится информация об идентификаторе контекста, в котором будет жить новый объект, хеш текущего потока и его тип (поток из пула потоков или нет). Эти данные будут использованы при проведении экспериментов,

♦ Метод Notify класса Tах выводит на консоль полученное уведомление и в свою очередь отсылает полученное уведомление экземпляру класса News. Здесь же на консоль выводится хеш текущего потока и его тип.

11. Определение класса News во многом похоже на определение класса Tах. Достаточно только отметить, что этот класс является последним в цепочке рассылки уведомлений, что приводит к упрощению кода.

12. Класс AccountApp содержит функцию Main и определяет консольное серверное приложение. Этот класс не претерпел никаких изменений по сравнению с соответствующим классом, описанным в предыдущей главе.


Атрибут трассировки вызовов

Прежде чем привести код этого атрибута, вернемся к понятию контекста. Контекст формируется только тогда, когда это необходимо, т. е. тогда, когда появляется первый объект, который будет жить в этом контексте.

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

Связь сервиса с контекстом осуществляется путем задания соответствующего свойства контекста для данного контекста. Каждое свойство контекста имеет имя и содержит ссылку на некоторый объект, реализующий интерфейс IContextProperty.

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

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

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

ICcontributeObjectSink, IContributeServerContextSink, IContributeClientContextSink. Выбор одного из этих интерфейсов определяет ту цепочку перехватчиков, в конец которой будет добавлен новый перехватчик. На самом деле в контексте может существовать несколько цепочек перехватчиков:

• Одна цепочка перехватчиков, перехватывающих все вызовы поступающие ко всем объектам, живущим в данном контексте. Для встраивания перехватчика в эту цепочку объект-свойство контекста должен реализовать интерфейс IContributeServerContextSink.

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

• Одна цепочка для всех вызовов, которые объекты контекста делают за пределы данного контекста. Для встраивания перехватчика в эту цепочку объект-свойство должен реализовать интерфейс IContributeClientContextSink.

После этих вводных замечаний о механизме работы атрибута и контекста перейдем к коду атрибута MyCallTraceAttribute и к комментариям к этому коду.

using System;

using System.10;

using System.Threading;

using System.Runtime.Remoting.Messaging;

using System.Runtime.Remoting.Contexts;

using System.Runtime.Remoting.Activation;

using System.Runtime.CompilerServices;


namespace SPbU.AOP_NET{


[AttributeUsage(AttributeTargets.Class)]

public class MyCaiiTraceAttribute: ContextAttribute,

IContributeServerContextSink {


private const String PROPERTY_NAME = "MyCallTrace";

private String _logFileName = null;

public MyCallTraceAttribute(String logFileName):

base(PROPERTY_NAME) {


if (logFileName == null) {

throw new ArgumentNullException("logFileName");

}

_logFileName = logFileName;

}


public override bool IsContextOK(Context ctx,

IConstructionCallMessage msg) {


if (ctx == null)

throw new ArgumentNullException("ctx");

if (msg == null)

throw new ArgumentNullException("msg");


MyCallTraceAttribute property =

(MyCallTraceAttribute)ctx.GetProperty(PROPERTY_NAME)


if ((property!= null) &&

(property._logFileName == _logFileName))

return true;

else

return false;

}


public override void GetPropertiesForNewContext {

IConstructionCallMessage ctorMsg) {


ctorMsg.ContextProperties.Add((IContextProperty) this);

}


public virtual IMessageSink GetServerContextSink {

IMessageSink nextSink) {


MyCallTraceServerContextSink propertySink =

new MyCallTraceServerContextSink(this, nextSink);

return (IMessageSink)propertySink;

}

[Methodlmpl(MethodImplOptions.Synchronized)]

internal void LogMessage(String msg){


StreamWriter logFile = null;


while (logFile == null) {

logFile = File.AppendText(_logFileName);

}


logFile.WriteLine(msg);

logFile.Close();

}

}


internal class MyCallTraceServerContextSink: IMessageSink {


internal IMessageSink _nextSink;

internal MyCallTraceAttribute _property;

internal IMessage _replyMsg;

internal MyCallTraceServerContextSink {

MyCaiiTraceAttribute property, IMessageSink nextSink) {


_property = property;

_nextSink = nextSink;

_replyMsg = null;

{


public virtual IMessage SyncProcessMessage(IMessage reqMsg) {


if (reqMsg is IMethodMessage) {


IMethodMessage call = reqMsg as IMethodMessage;


lock(_property){

_property.LogMessage("===" + call.TypeName);

_property.LogMessage("\n" + call.MethodName +

" \n\t <<>> parameters: (");


for (int i = 0; i < call.ArgCount; i++) {

if (i > 0) _property.LogMessage(", ");

_property.LogMessage(call.GetArgName(i) +

"= " + call.GetArg(i));

}

_property.LogMes sage(")\n");

}

}

_replyMsg = _nextSink.SyncProcessMessage(reqMsg);


if (_replyMsg is IMethodReturnMessage) {


IMethodReturnMessage retMsg =

(IMethodReturnMessage) _replyMsg;


Exception e = retMsg.Exception;

if (e!= null) {

Console.WriteLine(e.Mes sage);

return _replyMsg;

}


lock(_property) {

_property.LogMessage("===" + retMsg.TypeName);

_property.LogMessage("\n" + retMsg.MethodName +

" \n\t << parameters: (");


for (int i = 0; i < retMsg.OutArgCount; i++) {

if (i > 0) _property.LogMessage(", ");

_property.LogMessage(retMsg.GetOutArgName(i) +

" = " + retMsg.GetOutArg(i));

}

_property.LogMes sage(")\n");

}

}

return _replyMsg;


public virtual IMessageCtrl AsyncProcessMessage(IMessage msg,

IMessageSink replySink) {

throw new InvalidOperationExcept();

}

public IMessageSink NextSink {

get {

return _nextSink;

}

}

}

}


Комментарии к коду:

1. Данный код содержит определения двух классов:

MyCallTraceAttribute

Этот публичный класс доступен всем приложениям, имеющим доступ к сборке MyServer.ехе

MyCallTraceServerContextSink

Этот класс является внутренним (internal) для сборки MyServer.ехе и не доступен за ее пределами.

2. Классу MyCallTraceAttribute приписан атрибут [AttributeUsage (AttributeTargets. Class)]. Данный атрибут используется при определении пользовательских атрибутов для задания элементов, которым может быть приписан данный атрибут. В данном случае атрибут MyCallTraceAttribute можно приписать только классу (но нельзя приписать, например, какому-то методу).

3. Комментарии к коду класса MyCallTraceAttribute:

♦ Класс MyCallTraceAttribute является производным классом от класса ContextAttribute и реализует интерфейс IContributeServerContextSink. В свою очередь класс ContextAttribute реализует интерфейсы IContextProperty и IContextAttribute.

Реализация интерфейсов IContextProperty и IContextAttribute обеспечивает выбор контекста для размещения активируемого объекта (в старом или в новом контексте), а в случае формирования нового контекста — назначение ему свойств, которые объект может вызывать в своем коде явно.

Реализация интерфейса IContributeServerContextSink позволяет включить в конец цепочки перехватчиков, которые перехватывают все входящие в контекст вызовы, новый перехватчик. Это позволяет декларативно связать некоторый класс с некоторым автоматическим сервисом, что и является реализацией идей аспектно-ориентированного программирования.

♦ Константа PROPERTY_NAME будет использована для задания имени свойству контекста. Каждое свойство контекста имеет имя и для любого заданного контекста и имени можно определить — содержит ли данный контекст свойство с данным именем. Эта возможность используется при выяснении пригодности заданного контекста как среды для жизни некоторого объекта с определенными требованиями к наличию автоматических сервисов,

♦ Поле _logFileName используется свойством контекста для хранения имени файла, в который надо записывать данные о перехваченных вызовах.

♦ Конструктор атрибута принимает в качестве аргумента имя файла, которое и сохраняется в поле _logFileName. Если имя не задано, генерируется соответствующее исключение. В начале работы конструктора вызывается конструктор базового класса ContextAttribute, которому в качестве аргумента передается имя данного свойства контекста (MyCallTrace).

♦ Публичный виртуальный метод IsContextOK объявлен в интерфейсе IContextAttribute. Именно этот метод ответственен за определение пригодности заданного контекста _ctx (первый аргумент) для жизни объекта, требования которого заданы в сообщении ms g (второй аргумент).

Сообщение msg должно быть ссылкой на объект, реализующий интерфейс IConstructionCallMessage. Это сообщение представляет запрос на создание некоторого объекта. Тут нужно напомнить, что в .NET удаленные объекты активируются либо сервером, либо клиентом. Сообщение типа IConstructionCallMessage посылается от клиента на сервер именно во втором случае. Для этого клиент либо вызывает new, и тогда все требования относительно активации объекта берутся из конфигурационного файла, либо вызывает Activator.CreateInstance и передает все необходимые данные в аргументах. Это сообщение приходит на сервер, где активатор Activator его его обрабатывает и возвращает клиенту сообщение типа IConstructionReturnMessage. Последнее содержит информацию (objRef), достаточную для построения прокси к активируемому объекту.

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

Базовая реализация метода IsContextOK в классе ContextAttribute (согласно коду из SSCLI) возвращает true, если новый объект не привязан к контексту (IsContextful == false) или у контекста ctx имеется свойство, имя которого совпадает с именем данного свойства. В остальных случаях возвращается false.

В классе MyCallTraceAttribute виртуальный метод IsContextOK переопределяется (overide). Прежде всего генерируется исключение, если не задан контекст ctx или сообщение msg. Далее делается попытка получить в контексте ctx ссылку на его свойство с именем, хранящемся в константе PROPERTY_NAME, типа MyCallTraceAttribute. Если такое свойство контекста находится, и его поле _iogFileName хранит то же имя файла, что и текущее свойство, то контекст ctx признается подходящим для активации в нем экземпляра класса с атрибутом [MyCallTrace (х)], где х — Строка, равная строке, хранящейся _logFileName. В противном случае система выполнения (CLR) создаст новый контекст для активации в нем этого объекта.

Заметим, что при наличии нескольких атрибутов, приписанных классу, пригодность старого контекста для активации в нем нового экземпляра этого класса считается установленной, если вызовы метода IsContextOK для каждого атрибута возвратили true.

♦ Виртуальный метод GetPropertiesForNewContext объявлен в интерфейсе IContextAttribute. В качестве аргумента этот метод принимает сообщение ctorMsg типа IConstructionCallMessage. Данный метод вызывается средой выполнения CLR если контекст, из которого был сделан запрос на активацию объекта, не удовлетворяет его требованиям (вызов IsContextOK вернул false). Здесь мы включаем в сообщение ctorMsg ссылку на объект, который будет играть роль нового свойства контекста — (IContextProperty) this. Благодаря этой ссылке все объекты, которые будут жить в новом контексте, смогут пользоваться данным свойством контекста, вызывая явно в своем коде его методы и свойства.

♦ Метод GetServerContextSink объявлен в интерфейсе IContributeServerContextSink и не имеет реализации в классе ContextAttribute. Этот метод должен вернуть ссылку на объект, реализующий интерфейс IMessageSink, т. е. на перехватчик, который будет подключен в конец существующей цепочки перехватчиков, через которую идут все вызовы в данный контекст. Единственным аргументом является ссылка nextSink на конец имеющейся в данный момент цепочки перехватчиков. В данной реализации этого метода формируется экземпляр класса MyCallTraceServerContextSink, который будет прокомментирован ниже.

Ссылка на этот экземпляр (новый перехватчик) и возвращается как результат вызова GetServerContextSink.

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

♦ Последний метод (LogMessage) класса MyCallTraceAttribute является внутренним для сборки и будет вызываться из класса MyCallTraceServerContextSink. Данный метод обеспечивает запись переданной в качестве аргумента строки в конец файла с именем, хранящемся в поле _logFileName свойства типа MyCallTraceAttribute с именем MyCallTrace текущего контекста. Объекты, живущие в данном контексте, могут вызывать этот метод явно в своем коде, получив ссылку на это свойство контекста по его имени. Однако в данном примере будет продемонстрировано использование свойства контекста в стиле аспектно-ориентированного программирования. Поэтому метод LogMessage будет в нашем случае вызываться перехватчиком, а не самим объектом, получившим вызов.

Реализации метода LogMessage весьма не эффективна. При каждом вызове этого метода файл открывается для записи в конец, производится запись и файл закрывается. Для обеспечения потокобезопасности весь метод включается в критическую секцию, препятствующую параллельному выполнению этого метода несколькими потоками. Для этого методу приписан атрибут [MethodImpl(MethodImplOptions. Synchronized)].

Цикл

while (logFile == null) {

logFile = File. AppendText(_logFileName);

}

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

4. Комментарий К коду класса MyCallTraceContextSink

♦ Данный внутренний класс реализует интерфейс IMessageSink и, следовательно, реализует некоторый перехватчик,

♦ Поле _nextsink будет хранить ссылку на следующий перехватчик в той цепи перехватчиков, в конец которой будет добавлен данный перехватчик,

♦ Поле _property предназначено для хранения ссылки на свойство контекста типа MyCallTraceAttribute. Перехватчик порождается благодаря методу GetPropertiesForNewContext, реализованного этим свойством контекста. Однако сейчас нам это свойство важно тем, что именно его метод LogMessage будет вызываться перехватчиком при перехвате нового вызова,

♦ Поле _replyMsg типа IMessage будет хранить ответ вызванного метода, полученный данным перехватчиком от перехватчика nextsink. о Конструктор данного класса принимает два аргумента:

property

Это ссылка на свойство контекста, метод которого LogMessage будет вызываться данным перехватчиком.

nextSink

Это ссылка на следующий перехватчик в цепи перехватчиков.

Данные значения присваиваются соответственно полям _property и _nextsink.

♦ Виртуальный метод SyncProcessMessage объявлен в интерфейсе IMessageSink. Этот метод реализует обработку синхронных вызовов, т. е. вызовов, после инициализации которых клиенты блокируются до получения ответа.

Единственный аргумент данного метода — сообщение reqMsg (request message), содержащее вызов, полученный от клиента и уже, возможно, обработанный перехватчиками, находящимися в цепочке ближе к клиенту.

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

Обработка вызова состоит в том, что перехватчик выводит с помощью LogMessage информацию о типе, о методе и о входных ([IN]) аргументах. Пример (слегка отредактированный для удобства просмотра) выводимой информации в случае вызова метода Notify экземпляра класса News приведен ниже:

===SPbU.AOP_NET.News, MyServer, Version = 0.0.0.0,

Culture = neutral, PublicKeyToken = null

Notify

<<>> parameters: {

msg = new Account operation: +5)

}

В процессе обработки перехваченного вызова сообщение reqMsg приводится к типу IMethodMessage и блокируется свойство контекста _property. Эта блокировка позволит при записи информации в файл посредством вызова LogMessage группировать все строки, относящиеся к одному вызову, вместе, т. е. не допускать в файле с именем _logFileName чередования строк, относящихся к различным вызовам.

После завершения обработки перехваченного вызова данный перехватчик передает неизменное сообщение reqMsg следующему в цепочке перехватчику (перехватчику, находящемуся ближе к серверу). Для этого вызывается метод SyncProcessMessage перехватчика _nextSink.

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

При наличии неперехваченного исключения на консоль сервера выводится соответствующее сообщение и метод возвращает полученный ответ без какой-либо обработки. В противном случае выполняется процесс, аналогичный описанному ранее (обработка вызова). Блокируется свойство контекста _property и посредством вызова метода _property.LogMessage выводится информация о типе, о вызванном методе, список выходных [out] аргументов. Ниже приведен пример для ответа на вызов метода Notify класса News:

===SPbU.AOP_NET.News, MyServer, Version = 0.0.0.0,

Culture = neutral, PublicKeyToken = null

Notify

<<>> parameters: {

}

И, наконец, возвращается без изменений сообщение _repiyMsg.

Виртуальный метод AsyncProcessMessage объявлен в интерфейсе IMessageSink и обязательно должен быть реализован в данном классе. Этот метод предназначен для обработки асинхронных вызовов, т. е. вызовов, после инициализации которых клиент не блокируется, а продолжает свою работу.

Данный метод имеет два аргумента:

msg — сообщение типа IMessage, содержащее вызов.

repiysink — ссылка на перехватчик, на который надо отсылать результат. Именно этот перехватчик ответственен за уведомление клиента о полученном результате.

Атрибут MyCallTraceAttribute не предусматривает обработку асинхронных вызовов, в связи с чем при вызове данного метода генерируется исключение, уведомляющее об ошибочной операции.

♦ Свойство (только для чтения) NextSink также объявлено в интерфейсе IMessageSink и должно быть реализовано в данном классе. Здесь просто возвращается значение _nextsink.


Клиентское приложение

Клиентское приложение для рассматриваемого примера почти не отличается от клиентского приложения MуАрр. cs из предыдущей главы. Добавлен вывод на консоль клиента идентификатора контекста клиента, хеш потока и его тип.

using System;

using SPbU.AOP_NET;

using System.Threading;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Net;


public class MyApp {

public static void Main() {


HttpChannel с = new HttpChannel();

ChannelServices.RegisterChannel(c);


try {

Console.WriteLine("Client context = " +

Thread.CurrentContext.ContextID + "\n" +

"Client thread = " +

Thread.CurrentThread.GetHashCode () +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);


Account a = (Account)Activator.GetObject {

typeof (Account),

"http://localhost:8080/Account",

WellKnownObjectMode.Singleton);


a. Add(5);

Console.WriteLine("Current account: {0}",

a. Total());


}

catch(WebException e) {

Console.WriteLine(e.Message);

}

catch(Exception e) {

Console.WriteLine(e.Message);

}

finally!

Console.WriteLine("Bye");

}

}

}


Атрибут синхронизации

Цель данного раздела состоять в частичном объяснении (в той части, которая потребуется для рассматриваемого примера) семантики атрибута SynchronizationAttribute, реализованного в .NET.

Изложение будет основано на коде, опубликованном в рамках SSCLI (файл sscli clr scr bcl system runtime remoting synchronizeddispatch.cs). Целиком этот код здесь приводиться не будет, но читателям рекомендуется самостоятельно разобраться в нем для лучшего понимания этого атрибута.

В связи с тем, что между SSCLI и .NET Framework отсутствует совместимость на уровне реализации (хотя и имеется совместимость на уровне спецификации CLI от ЕСМА), нельзя проводить какие-либо Эксперименты, заменив атрибут SynchronizationAttribute из .NET кодом из файла synchronizeddispatch.cs. Однако можно полагать, что семантика данного атрибута в .NET и в SSCLI одна и та же.

Как и в .NET, в SSCLI атрибут SynchronizationAttribute определен в пространстве имен Ssystem.Runtime.Remoting.contexts. Реализация данного атрибута представлена следующими классами:

• SynchronizationAttribute

• Workltem

• SynchronizedServerContextSink

• SynchronizedClientContextSink

• AsyncReplySink

Можно надеяться, что после знакомства С атрибутом MyCallTraceAttribute читатель догадывается о семантике почти всех из упомянутых классов. Экземпляры класса WorkItem представляют отдельные поступившие вызовы, сохраняемые в очереди работ. Синхронизация обеспечивается последовательным извлечением и выполнением работ из этой очереди.

Рассмотрим заголовок класса SynchronizationAttribute:

SynchronizationAttribute: ContextAttribute, IContгibuteServerContexts ink, IContributeClientContextSink

Уже отсюда видно, что данный атрибут должен обеспечивать двух перехватчиков. Один будет встроен в цепочку перехватчиков, перехватывающих все поступающие в контекст вызовы, а другой в цепочку, перехватывающих все исходящие за пределы контекста вызовы.

Прежде всего необходимо остановиться на трех понятиях:

Домен синхронизации

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

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

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

Если старый и новый контексты синхронизации независимы, то возможно параллельное обращение к двум объектам из этих двух контекстов. Однако возможны ситуации, когда это недопустимо и необходимо обеспечить синхронный доступ к объектам, принадлежащим разным контекстам.

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

Вложенные вызовы

Получив внешний вызов, пересекающий границу любого контекста домена синхронизации, этот домен синхронизации блокируется. Однако возможна ситуация, когда для некоторых новых внешних вызовов блокировка домена синхронизации должна быть снята. Это так называемые вложенные вызовы. При выполнении некоторого внешнего вызова X, поступившего в некоторый контекст домена синхронизации, может быть сделан вызов X1 за пределы этого контекста. При выполнении вызова X1 где-то в другом домене может быть сделан вызов Х2 опять в некоторый контекст данного домена синхронизации. Нельзя блокировать выполнение вызова Х2 до завершения выполнения вызова х, т. к. последний ожидает выполнения вызова X1, который в свою очередь ожидает выполнения вызова Х2. Вся эта цепочка вложенных вызовов образует один так называемый логический вызов, имеющий уникальный идентификатор. По этому идентификатору домен синхронизации распознает вновь пришедший вызов как вложенный вызов и не блокирует его выполнение.

Реентерабельность

Предположим, что домен синхронизации блокирован на время выполнения вызова X. Пусть в процессе выполнения X был сделан вызов X1 за пределы домена синхронизации. Обычно ни один поток не может выполнить какой-либо работы в данном домене синхронизации пока не будет получен ответ или не придет вложенный вызов для вызова xi. Однако в ряде случаев можно разрешить принимать во время ожидания другие внешние вызовы, никак не связанные с вызовом X. В этом случае домен синхронизации называется реентерабельным (reentrant). Естественно, только разработчик некоторого класса может указать, что экземпляры данного класса могут жить в реентерабельном домене синхронизации. Разработка таких классов способствует повышению эффективности проектируемой системы.

Теперь обратимся к конструктору класса SynchronizationAttribute. Для этого класса имеется четыре конструктора. Рассмотрим прежде четвертый конструктор, т. к. первые три делегируют вызов четвертому.

public SynchronizationAttribute(int flag, bool reEntrant)

: base(PROPERTY_NAME) {

_bReEntrant = reEntrant;


switch (flag) {

case NOT_SUPPORTED:

case SUPPORTED:

case REQUIRED:

case REQUIRES_NEW:

_flavor = flag;

break;

default:

throw new ArgumentException(

Environment.GetResourceString(

"Argument_InvalidFlag"),

"flag");

}

}


Первый параметр flag принимает одно из четырех возможных значений:

NOT_SUPPORTED (== 0x00000001)

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

SUPPORTED (== 0x00000002)

Данный флаг означает, что разработчик не заботится о том, в каком именно контексте (синхронизации или нет) будет активирован экземпляр его класса.

REQUIRED (== 0x00000004)

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

REQUIRES_NEW (== 0x00000008)

Данный флаг означает необходимость создания нового контекста синхронизации для нового экземпляра класса.

Если аргумент flag содержит какое-либо иное значение, генерируется соответствующее исключение.

Второй логический аргумент равен true, если экземпляр класса, которому приписан данный атрибут, может жить в реентерабельном контексте. В противном случае второй аргумент равен false.

В конструкторе класса SynchronizationAttribute вызывается конструктор базового класса ContextAttribute с аргументом property_name (строковой константой равной "Synchronization"). Именно так называется свойство синхронизации в SSCLI.

Остальные конструкторы определяются через рассмотренный выше:

public SynchronizationAttribute()

: this(REQUIRED, false) {}


public SynchronizationAttribute(bool reEntrant)

: this(REQUIRED, reEntrant) {}


public SynchronizationAttribute(int flag)

: this(flag, false) {}

Теперь рассмотрим важнейший для правильного понимания и использования атрибута синхронизации вопрос — реализацию методов IsContextOK и GetPropertiesForNewContext.

Начнем с виртуального метода IsContextOK, объявленного в интерфейсе IContextAttribute и реализованного в классе ContextAttribute. В рассматриваемом коде из SSCLI этот метод переопределяется следующим образом:

public override bool IsContextOK(Context ctx,

IConstructionCallMessage msg) {


if (ctx == null)

throw new ArgumentNullException("ctx");

if (msg == null)

throw new ArgumentNullException("msg");


bool isOK = true;

if (_flavor == REQUIRES_NEW) {

isOK = false;

}

else {

SynchronizationAttribute syncProp =

(SynchronizationAttribute) ctx.GetProperty(PROPERTY_NAME);

if (((_flavor == NOT_SUPPORTED)&&(syncProp!= null))

|| ((_flavor == REQUIRED)&&(syncProp == null))

) {

sOK = false;

}


if (_flavor == REQUIRED) {

_cliCtxAttr = syncProp;

}

}

return isOK;

}


Таким образом контекст ctx признается непригодным для жизни экземпляра класса описанного с атрибутом синхронизации в следующих случаях:

1. В конструкторе атрибута был задан флаг REQUIRES_NEW

2. В контексте ctx имеется свойство контекста с именем "Synchronization" типа SynchronizationAttribute, а в конструкторе атрибута был явно задан флаг NOT_SUPPORTED

3. В контексте ctx нет свойства контекста с именем "Synchronization" типа SynchronizationAttribute, но при вызове конструктора атрибута был выбран (явно или неявно) флаг required.

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

Эти правила определяют условия размещения нового объекта в старом контексте или необходимость формирования нового контекста. Однако остается вопрос о домене синхронизации. Когда новый контекст, если он был построен, будет включен в тот домен синхронизации, в который входит контекст ctx?

Обратим внимание на строки

if (_flavor == REQUIRED) {

_cliCtxAttr = syncProp;

}

Переменная syncProp равна null или ссылке на свойство контекста ctx с именем "Synchronization" типа SynchronizationAttribute. Таким образом, при заданном флаге REQUIRED в поле _cliCtxAttr типа SynchronizationAttribute в текущем экземпляре атрибута синхронизации сохраняется ссылка на одноименное свойство контекста ctx. Это подготовка к включению нового контекста (если он понадобится) в домен синхронизации, в который уже входит контекст ctx. Подробнее этот вопрос будет изложен при комментировании кода метода GetPropertiesForNewContext.

Теперь обратимся к методу GetPropertiesForNewContext:

public override void GetPropertiesForNewContext {

IConstructionCallMessage ctorMsg) {


if ((_flavor==NOT_SUPPORTED) || (_flavor==SUPPORTED) ||

(null == ctorMsg)) {

return;

}


if (_cliCtxAttr!= null) {

ctorMsg.ContextProperties.Add(

(IContextProperty)_cliCtxAttr);

_cliCtxAttr = null;

}

else {

ctorMsg.ContextProperties.Add((IContextProperty)this);

}

}

Метод GetPropertiesForNewContext вызывается системой в том случае, когда старый контекст не пригоден для жизни нового объекта.

Единственный аргумент ctorMsg типа IConstructionCallMessage должен быть сообщением, передаваемым со стороны клиента на сторону сервера и содержащим необходимую информацию об активируемом объекте. Роль рассматриваемого метода состоит в добавлении в это сообщение дополнительной информации. Именно, добавляется ссылка на объект типа IContextProperty, который будет играть роль свойства синхронизации нового контекста (свойство типа SynchronizationAttribute с именем "Synchronization").

Из приведенного кода видно, что исходное сообщение ctorMsg никак не меняется, если при задании атрибута был выбран флаг not_supported или supported, иными словами, если активируемый объект не должен жить в контексте синхронизации или разработчику все равно.

В противном случае возможны два варианта:

_cliCtxAttr == null

Этот случай возникает, когда либо старый контекст не поддерживает сервис синхронизации, либо атрибут синхронизации был задан с флагом REQUIRES_NEW. В этом случае в качестве ссылки на свойство синхронизации в сообщение ctorMsg включается ссылка на новый экземпляр атрибута синхронизации, который активируется системой еще до активации нового объекта. Таким образом получается новый контекст синхронизации, никак не связанный с каким-либо из ранее созданных контекстов синхронизации. Этот новый контекст синхронизации образует и новый домен синхронизации.

• _cliCtxAttr!= null

В этом случае cliCtxAttr является ссылкой на свойство синхронизации старого контекста и именно эта ссылка включается в сообщение ctorMsg как ссылка на свойство синхронизации нового контекста. Таким образом оба контекста синхронизации (старый и новый) имеют одно и то же свойство синхронизации. В этом случае говорят, что оба контекста принадлежат одному домену синхронизации.

Теперь уместно отметить одну особенность домена синхронизации, связанную с реентерабельностью. Предположим, что активируется экземпляр o1 некоторого класса, описанного с атрибутом

[Synchronization(REQUIRIES\_NEW, true)]}.

В этом случае формируется новый контекст синхронизации, который начинает собой и новый домен синхронизации. До активации объекта o1 активируется новый экземпляр атрибута синхронизации. В процессе выполнения его конструктора в поле _bReEntrant атрибута синхронизации сохраняется значение true. Таким образом, созданный контекст синхронизации является реентерабельным контекстом.

Предположим теперь, что при выполнении некоторого метода объекта o1 активируется экземпляр о2 некоторого класса, описанного с атрибутом [synchronization ()]. Это означает, что объект о2 должен жить в контексте синхронизации (флаг required), но не допускается реентерабельность. В зависимости от других атрибутов, приписанных классам, экземплярами которых являются объекты o1 и о2, эти объекты будут жить в одном контексте, либо в различных контекстах, но в одном домене синхронизации. Это определяется тем, что реентерабельность никак не учитывается при определении границ домена синхронизации. В связи с тем, что все контексты одного домена синхронизации имеют одно на всех свойство контекста с именем "Synchronization", а это свойство в рассматриваемом примере разрешает реентерабельность, объект о2 против желания его разработчика оказывается в реентерабельном контексте, что может привести к ошибке.

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

Из этого анализа следует, что в методы IsContextOK и GetPropertiesForNewContext нужно внести изменения, касающиеся реентерабельности. В результате в один домен синхронизации должны помещаться только реентерабельные, либо только нереентерабельные объекты.

Теперь остановимся на том сервисе, который обеспечивает свойство контекста синхронизации (свойство синхронизации). Изложение носит описательный характер, но оно основано на изучении кода атрибута синхронизации.

При поступлении вызова в домен синхронизации (в форме сообщения типа IMessage) это сообщение перехватывается перехватчиком входящих вызовов, определенном в свойстве синхронизации данного домена синхронизации.

Вызов может быть как синхронным, так и асинхронным. В обоих случаях перехватчик входящих вызовов формирует соответствующую данному вызову работу (экземпляр класса WorkItem), в которой сохраняется:

• Само сообщение

• Идентификатор контекста (данного домена синхронизации), в который пришел вызов

• Контекст логического вызова

• Ссылка на перехватчик входящих вызовов, которому нужно передать сообщение для дальнейшей обработки

• Перехватчик результатов, на который нужно послать результат (для асинхронного вызова)

Дальнейшая судьба работы определяется ее типом и наличием в домене синхронизации других работ, ожидающих выполнения:

• Если домен синхронизации нереентерабельный и работа представляет собой вложенный вызов (для синхронного вызова, который сейчас выполняется в данном домене), то данная работа сразу же выполняется без какого-либо ожидания. Заведомо известно, что в данный момент в данном домене синхронизации не выполняется ни один другой поток.

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

• Асинхронный вызов всегда записывается в очередь (даже если она пуста и домен не блокирован).

Работы извлекаются из очереди, если в домене нет исполняемой работы (например, предыдущая работа завершена).

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

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

Важный практический вывод. Различие между синхронными и асинхронными вызовами состоит в том, что в случае синхронного вызова клиент блокируется в ожидании ответа, а в случае асинхронного не ожидает ответа (его уведомление о результате вызова возлагается на специальный перехватчик). Таким образом разработчик может надеяться на то, что синхронные вызовы могут обрабатываться в домене синхронизации в приоритетном порядке по отношению к асинхронным. Однако в случае реализации атрибута синхронизации в рамках SSCLI это не так — попавшие в очередь (единую для всего домена синхронизации) работы извлекаются из нее в порядке очереди, и тип работы (синхронный или асинхронный вызов) не влияет на этот порядок.


Эксперименты

Эксперименты с вышеописанным кодом должны продемонстрировать работу сервисов синхронизации и трассировки вызовов.


Все компоненты размещаются в одном контексте

Первый эксперимент не требует какой-либо модификации кода клиента и сервера. Каждому компоненту (Account, Tax, News) приписаны два атрибута:

[Synchronization ()]

[MyCallTrace("LogFile")]

Таким образом, все три компонента требует одного и того же набора сервисов и их экземпляры размещаются в одном контексте.

Запустив серверное приложение и параллельно два клиентские приложения, мы увидим на консоли сервера информацию о том, что все три компонента выполняются в контексте 1.

Ниже приведены несколько первых строк с консоли сервера

Server is listening

News context = 1 News constructor thread = 3 IsPoolThread = True

Tax context = 1 Tax constructor thread = 3 IsPoolThread = True

Account context = 1 Account constructor thread = 3 IsPoolThread = True

Tax notification: new Account operation: +5

Tax Notify thread = 3 IsPoolThread = True

News notification: new Account operation: +5

News Notify thread = 3 IsPoolThread = True

News notification: direct notification from Account

News Notify thread = 3 IsPoolThread = True

Account Add thread = 3 IsPoolThread = True

Tax notification: new Account operation: +5

Tax Notify thread = 65 IsPoolThread = True

News notification: new Account operation: +5

News Notify thread = 65 IsPoolThread = True

News notification: direct notification from Account

News Notify thread = 65 IsPoolThread = True

Account Add thread = 65 IsPoolThread = True

Tax notification: new Account operation: +5

Tax Notify thread = 3 IsPoolThread = True

News notification: new Account operation: +5

News Notify thread = 3 IsPoolThread = True

News notification: direct notification from Account

News Notify thread = 3 IsPoolThread = True

Account Add thread = 3 IsPoolThread = True

…….


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

Модификация кода сервера (MyServer.cs) связана с добавлением интерфейса IAccumuiatorNew:

……

namespace SPbU.AOP_NET {


public interface IAccumuiatorNew{

void Add(int sum, int clientProcessId);

}

……

public class Account: ContextBoundObject, IAccumulator,

IAudit, IAccumuiatorNew!

……

public void Add(int sum, int clientProcessId){

_sum += sum;

_tax.Notify("new Account operation: +" + sum);

_tax.news.Notify("direct notification from Account");


Console.WriteLine("Account Add thread = " +

Thread.CurrentThread.GetHasheode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread +

" clientProcessId ="+ clientProcessId);

}

……

Необходимая модификация клиента (MуАрр. cs) представлена ниже

…….

using System.Diagnostics;

public class MyApp {

public static void Main() {


HttpChannel с = new HttpChannel();

ChannelServices.RegisterChannel(c);


Process p = Process.GetCurrentProcess();


try {

……

for (int i=0; i<100; i++) {

a. Add(5, p.Id);

}

…….

}

…….


Вот фрагмент вывода на консоль сервера из которого видно, что клиент никак не связан с рабочим потоком, выполняющим его вызов на сервере.

Account Add thread = 65 IsPoolThread = True clientProcessId =192

Tax notification: new Account operation: +5

Tax Notify thread = 65 IsPoolThread = True

News notification: new Account operation: +5

News Notify thread = 65 IsPoolThread = True

News notification: direct notification from Account

News Notify thread = 65 IsPoolThread = True

Account Add thread = 65 IsPoolThread = True clientProcessId =192

Tax notification: new Account operation: +5

Tax Notify thread = 3 IsPoolThread = True

News notification: new Account operation: +5

News Notify thread = 3 IsPoolThread = True

News notification: direct notification from Account

News Notify thread = 3 IsPoolThread = True

Account Add thread = 3 IsPoolThread = True clientProcessId =165

Tax notification: new Account operation: +5

Tax Notify thread = 3 IsPoolThread = True

News notification: new Account operation: +5

News Notify thread = 3 IsPoolThread = True

News notification: direct notification from Account

News Notify thread = 3 IsPoolThread = True

…….

Еще стоит обратить внимание на то, что рабочий поток, выполняющий вызов метода Add компонента Account, выполняет и вызовы метода Notify для компонентов Tах и News, инициированные из компонента Account. Причина в том, что в рамках данного эксперимента все три компонента находятся в одном контексте синхронизации, и поток, вошедший в этот контекст, не выходит из него, пока не будут сделаны все вышеупомянутые вызовы. Только после этого другой поток может войти в данный контекст и начать выполнение метода Add.

Просмотрев файл LogFile, в который выполняется запись данных о перехваченных вызовах, можно заметить, что трассируются только вызовы к компоненту Account. Это объясняется тем, что именно компонент Account получает вызовы извне контекста (от клиентов), и эти вызовы перехватываются перехватчиком входящих вызовов атрибута трассировки. Вызовы, которые делаются к компонентам Tах и News, идут от компонентов Account и Tах и не пересекают границу контекста. Именно поэтому они и не перехватываются. Первые строки файла LogFile представлены ниже:

===SPbU.AOP_NET.Account, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

ctor

<<>> parameters: (

}

===SPbU.AOP_NET.Account, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

ctor

<<>> parameters: {

}

===System.MarshalByRefObject, mscorlib, Version=1.0.3300.0,

Culture=neutral, PublicKeyToken=b77a5c561934e089

InitializeLifetimeservice

<<>> parameters: (

}

===System.MarshalByRefObject, mscorlib, Version=1.0.3300.0,

Culture=neutral, PublicKeyToken=b77a5c561934e089

InitializeLifetimeservice

<<>> parameters: (

}

===clr: SPbU.AOP_NET.Account, MyServer

Add

<<>> parameters: {

sum= 5

'

clientProcessId= 192)

===clr: SPbU.AOP_NET.Account, MyServer

Add

<<>> parameters: {

sum= 5

'

clientProcessId= 165)

===clr: SPbU.AOP_NET.Account, MyServer

Add

<<>> parameters: {

}

===clr: SPbU.AOP_NET.Account, MyServer

Add

<<>> parameters: {

}

===clr: SPbU.AOP_NET.Account, MyServer

Add

<<>> parameters: {

sum= 5

'

clientProcessId= 192)

===clr: SPbU.AOP_NET.Account, MyServer

Add

<<>> parameters: {

}


Компоненты размещаются в двух контекстах, но в одном домене синхронизации

Закомментируем атрибут Synchronization (), приписанный классу Account. Теперь, просматривая вывод на консоль сервера, можно заметить, что компонент Account размещается в контексте 1, а компоненты Tax и News в контексте 2:

Server is listening

News context = 2 News constructor thread = 9 IsPoolThread = True

Tax context = 2 Tax constructor thread = 9 IsPoolThread = True

Account context = 1 Account constructor thread = 9 IsPoolThread = True

…….

Просматривая файл LogFile видим, что теперь перехватываются вызовы к компонентам Tax и News, поступающие от компонента Account:

===SPbU.AOP_NET.Tax, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

ctor

<<> parameters: {

}

===SPbU.AOP_NET.Tax, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

ctor

<<>> parameters: {

}

===SPbU.AOP_NET.Tax, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

Notify

<<>> parameters: {

msg= new Account operation: +5)

===SPbU.AOP_NET.Tax, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

Notify

}

===SPbU.AOP_NET.Tax, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

get_news

<<>> parameters: {

}

===SPbU.AOP_NET.Tax, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

get_news

<<>> parameters: {

}

===SPbU.AOP_NET.News, MyServer, Version=0.0.).0,

Culture=neutral, PublicKeyToken=null

Notify

<<>> parameters: {

msg= direct notification from Account)

===SPbU.AOP_NET.News, MyServer, Version=0.0.0.0,

Culture=neutral, PublicKeyToken=null

Notify

<<>> parameters: {

}


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

Внесем в код сервера следующие дополнения:

namespace SPbU.AOP_NET {

……

public class Account: ContextBoundObject, IAccumulator,

IAudit, IAccumulatorNew{

……

public Account() {

……

SynchronizationAttribute syncProperty =

(SynchronizationAttribute)

Thread.CurrentContext.GetProperty {

"Synchronization");

Console.WriteLine {

"Account syncProperty == Tax syncProperty "+

Object.ReferenceEquals(syncProperty, _tax.syncProperty));

}

……

}

……

public class Tax: ContextBoundObject {

……

private SynchronizationAttribute _syncProperty;


public Tax() {

…….

_syncProperty =

(SynchronizationAttribute)

Thread.CurrentContext.GetProperty {

"Synchronization");

……

}

…….

internal SynchronizationAttribute syncProperty {

get { return _syncProperty;}

}

……

}

…….


Просматривая консоль сервера убеждаемся, что ссылки на свойство синхронизации в обоих контекстах (в контексте, в котором живет компонент Account и в контексте, в котором живут компоненты Tax и News) указывают на один объект — свойство синхронизации домена синхронизации:

Server is listening

News context = 2 News constructor thread = 9 IsPoolThread = True

Tax context = 2 Tax constructor thread = 9 IsPoolThread = True

Account context = 1 Account constructor thread = 9 IsPoolThread = True

Account syncProperty == Tax syncProperty True

…….


Компоненты размещаются в трех контекстах и в двух доменах синхронизации

Заменим атрибут синхронизации, приписанный компоненту News следующей его версией:

[Synchronization(0x00000008)]

Задание данного атрибута означает, что компонент News будет располагаться в новом контексте (3), причем этот контекст образует и новый домен синхронизации. Компоненты Account и Tax должны располагаться в различных контекстах (1 и 2), но эти контексты должны входить в один домен синхронизации.

Для проверки этих утверждений внесем еще несколько дополнений в код сервера:

…….

namespace SPbU.AOP_NET {

……

public class Tax: ContextBoundObject {

…….

public Tax() {

…….

Console.WriteLine {

"Tax syncProperty == News syncProperty "+

Object.ReferenceEquals(_syncProperty,

_news.syncProperty));

……

}

…….

}

…….

public class News: ContextBoundObject {


private SynchronizationAttribute _syncProperty;


public News() {


_syncProperty =

(SynchronizationAttribute)

Thread.CurrentContext.GetProperty {

"Synchronization");

……

}

……

internal SynchronizationAttribute syncProperty {

get { return _syncProperty;}

}

…….

}


Просматривая консоль сервера убеждаемся в том, что компонент News размещается в новом домене синхронизации:

Server is listening

News context = 3 News constructor thread = 9 IsPoolThread = True

Tax syncProperty == News syncProperty False

Tax context = 2 Tax constructor thread = 9 IsPoolThread = True

Account context = 1 Account constructor thread = 9 IsPoolThread = True

Account syncProperty == Tax syncProperty True

…….

И последнее замечание касается файла LogFile. Просматривая его, можно заметить, что теперь перехватываются все вызовы, идущие к компонентам Tах и News, идущие не только от компонента Account, но и от компонента Tах к компоненту News. Это объясняется тем, что все компоненты живут в различных контекстах и все вызовы пересекают границу контекста и, следовательно, перехватываются.


Еще раз про атрибут синхронизации

Эта глава продолжает изучение кода атрибута синхронизации из Rotor, рассмотрение которого было начато в предыдущей главе. Там мы рассмотрели основные механизмы, связанные с определением контекста и домена синхронизации, в которых будет размещен новый объект — экземпляр класса, которому приписан атрибут SynchronizationAttribute. Это конструкторы (четыре варианта) и методы IsContextOK и GetPropertiesForNewContext класса SynchronizationAttribute. Теперь мы сосредоточимся на самом алгоритме синхронизации и попутно рассмотрим несколько важных понятий, связанных с программированием в CLR.


Инициализация свойства синхронизации в домене синхронизации

Начнем с метода InitIfNecessary класса SynchronizationAttribute:

internal virtual void InitlfNecessary() {

lock(this) {

if (_asyncWorkEvent == null) {

_asyncWorkEvent = new AutoResetEvent(false);


_workltemQueue = new Queue();

_asyncLcidList = new ArrayList();


WaitOrTimerCallback callBackDelegate =

new WaitOrTimerCallback(this.DispatcherCallBack);


ThreadPool.RegisterWaitForSingleObject {

_asyncWorkEvent,

callBackDelegate,

null,

_timeOut,

false);

}

}

}


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

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


Критические секции

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

Рассмотрим несколько примеров.

Ранее мы уже рассматривали консольное серверное приложение MyServer, поддерживающее некоторый банковский счет. Клиентские приложения могли параллельно делать вклады на этот счет. Синхронизация обеспечивалась за счет использования атрибута синхронизации SynchronizationAttribute, который приписывался классу Account, и наследования этого класса от класса ContextBoundObject.

Теперь мы обеспечим синхронизацию за счет использования критических секций.

Простейший способ связан с приписыванием методу Add атрибута [MethodImpl (MethodImplOptions.Synchronized)]. Данный атрибут запретит вод в тело метода Add какого-либо потока, если этот метод уже выполняется в другом потоке. В данном случае мы полностью полагаемся на компилятор, который должен обеспечить требуемую функциональность.

…….

namespace MyServer {

……

public class Account: MarshalByRefObject,

IAccumulator, IAudit {

…….

[MethodImpl(MethodImplOptions.Synchronized)]

public void Add(int sum) {

_sum += sum;

}

…….

}

}


Заметим, ЧТО теперь достаточно наследования класса Account от класса MarshalByRefObject, так как привязка экземпляра этого класса к контексту более не нужна.

Использование атрибута [MethodImpl (MethodImplOptions.Synchronized)] конечно удобно, однако и накладывает на программиста определенные ограничения:

• Критическая секция охватывает все тело метода

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

• Нет возможности запретить параллельный доступ к совместно используемым объектам Предположим, в данном методе выполняется работа с некоторой очередью (экземпляр класса Queue). Конечно, благодаря наличию атрибута [MethodImpl (MethodImplOptions.Synchronized)] В рамках данного метода два потока не смогут параллельно работать с этой очередью и целостность данных будет обеспечена. Однако, ничто не запрещает какому-то другому потоку обратиться к этой же самой очереди в процессе выполнения какого-либо другого метода. Вот тут и возможны нарушения целостности, т. к. между различными потоками, выполняющими параллельно различные методы, нет никакой коммуникации.

Указанные выше проблемы решаются при использовании класса Monitor.

……

namespace MyServer {

…….

public class Account: MarshalByRefObject,

IAccumulator, IAudit {

……

public void Add(int sum) {

…….

Monitor.Enter(this);

try {

_sum += sum;

}

finally {

Monitor.Exit(this);

}

……

}

……

}

}


Вызов статического метода Monitor.Enter () помечает начало критической секции, а вызов метода Monitor.Exit () — ее конец. Аргумент в методе Enter представляет собой ссылку на некоторый объект. В данном случае это ссылка на экземпляр класса Account, на котором и вызван метод Enter, однако ничто не мешает указать ссылку на какой-либо другой объект.

Объект, на который указывает ссылка при вызове Enter, начинает играть роль "эстафетной палочки". Поток, которому удалось вызвать Monitor.Enter (obj), входит в данную критическую секцию, и никакой другой поток не получит ответа от вызова Monitor.Enter (obj), пока первый поток не вызовет Monitor.Exit (obj). Все потоки, сделавшие вызов Monitor.Enter (obj), находятся в одной очереди потоков готовых к выполнению, и эта очередь связана с объектом obj.

Использование блока try и включение вызова Monitor.Exit (obj) в блок finally способствует повышению надежности программирования. Если даже после входа в критическую секцию будет сгенерировано какое-то исключение, вызов Monitor.Exit (obj) будет выполнен в любом случае, и очередной готовый к выполнению поток, заблокированный при вызове Monitor.Enter (obj), начнет выполняться.

Хотя, как указывалось ранее, в качестве "эстафетной палочки" можно использовать любой объект, разумно использовать именно тот объект, ради безопасного доступа к которому и была сформирована данная критическая секция. В этом случае (если такой же подход будет использован при формировании всех критических секций) два различных потока не будут параллельно выполнять критичные для целостности данных операции над одним и тем же объектом.

Компилятор для C# допускает использование конструкции lock (obj) {} для задания критической секции. При этом неявно используется тот же класс Monitor:

……

namespace MyServer {

…….

public class Account: MarshalByRefObject,

IAccumulator, IAudit {

…….

public void Add(int sum) {

lock(this) {

_sum += sum;

}

}

…….

}

}


Имеются еще два метода класса Monitor, которые используются в коде атрибута синхронизации. Это Monitor.Wait () и Monitor.Pulse ().

Рассмотрим следующую модификацию предыдущего примера:

…….

namespace MyServer {

…….

public class Account: MarshalByRefObject,

IAccumulator, IAudit {

…….

public void Add(int sum) {


lock(this) {

Console.WriteLine (Thread.CurrentThread.GetHashCode ()};

int s = _sum;

Thread.Sleep(1);

_sum = s + sum;

if (_sum == 5) {Monitor.Wait(this);}

if (_sum == 505) {Monitor.Pulse(this);}

}

}

……

}


Напомним, что данный фрагмент кода выполняется на сервере MyServer.ехе, к которому параллельно могут обращаться несколько клиентов. Каждый клиент (приложение MуАрр) посылает на сервер 100 раз по 5 условных единиц.

Выводя на консоль хеш потока, мы можем отследить чередование рабочих потоков в очереди готовых к выполнению потоков. Сохранение текущей величины счета в локальной переменной s и вызов Thread.Sleep (1) используются для более явного выявления эффектов, связанных с многопоточностью.

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

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

В связи с выполнением условия _sum == 5 выполняется вызов Monitor.Wait (this). В этот момент первый поток освобождает объект this и становится в очередь ожидания. Эта еще одна, связанная с объектом очередь (наряду с очередью потоков, готовых к выполнению). Разница между ними состоит в следующем. Очередной поток из очереди готовых к выполнению потоков начинает выполняться, если текущий исполняемый поток завершил выполнение критической секции (вызвал Monitor.Exit (this), то есть освободил объект this). Потоки из очереди ожидания становятся в очередь потоков готовых к выполнению, если текущий исполняемый поток вызвал Monitor.Pulse (this), сигнализируя тем самым, что состояние объекта this изменилось и ожидающие потоки могут работать с данным объектом.

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


Делегаты, регистрация callback делегата в пуле рабочих потоков

В коде метода InitIfNecessary класса SynchronizationAttribute используются упомянутые в заголовке данного раздела сущности. Познакомимся с их применением в процессе разбора следующего примера:

using System;

using System.Threading;


public class Test {

private AutoResetEvent _myEvent;

private int _count = 0;


public Test() {

Console.WriteLine("»> Test constructor thread = " +

Thread.CurrentThread.GetHashCode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);


_myEvent = new AutoResetEvent(false);


WaitOrTimerCallback myCallBackDelegate =

new WaitOrTimerCallback(this.MyCallBack);


ThreadPool.RegisterWaitForSingleObject Х

_myEvent,

myCallBackDelegate,

null,

100,

false);

}


public int count {

get { return _count; }

set { _count = value;}

}


private delegate String MyDelegate ();


private void MyCallBack (Object state, bool timedOut) {

Console.WriteLine("»> MyCallback thread = " +

Thread.CurrentThread.GetHashCode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);


MyDelegate hello = new MyDelegate(MyHello);

count++;

Console.WriteLine(hello() + " Count = " + count +

" timedOut = " + timedOut);

}

private String MyHello() {

return "Test_" + count +": ";

}

public void NewEvent() {

_myEvent.Set();

}

}


public class MyApp {


public static void Main () {

Console.WriteLine("ЮЮ> MyApp thread = " +

Thread.CurrentThread.GetHashCode() +

" IsPoolThread = " +

Thread.CurrentThread.IsThreadPoolThread);

Test test = new Test();

test.NewEvent();

Thread.Sleep(500);

test.NewEvent();

Thread.Sleep(1000);

}

}


Опишем прежде всего семантику нашего приложения.

Метод Main выполняется в основном потоке приложения. Все приложение в целом завершается по завершении этого метода (после возвращения из вызова функции Thread.Sleep (1000)). Параллельно с выполнением основного потока несколько раз успевает выполниться метод MyCallBack класса Test. Заметим, что этот метод выполняется так называемыми рабочими потоками, извлекаемыми системой из пула рабочих потоков. Рабочий поток не может пережить основной поток и по завершении последнего завершается и текущий рабочий поток.

Данное консольное приложение после запуска прежде всего выводит на консоль информацию о потоке, выполняющем код функции Main. Это хеш потока и информация о том, является ли данный поток рабочим потоком. Очевидно, что в данном случае значение последнего параметра должно быть равно false так как текущий поток является основным потоком приложения.

Далее вызывается конструктор класса Test. Здесь также выводится информация о потоке — том потоке, который выполняет код конструктора. Этот тот же самый основной поток приложения.

Далее в конструкторе создается экземпляр myEvent события типа AutoResetEvent. События в .NET еще не обсуждались в данном курсе, но в данном случае используется событие специального типа, реализованное в системе. В связи с этим пока будет достаточно рассмотреть только это событие и только в контексте данного примера.

Для работы с пулом потоков мы регистрируем с помощью статического метода ThreadPool.RegisterWaitForSingleObject делегат myCallBackDelegate специального известного системе типа WaitOrTimerCallback. Делегат является некоторым аналогом указателя на функцию (в данном случае на MуCаllBаск), которая должна иметь сигнатуру, заданную при объявлении делегата. В случае делегата типа WaitOrTimerCallback возвращаемое значение должно отсутствовать (void), первый аргумент должен иметь тип System.Object и может использоваться для передачи вызываемой функции произвольных данных, второй параметр должен иметь тип bool и система через него передает значение true, если данный вызов произошел в связи с истечением времени ожидания (см. объяснение ниже).

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

• Событие _myEvent (зарегестрированное вместе с делегатом) будет установлено в состояние signaled

Для установки события типа AutoResetEvent в данное состояние достаточно вызвать его метод Set ().

• Время ожидания превысило пороговое значение (четвертый параметр в ThreadPool.RegisterWaitForSingleObject)

Отсчет времени идет от момента регистрации делегата или от момента последнего его вызова.

Заметим, что при создании myEvent вызывался конструктор AutoResetEvent (false). Задание параметра false привело к созданию события, не находящегося в состоянии signaled. При задании в этом конструкторе параметра true инициированное событие сразу же находится в состоянии signaled, и делегат вызывается сразу же после его регистрации.

Событие AutoResetEvent обладает еще одним важным свойством, отраженным в его названии — оно переходит в исходное состояние автоматически после очередного вызова делегата.

Третий параметр в ThreadPool.RegisterwaitForSingleObject задает ссылку на объект, содержащий данные передаваемые связанной с делегатом функции при вызове последнего (в данном случае ничего не передается).

Последний параметр определяет, что делегат зарегистрирован навсегда (точнее в данном случае до момента уничтожения экземпляра класса Test, в конструкторе которого выполняется регистрация). Если бы последний параметр равнялся true, регистрация делегата была бы действительна только на один вызов.

Метод MyCallBack, ссылка на который передана конструктору при создании делегата myCallBackDelegate, прежде всего выводит на консоль информацию о потоке, в котором он исполняется. Как и раньше это хеш потока и данные о его типе. В данном случае мы полагаем, что значение последнего параметра при каждом вызове этого метода будет равно true, так как выполняться этот метод должен рабочим потоком.

Далее MyCallBack выводит на консоль некоторое сообщение. Это сообщение состоит из следующих частей:

1. Префикс

Ради демонстрации того, как можно объявлять делегат нового типа, префикс формируется излишне сложно — посредством использования делегата нового типа — MyDelegate. Из его объявления

2. private delegate String MyDelegate ();

видно, что с делегатом данного типа можно связать любую функцию, возвращающую String и не имеющую аргументов. В теле метода MyCallBack создается новый делегат hello

MyDelegate hello = new MyDelegate(MyHello);

которому передается ссылка на метод MyHello этого же класса Test. Именно этот метод и формирует префикс вида Tеst_XXX:, где вместо XXX будет подставляться порядковый номер текущего вызова метода MyCallBack.

3. Порядковый номер вызова данного метода

При каждом вызове метода MyCallBack счетчик count увеличивается на единицу.

4. Информация о причине вызова данного метода

Через параметр timedOut метод MyCallBack получает от системы информацию о причине его вызова. Если получено значение false, то этот метод был вызван благодаря тому, что кто-то установил событие myEvent в состояние signaled. Значение true будет получено в том случае, если метод был вызван по причине завершения срока ожидания.

Последний метод NewEvent класса Test как раз и может использоваться клиентами для перевода события _myEvent в состояние signaled.

Теперь вновь обратимся к коду метода Main.

После создания экземпляра test класса Test вызывается его метод NewEvent в результате чего из пула рабочих потоков извлекается новый поток, который и выполняет метод MyCallBack. Напомним, что после этого событие _myEvent автоматически переходит в начальное состояние.

Далее основной поток засыпает на 500 тс. В связи с тем, что интервал ожидания, заданный четвертым параметром в ThreadPool.RegisterWaitForSingleObject равен 100 mc, метод MyCallBack будет вызван несколько раз по причине завершения периода ожидания.

Далее во второй раз вызывается метод NewEvent, и MyCallBack вызывается по причине перехода события myEvent в состояние signaled.

И, наконец, основной поток засыпает еще на 1000 mс, в течении которых MyCallBack вызывается с интервалом 100 mс по причине завершения времени ожидания.

Через 1000 mс основной поток просыпается и выполнение всего приложения (включая все рабочие потоки) завершается.

Ниже приводится вывод на консоль, полученный после запуска данного приложения:

>>> МуАрр thread = 16 IsPoolThread = False

>>> Test constructor thread = 16 IsPoolThread = False

>>> MyCallback thread = 18 IsPoolThread = True

Test_1: Count = 1 timedOut = False

>>> MyCallback thread = 18 IsPoolThread = True

Test_2: Count = 2 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_3: Count = 3 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_4: Count = 4 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_5: Count = 5 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_6: Count = 6 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_7: Count = 7 timedOut = False

>>> MyCallback thread = 18 IsPoolThread = True

Test_8: Count = 8 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_9: Count = 9 timedOut = True

>>>MyCallback thread = 18 IsPoolThread = True

Test_10: Count = 10 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_11: Count = 11 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_12: Count = 12 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_13: Count = 13 timedOut = True

>>>MyCallback thread = 18 IsPoolThread = True

Test_14: Count = 14 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test_15: Count = 15 timedOut = True

>>> MyCallback thread = 18 IsPoolThread = True

Test 16: Count = 16 timedOut = True


Возвращаемся к коду инициализации атрибута

Теперь можно более подробно обсудить код метода InitIfNecessary. Все тело этого метода включено в критическую секцию lock(this) {}. Здесь this является ссылкой на экземпляр текущего класса (SynchronizationAttribute), который и является собственно свойством синхронизации (как контекста, так и домена синхронизации). Таким образом, при входе текущего потока в данную критическую секцию никакой другой поток не может войти в эту секцию (и в любую другую типа lock (obj) {}, где obj является ссылкой на данное свойство синхронизации).

Далее проверяется условие _asyncWorkEvent == null. Это условие выполняется только тогда, когда текущее свойство синхронизации еще не инициализовано, т. е. в данный момент формируется новый домен синхронизации и текущее свойство будет его свойством синхронизации. Именно в этом случае выполняется инициализация свойства. В противном случае код инициализации пропускается, т. к. текущее свойство уже инициализирование ранее.

Инициализация состоит из следующих шагов:

• Создается экземпляр _asyncWorkEvent события AutoResetEvent

Данное событие будет использовано для уведомления системы о том, что очередной рабочий поток из пула потоков может выполнить очередной вызов, сохраненный в очереди вызовов (см. следующий пункт). Начальное состояние данного события не равно signaled, и для уведомления системы это событие надо перевести в состояние signaled (после чего оно автоматически вернется в исходное состояние).

• Создается экземпляр _workItemQueue очереди Queue

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

• Создается список _asyncLcidList

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

• Создается делегат callBackDelegate типа WaitOrTimerCallBack

Этот делегат хранит ссылку на функцию DispatcherCallBack, которая и будет обрабатывать вызовы, извлекаемые из очереди вызовов.

• Регистрация делегата callBackDelegate и события _asyncWorkEvent в пуле рабочих потоков

Для регистрации используется статический метод RegisterWaitForsingieObject класса ThreadPool. Третий параметр в вызове данного метода равен null, что говорит о том, что функции DispatcherCallBack не передаются никакие данные. Величина интервала ожидания timeout, по истечении которого автоматически вызывается делегат (если ранее состояние _asyncWorkEvent не было переведено в состояние signaled), задается при инициализации атрибута синхронизации и доступна только для чтения:

• private static readonly UInt32 _timeOut = (UInt32)0x7fffffff;

Последний параметр в вызове метода RegisterWaitForSingleObject равен false, что означает, что данная регистрация сохраняется до момента уничтожения самого свойства синхронизации.


Обработка вызова, извлекаемого из очереди вызовов

Прежде всего рассмотрим класс internal class Workltem {….. } экземпляры которого используются для хранения информации о вызовах в очереди вызовов.


Представление вызова в виде работы — экземпляра класса WorkItem

Каждый вызов представляется экземпляром класса WorkItem, в котором необходимая информация задается следующими полями:

internal IMessage _reqMsg

Это поле хранит ссылку на объект, представляющий собственно вызов в форме сообщения. Именно в этой форме вызов передается между контекстами клиента и сервера. Соответствующий класс должен реализовать интерфейс message.

internal IMessageSink _nextSink

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

internal IMessageSink _replySink

Вызовы разделяются на два типа: синхронные и асинхронные. В случае синхронного вызова вызывающая сторона блокируется до получения ответа, в асинхронном случае такая блокировка не выполняется. Однако, в асинхронном случае может оказаться необходимым как-то обеспечить уведомление вызывающей стороны о завершении вызова и о его результатах. В данном случае в вызов включается ссылка _replySink на специальный перехватчик, которому система должна передать уведомление. В случае синхронного вызова значение данного поля равно null.

internal IMessage _replyMsg

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

internal Context _ctx

Домен синхронизации может состоять из нескольких контекстов, и только один из них содержит свойство синхронизации данного домена. С каждым контекстом связана своя цепочка перехватчиков. В каждой такой цепочке имеется перехватчик, ассоциированный со свойством синхронизации. Вызов, перемещающийся по некоторой цепочке перехватчиков в некоторый контекст, прерывает свое путешествие в перехватчике, ассоциированном со свойством синхронизации, и отправляется в очередь вызовов в виде работы — экземпляра класса WorkItеm. Эта очередь поддерживается в свойстве синхронизации данного домена, которое может размещаться совсем не в том контексте, куда первоначально направлялся вызов. Поле _ctx используется для хранения информации о первоначальном контексте, в котором должен выполняться данный вызов после его освобождения из очереди.

internal LogicalCallContext _callCtx

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


Немного про асинхронные вызовы

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

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

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

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

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

using System;

using System.Threading;

using System.Runtime.Remoting;


public class Server {


public static bool Sum(int x, int y, out int z) {

Console.WriteLine(

"Server (Sum method) thread = " +

Thread.CurrentThread.GetHashCode()+

"; PoolThread = "+

Thread.CurrentThread.IsThreadPoolThread);


Thread.Sleep(1000);


z = 0;

try {

z = checked((int)(x + y));

}

catch (Exception) {

return false;

}

return true;

}


public static bool MultBy2(int x, out int y) {

Console.WriteLine {

"Server (MultBy2 method) thread = " +

Thread.CurrentThread.GetHasheode()+

"; PoolThread = "+

Thread.CurrentThread.IsThreadPoolThread);


Thread.Sleep(1000);


y = 0;

try {

у = checked((int) (x*2));

}

catch (Exception) {

return false;

}

return true;

}

}


Public class Client {


private static int workCount = 0;


private delegate bool HardFunction2Args (

int x, int y, out int result);

private delegate bool HardFunctionlArg (

int x, out int result);

private static void SumCallback(IAsyncResult ar) {

int z;


HardFunction2Args sum =

(HardFunction2Args)ar.AsyncState;


bool result = sum.Endlnvoke(out z, ar);

if (result) Console.WriteLine (

"SumCallback: Sum = " + z);

else Console.WriteLine (

"SumCallback: Bad arguments for Server.Sum

workCount++;

}


private static void MultCallback(IAsyncResult ar) {

int z;


HardFunctionlArg mult =

(HardFunctionlArg)ar.AsyncState;

bool result = mult.Endlnvoke(out z, ar);


if (result) Console.WriteLine (

"MultCallback: MultBy2 = " + z);

else Console.WriteLine (

"MultCallback: Bad argument for MultBy2");

workCount++;

}


public static void Main() {


int sumResult, multResult, count = 0;


Console.WriteLine("Client thread = " +

Thread.CurrentThread.GetHashCode() + PoolThread = "+

Thread.CurrentThread.IsThreadPoolThread);


HardFunction2Args sum =

new HardFunction2Args(Server.Sum);


HardFunctionlArg mult =

new HardFunctionlArg(Server.MultBy2);


AsyncCallback sumCallback =

new AsyncCallback(SumCallback);


AsyncCallback multCallback =

new AsyncCallback(MultCallback);


IAsyncResult arSum = sum.Beginlnvoke(3, 4,

out sumResult, sumCallback, sum);


IAsyncResult arMult = mult.Beginlnvoke(5,

out multResult, multCallback, mult);


while (workCount < 2) {

Console.WriteLine("Client thread: count = "+ count++);

Thread.Sleep(100);

}


Console.WriteLine("Bye!");

}

}


Комментарии к коду.

Сервер и клиент представлены соответственно классами Server и Client.


Сервер

Сервер реализует два статических метода:

• Метод public static bool Sum(int x, int у, out int z) {… } обеспечивает сложение двух чисел типа int. Результат записывается в переменную z типа int. В случае возникновения переполнения возвращается false, при его отсутствии — true.

Временная сложность проводимых вычислений имитируется с помощью вызова Thread.Sleер(1000).

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

• Метод public static bool MuitBy2(int x, out int y) {… } обеспечивает умножение числа на 2 и реализован аналогично предыдущему методу.

Здесь важно отметить, что разработчик сервера не заботится о том, как именно будут вызываться методы сервера клиентами — синхронно или асинхронно. Все зависит от клиента. Он может вызывать методы сервера как синхронно, так и асинхронно.


Клиент. Типы используемых делегатов

В данном примере клиент вызывает методы сервера асинхронно, что достигается за счет использования делегатов.

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

HardFunction2Args

Этот тип определяется в классе client:

private delegate bool HardFunction2Args (int x, int y, out int result);

Данный делегат может делегировать вызов (как синхронный так и асинхронный) любому методу (как статическому так и нестатическому) любого класса с заданной сигнатурой (два входных параметра типа int, один выходной типа int, возвращаемое значение типа bool). В нашем случае вызов будет делегироваться методу Server::Sum.

HardFunctionlArg

Этот тип также определяется в классе Client:

private delegate bool HardFunctionlArg (int x, out int result);

Данный делегат может делегировать вызов (как синхронный так и асинхронный) любому методу (как статическому так и нестатическому) любого класса с заданной сигнатурой (один входной параметр типа int, один выходной типа int, возвращаемое значение типа bool). В данном случае вызов будет делегироваться методу Server::MultBy2.

AsyncCallback

Этот тип определен в System. Он может использоваться для делегирования вызова методу со следующей сигнатурой:

♦ один входной параметр типа IAsyncResult (тип определен в System);

♦ возвращаемое значение отсутствует (void).

В нашем случае делегаты данного типа будут использоваться для делегирования вызовов методам клиента Client::SumCallback и Client::MultCallback.

Метод

private static void SumCallback (IAsyncResult ar) {… }

клиента вызывается инфраструктурой асинхронных вызовов для уведомления клиента о том, что сделанный им ранее асинхронный вызов метода sum сервера завершен.

Аналогично, метод

private static void MultCallback (IAsyncResult ar) {… }

клиента вызывается инфраструктурой асинхронных вызовов для уведомления клиента о том, что сделанный им ранее асинхронный вызов метода MuitBy2 сервера завершен также.


Клиент. Инициирование асинхронных вызовов

Прежде чем обсуждать завершение асинхронных вызовов уместно рассмотреть их инициирование. Для этого обратимся к коду метода Client::Main.

Прежде всего клиент выводит на консоль хеш основного потока

(Thread. CurrentThread. GetHashCode()) и информацию о принадлежности данного потока классу рабочих потоков из пула потоков

(Thread.CurrentThread.IsThreadPoolThread)).

Далее создаются два делегата для инициирования асинхронных вызовов методов сервера. Делегат sum

HardFunction2Args sum = new HardFunction2Args(Server.Sum);

используется для асинхронного вызова метода Server::Sum, а делегат mult

HardFunctionlArg mult = new HardFunctionlArg(Server.MultBy2);

используется для асинхронного вызова метода Server::MultBy2.

Далее формируются делегаты sumCallback и multCallback

AsyncCallback sumCallback = new AsyncCallback(SumCallback);

AsyncCallback multCallback = new AsyncCallback(MultCallback);

которые будут использоваться инфраструктурой асинхронных вызовов для уведомления клиента о завершении соответственно Server::Sum и Server::MultBy2 вызовов.

Инициирование асинхронных вызовов производится клиентом следующим образом:

IAsyncResult arSum = sum.Beginlnvoke(3, 4, out sumResult, sumCallback, sum);

IAsyncResult arMult = mult.Beginlnvoke(5, out multResult, multCallback, mult);


Немного о делегатах в связи с асинхронными вызовами

Остановимся на некоторых вопросах, связанных с делегатами, имеющими отношение к асинхронным вызовам.

Делегаты sum и mult являются экземплярами ненаследуемых классов, производных от класса System.MuiticastDelegate. Система автоматически формирует эти классы, и, в том числе, реализации их методов Invoke, Begininvoke и EndInvoke. Рассмотрим, для примера, сигнатуры этих методов для делегата sum:

public bool Invoke(int, int, out int)

Данный метод может использоваться для синхронного вызова метода Server::Sum. Первые два аргумента используются для передачи по значению суммируемых величин, последний — для передачи по ссылке результата, возвращаемое значение говорит об отсутствии переполнения.

В нашем случае этот метод будет вызван инфраструктурой асинхронных вызовов после инициирования вызова метода sum клиентом.

public IAsyncResult Begininvoke(int, int, out int, AsyncCallback, Object)

Данный метод используется для инициирования асинхронного вызова метода Server::Sum клиентом.

Смысл первых трех параметров описан в предыдущем пункте.

Четвертый параметр служит для передачи ссылки на делегат типа AsyncCallback, который будет использоваться инфраструктурой асинхронных вызовов для вызова callback функции на стороне клиента для уведомления последнего о завершении вызова. Этот параметр может быть задан как null. В этом случае клиент может использовать другие способы получения уведомления о завершении вызова.

Последний параметр задает ссылку на некоторый объект, которая будет доступна из объекта, полученного как результат инициирования асинхронного метода. В данном случае мы будем тут задавать ссылку на делегат sum. Это позволит клиенту в рамках callback функции SumCallback получить доступ к делегату sum и завершить асинхронный вызов, вызвав метод EndInvoke. Если callback функция не используется, этот параметр можно задать равным null.

Возвращаемое значение типа IAsyncResult дает клиенту ссылку на объект, который может использоваться последним для получения информации о выполнении асинхронного вызова. В частности, свойство bool Completed {get; } интерфейса IAsyncResult может использоваться клиентом для опроса инфраструктуры асинхронных вызовов — возвращаемое значение равно true, если вызов завершен. Это один из способов получить информацию о завершении асинхронного вызова без использования callback функции.

public bool EndInvoke(out int, IAsyncResult)

Данный метод вызывается клиентом для завершения асинхронного вызова Server::Sum и получения результатов. В нашем случае вызов этого метода выполняется в callback функции SumCallback.

Первый параметр — результат операции сложения, возвращаемое логическое значение равно true, если в процессе вычислений не произошло переполнения.

Последний параметр типа IAsyncResult задается клиентом. Это ссылка, полученная клиентом в результате инициирования асинхронного вызова путем вызова метода BeginInvoke.


Клиент. Обработка уведомления о завершении асинхронного вызова

Теперь, обсудив механизм инициирования асинхронного вызова, следует рассмотреть вопрос о обработке уведомления о его завершении. Остановимся для примера на методе

private static void SumCallback(IAsyncResult ar) {… }

Этот метод вызывается инфраструктурой асинхронных вызовов по завершении асинхронного вызова, инициированного в Client::Main следующим образом:

IAsyncResult arSum = sum.Beginlnvoke(3, 4, out sumResult, sumCallback, sum);

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

Прежде всего клиент использует полученную ссылку для получения ссылки на делегат sum:

HardFunction2Args sum = (HardFunction2Args)ar.AsyncState;

Далее клиент получает результаты завершенного асинхронного вызова

bool result = sum.Endlnvoke(out z, ar);

и выводит их на консоль

if (result) Console.WriteLine ("SumCallback: Sum = " + z);

else Console.WriteLine("SumCallback: Bad arguments for Server.Sum");

Все завершается увеличением на 1 статического счетчика workCount, служащего для подсчета числа выполненных асинхронных вызовов.


Клиент. Что он делает полезного во время ожидания

Вернемся снова к коду в Client::Main. Непосредственно после строк, инициирующих асинхронные вызовы, идет следующий код:

while (workCount < 2) {

Console.WriteLine("Client thread: count = "+ count++);

Thread.Sleep(100);

}

Именно тут клиент выполняет некоторую работу в ожидании завершения обоих асинхронных вызовов. До тех пор, пока счетчик числа завершенных асинхронных вызовов workCount не достигнет значения 2, клиент выводит на консоль очередное положительное целое с интервалом в 100 тс.

Результаты приведены ниже:

Client thread =16; PoolThread = False

Client thread: count = 0

Server (Sum method) thread =25; PoolThread = True

Client thread: count = 1

Client thread: count = 2

Client thread: count = 3

Client thread: count = 4

Client thread: count = 5

Server (MultBy2 method) thread =27; PoolThread = True

Client thread: count = 6

Client thread: count = 7

Client thread: count = 8

Client thread: count = 9

Client thread: count = 10

SumCallback: Sum = 7

Client thread: count = 11

Client thread: count = 12

Client thread: count = 13

Client thread: count = 14

Client thread: count = 15

MultCallback: MultBy2 = 10

Bye!


Обсуждение результатов:

• Видно, что клиентский поток не является потоком из пула рабочих потоков.

• Серверные методы (Sum и MuitBy2) выполняются асинхронно в различных рабочих потоках.

• Во время выполнения асинхронного вызова (от его инициирования до вызова соответствующей callback функции должно пройти 1000 mс) клиент успевает вывести на консоль 10 строк с очередными значениями переменной count.

• Завершается основной поток по завершении последнего асинхронного вызова. Это обеспечивается за счет подсчета числа завершенных вызовов. Заметим, что рабочий поток не может пережить основной поток. Если бы основной поток не контролировал завершение рабочих потоков и завершился бы раньше, то не завершенные рабочие потоки были бы уничтожены.

• Выполнение второго асинхронного вызова начинается с некоторой задержкой. Это связано с тем, что при наличии непустой очереди работ новый рабочий поток формируется через 500 mс после возникновения необходимости в нем. Ранее созданный поток уничтожается, если он никому не понадобился в течении 30 с. Общее число потоков в пуле не должно превышать 25 (на один процесс).


Немного про контекст вызова

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

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

Свойство контекста вызова состоит из пары (имя свойства, значение свойства). Имя свойства должно иметь тип System.String, а в качестве его значения можно задать ссылку на любой объект (производный от System.Object). Если предполагается передача контекста вызова через границу контекста, домена приложения, процесса, машины, значение каждого передаваемого свойства должно быть экземпляром класса, определенного с атрибутом Serializable и производного от ILogicaiThreadAffinative. Определение интерфейса ILogicalThreadAffinative не содержит никаких методов и данный интерфейс используется просто как маркер классов, допускаемых для передачи в контексте вызова за пределы контекста, домена приложения и т. п.

Реально контекст вызова передается через ранее упомянутые границы в форме сообщения типа IMessage. В связи с этим и требуется сериализуемость всех передаваемых объектов (т. е. возможность их передачи по значению).

Ниже представлен пример, демонстрирующий применение контекста вызова, пересекающего границу между процессами. За основу взят многократно рассмотренный пример, связанный с перечислением 5 условных единиц со стороны клиента на счет, поддерживаемый сервером.

Рассмотрим прежде всего код сервера.


Сервер

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Threading;

using System.Runtime.Remoting.Contexts;

using System.Runtime.Remoting.Messaging;

using System.Reflection;


namespace MyServer {


[Serializable]

public class MyCallContextUserName:

ILogicalThreadAffinative {


private String _userName;


public MyCallContextUserName() {

_userName = Environment.UserName;

}


public String UserName {

get { return _userName; }

}

}


[Serializable]

public class MyCallContextServerName:

ILogicalThreadAffinative {


private Assembly _assembly;

private String _serverName;

public MyCallContextServerName() {

_assembly = Assembly.GetExecutingAssembly();

_serverName = _assembly.FullName;

}


public String ServerName {

get { return _serverName; }

}

}


public interface IAccumulator {

void Add(int sum);

}


public interface IAudit {

int Total();

}

[Synchronization()]

public class Account: ContextBoundObject,

IAccumulator, IAudit{


protected int sum = 0;


public void Add(int sum) {

this.sum += sum;


MyCallContextUserName userName =

(MyCallContextUserName)CallContext.GetData("UserName");


Console.WriteLine("UserName = " +

userName.UserName);


CallContext.SetData("ServerName",

new MyCallContextServerName());

}


public int Total() {

return this.sum;

}

}


public class AccountApp {

public static void Main() {


HttpChannel myChannel = new HttpChannel(8080);

ChannelServices.RegisterChannel(myChannel);


RemotingConfiguration.RegisterWellKnownServiceType {

typeof(Account), "Account",

WellKnownObjectMode.Singleton);


Console.WriteLine("Server is listening");

Console.ReadLine();

Console.WriteLine("Bye");

}

}

}


Относительно этого кода можно сделать следующие комментарии.

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

Во-первых, предполагается, что клиент перед вызовом метода Add добавил в контекст вызова свойство с именем UserName. Соответствующее значение содержит учетные данные пользователя, от имени которого было запущено клиентское приложение. Значение свойства UserName задается ссылкой на экземпляр класса

[Serializable]

public class MyCallContextUserName:

ILogicalThreadAffinative {…}

Из определения этого класса видно, что его экземпляры могут передаваться в контексте вызова за пределы текущего контекста, т. к. этот класс определяется с атрибутом сериализации и наследует интерфейс ILogicalThreadAffinative.

Конструктор данного класса

public MyCallContextUserName() {

_userName = Environment.UserName;

}

сохраняет в строковом поле _userName значение соответствующей переменной среды, получаемой как статическое свойство UserName класса Environment.

Используя свойство userName контекста вызова сервер в методе Add выясняет имя пользователя и выводит его на консоль:

MyCallContextUserName userName =

(MyCallContextUserName)CallContext.GetData("UserName");

Console.WriteLine("UserName = " +

userName.UserName);

Для доступа к нужному свойству используется статический метод GetData класса CallContext, которому в качестве параметра передается имя свойства. Полученное значение приводится к типу MyCallContextUserName.

Во-вторых, получив и выведя на консоль имя пользователя, сервер заканчивает выполнение метода Add, включая в контекст вызова свою информацию. Эту новую информацию сможет получить клиент, дождавшийся возврата из метода Add.

Итак, сервер добавляет в контекст вызова новое свойство с именем ServerName:

CallContext.SetData("ServerName",

new MyCallContextServerName()};

Класс MyCallContextS erverName определяется аналогично классу MyCallContextUserName.

Основная функциональность этого класса определяется его конструктором:

public MyCallContextServerName() {

_assembly = Assembly.GetExecutingAssembly();

_serverName = _assembly.FullName;

}


Здесь мы получаем ссылку на исполняемую сборку (т. е. на сборку сервера) и сохраняем в _serverName ее полное имя.

Остальная часть кода сервера не претерпела каких-либо изменений.


Клиент

using System; using MyServer;

using System. Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Net;

using System.Runtime.Remoting.Messaging;


public class MyApp {


public static void Main() {


HttpChannel с = new HttpChannel();

ChannelServices.RegisterChannel(c);


try {

Account a = (Account)Activator.GetObject(typeof(Account),

"http://localhost:8080/Account",

WellKnownObjectMode.Singleton);

CallContext.SetData("UserName",

new MyCallContextUserName());


a. Add(5);


Console.WriteLine("5 is sent to " +

((MyCallContextServerName)CallContext.GetData(

"ServerName")).ServerName);

Console.WriteLine("Total = " +a.Total());

}

catch(WebException e) {

Console.WriteLine(e.Message);

}

catch(Exception e) {

Console.WriteLine(e.Message);

}

finally!

Console.WriteLine("Bye");

}

}

}


Все отличие данного кода клиента от рассмотренных ранее примеров представлено в следующих строках

CallContext.SetData("UserName",

new MyCallContextUserName());


a. Add(5);

Console.WriteLine("5 is sent to " +

((MyCallContextServerName)CallContext.GetData (

"ServerName")).ServerName);


Перед вызовом метода Add клиент добавляет в контекст вызова уже рассмотренное свойство userName. В результате, при выполнении вызова Add, сервер получает доступ к учетным данным пользователя, от имени которого было запущено клиентское приложение, и может принимать решение о выполнении данного вызова.

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


Результаты

Ниже приведен вывод на консоль сервера. Видно, что сервер получил информацию о том, что клиентское приложение было запущено Администратором.

Server is listening

UserName = Администратор

Bye

А вот и вывод на консоль клиента:

5 is sent to MyServer, Version=0.0.0.0, Culture=neutral,

PublicKeyToken=null

Total = 5

Bye


Возвращаемся к классу Workltem


Конструктор

Конструктор в качестве входных параметров принимает вызов в форме сообщения (reqMsg), ссылку на следующий перехватчик, которому будет переадресован данный вызов после того, как он отстоит всю очередь (nextSink) и ссылку на перехватчик для результатов асинхронного вызова (replySink). В случае синхронного вызова последний параметр задается равным null.

Ниже приведен код конструктора:

internal Workltem(IMessage reqMsg, IMessageSink nextSink,

IMessageSink replySink) {

_reqMsg = reqMsg;

_replyMsg = null;

_nextSink = nextsink;

_replySink = replySink;

_ctx = Thread.CurrentContext;

_callCtx = CallContext.GetLogicalCallContext();

}


Судя по приведенному коду, этот конструктор вызывается в том контексте, в котором в последствии будет выполняться вызов, инкапсулируемый в данный момент в экземпляр класса WorkItem. Об этом говорят строки, в которых присваиваются значения полям ctx и _callContext. Вызов Thread.CurrentContext возвращает текущий контекст (ссылку на экземпляр класса Context), а вызов CallContext.GetLogicalCallContext возвращает контекст логического вызова (ссылку на экземпляр класса LogicalCallCcontext), соответствующие текущим контексту и потоку. Здесь следует отметить, что в .NET Framework класс CallContext не реализует метод GetLogicalCallContext.

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


Флаги

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

private const int FLG_WAITING = 0x0001;

private const int FLG_SIGNALED = 0x0002;

private const int FLG_ASYNC = 0x0004;

private const int FLG_DUMMY = 0x0008;

Флаг FLGg_WAITING означает, что работа поставлена в очередь, флаг FLG_SIGNALED указывает на то, что первая в очереди работа начинает исполняться, флаг FLG_ASUNC помечает асинхронные работы (работы, представляющие асинхронные вызовы), и, наконец, флаг FLG_DUMMY помечает работу-заглушку. Этот флаг помечает фиктивную работу,

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

Текущая комбинация флагов сохраняется в поле internal int flags, для задания и чтения которого используются следующие методы:

internal virtual void SetWaiting() {

_flags |= FLG_WAITING;

}

internal virtual bool IsWaiting() {

return (_flags &FLG_WAITING) == FLG_WAITING;

}

internal virtual void SetSignaled() {

_flags |= FLG_SIGNALED;

}

internal virtual bool IsSignaled() {

return (_flags & FLG_SIGNALED) == FLG_SIGNALED;

}

internal virtual void SetAsync() {

_flags |= FLG_ASYNC;

}

internal virtual bool IsAsync() {

return (_flags & FLG_ASYNC) == FLG_ASYNC;

}

internal virtual void SetDummy() {

_flags |= FLG_DUMMY;

}

internal virtual bool IsDummy() {

return (_flags & FLG_DUMMY) == FLG_DUMMY;

}


Выполнение работы

Самый важный метод класса WorkItem — это метод Execute, обеспечивающий выполнение текущей работы. Этот метод вызывается в тот момент, когда подошла очередь выполнения этой работы.

internal virtual void Execute() {


ContextTransitionFrame frame = new ContextTransitionFrame();

Thread.CurrentThread.EnterContext(_ctx, ref frame);

LogicalCallContext oldCallCtx =

CallContext.SetLogicalCallContext(_callCtx);


if (IsAsync()) {

_nextSink.AsyncProcessMessage(_reqMsg, _replySink);

}

else if (_nextSink!= null) {

_replyMsg = _nextSink.SyncProcessMessage(_reqMsg);

}


CallContext.SetLogicalCallContext(oldCallCtx);

Thread.CurrentThread.ReturnToContext(ref frame);

}


Метод Execute может выполняться в различных контекстах и потоках, поэтому прежде всего нужно восстановить ту среду, в которой находился вызов в момент его перехвата и инкапсулирования в работу типа WorkItem.

Для этого формируется фрейм, сохраняющий некоторую информацию о переходе их одного контекста в другой контекст (эта информация позже используется для возвращения в контекст, в котором началось исполнение метода Execute) и выполняется переход из текущего контекста в контекст, сохраненный в поле ctx:

ContextTransitionFrame frame = new ContextTransitionFrame();

Thread.CurrentThread.EnterContext(_ctx, ref frame);

Отметим, что в .NET Framework нет класса ContextTransitionFrame, а в сигнатуре класса Thread нет метода EnterContext.

Далее сохраняется текущий контекст вызова в оldCаllСontехt, а с текущим потоком связывается тот контекст вызова, который был сохранен в поле _сallContext:

LogicalCallContext oldCallCtx =

CallContext.SetLogicalCallContext(_callCtx);

К сожалениею, в сигнатуре класса CallContext в .NET Framework нет метода SetLogicalCallContext.

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

Интерфейс IMessageSink объявляет два метода, реализующие обработку соответственно синхронных и асинхронных вызовов. Это SyncProcessMessage И AsyncProcessMessage.

Таким образом, в зависимости от типа текущей работы (инкапсулированного в ней вызова), необходимо вызвать один из упомянутых вызовов на следующем перехватчике:

if (IsAsync()) {

_nextSink.AsyncProcessMessage(_reqMsg, _replySink);

}

else if (_nextSink!= null) {

_replyMsg = _nextSink.SyncProcessMessage(_reqMsg);

}


Вызов IsAsync возвращает true если текущая работа асинхронна. В этом случае на следующем перехватчике _nextSink вызывается AsyncProcessMessage и в качестве параметров передаются исходный вызов _reqMsg и ссылка на перехватчик для возвращения результата _replySink.

В случае синхронной работы вызывается SyncProcessMessage. Параметром является вызов, возвращаемое значение сохраняется в поле replyMsg.

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

И, наконец, текущий поток должен вернуться в старый контекст и связаться со старым контекстом вызова:

CallContext.SetLogicalCallContext(oldCallCtx);

Thread.CurrentThread.ReturnToContext(ref frame);

Завершая обсуждение класса WorkItem осталось упомянуть свойство ReplyMessage, с помощью которого можно получить ответ, возвращенный для синхронного вызова:

internal virtual IMessage ReplyMessage {

get {return _replyMsg;}

}


Извлечение работы из очереди и ее выполнение

Напомним, что вызовы, перехваченные перехватчиком, ассоциированным со свойством синхронизации, инкапсулируются в работу (экземпляр класса WorkItem) и записываются в очередь работ (_workItemQueue). Эта очередь поддерживается свойством синхронизации, и мы уже говорили об ее инициализации как составной части процесса инициализации свойства синхронизации в целом. Тогда же говорилось о том, что в пуле рабочих потоков регистрируется делегат callBackDelegate типа WaitOrTimerCallBack, который будет вызываться и выполняться некоторым рабочим потоком всякий раз, когда будет установлено в состояние signaled событие _asyncWorkEvent.

Забегая вперед можно сказать, что это событие будет устанавливаться в состояние signaled всякий раз, когда можно будет начинать выполнение очередной работы из очереди работ, причем эта работа должна иметь асинхронный тип (инкапсулирует асинхронный вызов). В этом случае свободный или вновь сгенерированный рабочий поток начнет выполнять данную асинхронную работу путем вызова упомянутого делегата.

Делегат callBackDelegate содержит ссылку на функцию

DispatcherCallBack:


WaitOrTimerCallback callBackDelegate =

new WaitOrTimerCallback(this.DispatcherCallBack);

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

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

Код функции DispatcherCallBack представлен ниже:

private void DispatcherCallBack(Object statelgnored,

bool ignored) {


Workltem work;


lock (_workItemQueue) {

work = (Workltem) _workltemQueue.Dequeue();

}


ExecuteWorkltem(work);

HandleWorkCompletion();

}


В соответствии с определением типа WaitorTimerCallBack функция DispatcherCallBack имеет два параметра. Входной параметр типа Object (третий параметр при вызове ThreadPool.RegisterWaitForSingleObject. В нашем случае null) используется для задания подлежащей обработке информации. Выходной параметр типа bool принимает значение true в том случае, если вызов зарегистрированного в пуле рабочих потоков соответствующего делегата (в нашем случае callBackDelegate) произошел по причине истечения времени ожидания (в нашем случае это значение поля _timeOut). Если же вызов упомянутого делегата произошел в связи с тем, что зарегистрированное в этом же пуле событие типа AutoResetEvent (в нашем случае _asyncWorkEvent) перешло в состояние signaled, то второй параметр принимает значение false. В нашем случае оба эти параметра не учитываются.

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

Единственной операцией, выполняемой в данной критической секции, является извлечение из очереди работ очередной работы (с удалением из очереди). Здесь предполагается, что все необходимые для этого условия уже выполнены. О том, каковы эти условия, мы будем говорить позже.

После выхода из критической секции (освобождающего очередь работ для других потоков), вызываются последовательно ExecuteWorkltem(work) и HandleWorkCompletion ().

Реализация метода ExecuteWorkItem в рассматриваемом классе SynchronizationAttribute Очень проста:

internal void ExecuteWorkltem(Workitem work) {

work.Execute();

}

Тут все сводится к уже рассмотренному методу Execute класса WorkItem. Иными словами, исполняющий этот метод поток переходит в тот контекст, где должен будет выполняться вызов, с этим потоком связывается тот самый контекст вызова, который сопровождал этот вызов до его перехвата. После этого вызов передается следующему перехватчику, который будет его обрабатывать в зависимости от типа вызова (синхронный или асинхронный). В случае асинхронного вызова сразу же после этого происходит возврат потока в исходный контекст и восстанавливается его связь с исходным контекстом вызова. В случае синхронного вызова происходит тоже самое, но только после возврата ответа.

Вызов метода HandleWorkCompletion() должен выполнить некоторую подготовительную работу к выполнению следующей работы из очереди работ. Мы рассмотрим этот метод позже. Сейчас же следует начать с начала, т. е. рассмотреть весь процесс перехвата вызова и включения его в очередь (или, в некоторых случаях, исполнения без включения в очередь).


Перехват входящего вызова

Формирование перехватчика входящих вызовов

Класс SynchronizationAttribute реализует интерфейс IContributeServerContextSink. Благодаря этому факту, при формировании нового контекста синхронизации (в новом или в старом домене синхронизации) автоматически вызывается метод GetServerContextSink, объявленный в данном интерфейсе, для формирования перехватчика входящих вызовов для данного контекста. Ниже приводится код этого метода из Rotor:

public virtual IMessageSink GetserverContextsink {

IMessageSink nextSink) {


InitlfNecessary();


SynchronizedServerContextSink propertySink =

new SynchronizedServerContextSink (

this,

nextSink);

return (IMessageSink) propertySink;

}


Единственным входным параметром является ссылка на перехватчик, последний на данный момент в цепочке перехватчиков входящих вызовов для данного контекста. Возвращаемое значение является ссылкой на новый перехватчик, который будет добавлен в эту цепочку.

Прежде всего выполняется инициализация свойства синхронизации, если это необходимо. Метод InitIfNeccessary был рассмотрен ранее. Реально он будет выполнять инициализацию свойства синхронизации только в том случае, когда текущий контекст является первым контекстом домена синхронизации. В противном случае свойство синхронизации уже было инициализировано ранее.

Далее формируется экземпляр класса SynchronizedServerContextSink, который будет рассмотрен позже. Этот объект и будет выступать в роли перехватчика входящих вызовов. Ему передаются два параметра:

this — ссылка на свойство синхронизации (экземпляр данного класса SynchronizationAttribute) — новое или ранее сформированное, живущее в первом контексте домена синхронизации

nextsink — ссылка на следующий в цепочке перехватчик.

И, наконец, возвращается полученная ссылка на новый перехватчик.

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


Как перехватчик обрабатывает синхронные вызовы

Теперь рассмотрим класс SynchronizedServerContextSink:

internal class SynchronizedServerContextSink

: InternalSink, IMessageSink {… }

Мы видим, что данный класс наследует классу InternalSink и реализует интерфейс IMessageSink. Класс InternalSink отсутствует в .NET. Интерфейс IMessageSink объявляет методы, которые должны быть реализованы перехватчиками всех типов.

Класс SynchronizedServerContextSinkсодержит два поля данных:

internal IMessageSink _nextSink;

internal SynchronizationAttribute _property;

Первое содержит ссылку на следующий перехватчик, а второе — на свойство синхронизации текущего домена синхронизации.

Единственный конструктор инициирует эти поля:

internal sSynchronizedServerContextSink (

SynchronizationAttribute prop,

IMessageSink nextSink) {


_property = prop;

_nextSink = nextSink

}


Каждый перехватчик должен обеспечить обработку как синхронных, так и асинхронных вызовов. Соответствующие методы объявлены в интерфейсе IMessageSink. Это SyncProcessMessage и AsyncProcessMessage.

В данном перехватчике обработка синхронных вызовов осуществляется следующим образом:

public virtual IMessage SyncProcessMessage(IMessage reqMsg) {

Workitem work = new Workitem(reqMsg,

_nextSink, null);

_property.HandleWorkRequest(work);

return work.ReplyMessage;

}


Собственно вызов в форме сообщения типа IMessage передается как единственный входной параметр. Возвращаемое значение (также типа IMessage) содержит результат, полученный данным перехватчиком от следующего в цепочки перехватчика после того, как вызов пройдет через всю цепочку перехватчиков до сервера, будет обработан на сервере, а ответ от него пройдет через всю цепочку перехватчиков в обратном направлении до данного перехватчика. Тут надо заметить, что на этом пути в каком-либо промежуточном перехватчике вызов может быть преобразован в асинхронную форму.

Роль данного перехватчика состоит в инкапсуляции вызова в объект типа WorkItem и его сохранении в очереди работ. Для этого вызывается конструктор WorkItem (reqMsg, _nextSink, null), первые два параметра которого задают вызов в форме сообщения и ссылку на следующий перехватчик. Третий параметр используется только в случае асинхронных вызовов. По умолчанию инкапсулирующая вызов работа work относится к синхронному типу.

После инкапсуляции вызова соответствующая работа передается свойству синхронизации:

_property.HandleWorkRequest(work);

Метод HandleWorkRequest класса SynchronizationAttribute ответственен за запись инкапсулированного вызова в очередь работ, за своевременное извлечение его из очереди и передачу следующему перехватчику, и, наконец, за получение ответа. Ответ доступен через свойство ReplyMessage работы, инкапсулирующей вызов. Значение этого свойства и возвращается как результат вызова метода SyncProcessMessage.


Как свойство синхронизации обрабатывает инкапсулированный синхронный вызов, полученный от перехватчика

Теперь временно прервем процесс изучение класса SynchronizedServerContextSink И рассмотрим метод HandleWorkRequest класса SynchronizationAttribute. Ниже приведена часть кода этого метода, которая относится к обработке именно синхронных вызовов:

internal virtual void HandleWorkRequest(WorkItem work) {

bool bQueued;

if (!IsNestedCall(work._reqMsg)) {

if (work.IsAsync()) {

…….

}

else {

lock(work) {

lock(_workItemQueue) {

if ((!_locked) &&

(_workltemQueue.Count == 0)) {

_locked = true;

bQueued = false;

}

else {

bQueued = true;

work.SetWaiting();

_workltemQueue.Enqueue(work);

}

}

if (bQueued == true) {

Monitor.Wait(work);

if (!worк. IsDummy()) {

DispatcherCallBack(null, true);

}

else {

lock(_workltemQueue) {

_workItemQueue.Dequeue();

}

}

}

else {

if (!worк. IsDummy()) {

work.SetSignaled();

ExecuteWorkltem(work);

HandleWorkCompletion();

}

}

}

}

}

else {

work.SetSignaled();

work.Execute();

}

}


Прежде всего выясняется — является ли инкапсулированный вызов work._reqMsg вложенным вызовом, т. е. вызовом, инициированным в процессе выполнения выполняемого в данный момент синхронного или исходящего асинхронного вызова:

if (!IsNestedCall(work._reqMsg)) {……..

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

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

Рассмотрим определенный В ЭТОМ же классе SynchronizationAttribute метод IsNestedCall.

internal bool IsNestedCall(IMessage reqMsg) {

bool bNested = false;

if (!IsReEntrant) {

String lcid = SyncCallOutLCID;

if (lcid!= null) {

LogicalCallContext callCtx =

(LogicalCallContext)

reqMsg.Properties[Mes sage.CallContextKey];


if (callCtx!=null &&

lcid.Equals(callCtx.RemotingData.LogicalCalllD)) {


bNested = true;

}

}

if (IbNested && AsyncCallOutLCIDList.Count>0) {

LogicalCallContext callCtx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];

if (AsyncCallOutLCIDList.Contains(

callCtx.RemotingData.LogicalCalllD)) {


bNested = true;

}

}

}

return bNested;

}


Этот метод возвращает true если текущий контекст (точнее, текущий домен синхронизации) нереентерабельный, а новый вызов (reqMsg) является вложенным для исполняемого в данный момент синхронного вызова, или для одного из исходящих асинхронных вызовов. Во всех остальных случаях возвращается false.

Вначале выясняется синхронность выполняемого в данный момент вызова:

String lcid = SyncCallOutLCID;

if (lcid!= null) {

......

}

Свойство SyncCallOutLCID атрибута синхронизации возвращает идентификатор логического вызова исполняемого в данный момент вызова, если этот вызов синхронный. В противном случае возвращается null.

Теперь в случае синхронности исполняемого вызова мы получаем доступ к контексту вызова для вызова reqMsg:

LogicalCallContext callCtx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];

Надо заметить, что тут имеется ввиду класс Message из пространства имен System.Runtime.Remoting.Messaging. Реализация такого класса в этом пространстве имен имеется в Rotor, но отсутствует в .NET. Статическое поле CallContextKey равно __CallContext.

Если доступ к контексту вызова получен, то проверяется, что идентификатор логического вызова исполняемого в данный момент синхронного вызова lcid совпадает с идентификатором логического вызова для вызова reqMsg. В случае их совпадения флаг вложенности bNested получает значение true:

if (callCtx!=null &&

lcid.Equals(callCtx.RemotingData.LogicalCalllD)) {


bNested = true;

}


Здесь ОПЯТЬ приходится отметить, что в .NET у класса LogicalCallContext нет свойства RemotingData типа CallContextRemotingData, нет и самого класса CallContextRemotingData и его свойства LogicalCallID.

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

Выполнение условия AsyncCallOutLCIDList.count>0 означает, что список идентификаторов логических вызовов, соответствующих исходящим асинхронным вызовам, не пуст. Свойство AsyncCallOutLCIDList типа ArrayList возвращает ссылку на этот список.

В этом случае как и ранее выясняется контекст вызова reqMsg и проверяется, что соответствующий ему идентификатор логического вызова содержится в списке исходящих асинхронных вызовов. В случае успеха флаг bNested получает значение true:

LogicalCallContext callCtx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];

if (AsyncCallOutLCIDList.Contains(

callCtx.RemotingData.LogicalCalllD)) {


bNested = true;

}


Возвращаемся вновь к коду метода HandleWorkRequest, который мы рассматриваем в данный момент только для случая инкапсулированного синхронного вызова.

Здесь возможны два случая:

IsNestedCall(work._reqMsg) == false

Это случай соответствует реентерабельному контексту или невложенному вызову.

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

IsNestedCall(work._reqMsg) == true

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

Другая возможность — инкапсулированный вызов является вложенным для исходящего асинхронного вызова. Но можно ли этот инкапсулированный вызов исполнять вне очереди и в этом случае? Отложим обсуждение этого вопроса.

Итак, в первом случае (не вложенный вызов), поток входит в критическую секцию

lock(work) {

……

}

заблокировав доступ к работе work, инкапсулирующей входящий вызов. В рамках этой критической секции поток ожидает входа в критическую секцию, для доступа к которой надо заблокировать очередь работ:

lock(_workltemQueue) {

……

}

Войдя в эту вторую критическую секцию, поток проверяет — имеется ли блокировка домена синхронизации. Поле _locked атрибута синхронизации равно false в том случае, когда после выполнения очередной работы обнаруживается, что очередь работ пуста, то есть нет работы, которую следует начинать выполнять в рамках данного домена синхронизации.

Если домен синхронизации не заблокирован, и очередь работ пуста, то домен блокируется, но флаг постановки работы в очередь bQueued не задается (работа может быть выполнена сразу же):

if ((!_locked) &&

(_workltemQueue.Count == 0)) {

_locked = true;

bQueued = false;

}

В противном случае задается флаг bQueued постановки работы в очередь, в самой работе задается флаг, указывающий на то, что она стоит в очереди (work.SetWaiting ()) и выполняется реальная запись работы в очередь:

else {

bQueued = true;

work.SetWaiting();

_workltemQueue.Enqueue(work);

}

Теперь поток выходит из второй (внутренней) критической секции, разблокировав очередь работ. Но что ему делать дальше?

Если работа была поставлена в очередь, то в силу синхронности вызова данный поток должен сам перейти в состояние ожидания, из которого он выйдет только тогда, когда только что поставленная в очередь работа продвинется в ее начало и будет готова для выполнения:

if (bQueued == true) {

Monitor.Wait(work);

if (!work.IsDummy()) {

DispatcherCaiiBack(null, true);

}

else {

lock(workltemQueue) {

_workltemQueue.Dequeue();

}

}

}


Вызов Monitor.Wait (work) переведет текущий поток в состояние ожидания, причем этот поток освободит ранее заблокированную им работу work и будет ожидать сигнала, говорящего о том, что состояние объекта work изменилось, нужно проснуться и продолжить работу с этим объектом. Это сигнал будет выдан другим потоком, заметившим, что работа work первая в очереди и нет препятствий для ее выполнения.

Следующая за Monitor.Wait(work); строка кода будет выполняться уже разбуженным потоком, который вновь получает исключительный доступ к объекту work. Если данная работа не является работой-заглушкой (об этом позже), то вызывается уже рассмотренный метод DispatcherCallBack, который и извлечет эту работу из очереди, выполнит ее и инициирует выполнение следующей работы. В случае же работы-заглушки просто блокируется очередь работ и эта работа-заглушка удаляется из очереди.

А вот что происходит с работой, которую не пришлось ставить в очередь:

else {

if (!work.IsDummy()) {

work.SetSignaled();

ExecuteWorkltem(work);

HandleWorkCompletion();

}

}

Если это не заглушка, устанавливается флаг готовности к выполнению, работа выполняется и потом все подготавливается для выполнения следующей работы.

Здесь надо бы рассмотреть код для метода HandleWorkCompletion(), который собственно и подготавливает все для выполнения следующей работы, но в очередной раз отложим это рассмотрение на потом.

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

if (!IsNestedCall(work._reqMsg)) {

}

else {

work.SetSignaled();

work.Execute();

}

Тут устанавливается флаг готовности к выполнению, работа выполняется, но никакой подготовки к выполнению следующей работы не проводится, т. к. уже имеется находящийся в состоянии выполнения вызов, ожидающий выполнения данного синхронного вызова.

Тут все в порядке, если только инкапсулированный вызов вложен в исполняемый синхронный вызов. Если же этот инкапсулированный вызов вложен в некоторый исходящий асинхронный вызов, то могут быть проблемы. Например, в данное время в данном домене синхронизации уже может выполняться некоторый синхронный вызов и параллельное выполнение еще одного вызова противоречит логике использования домена синхронизации (?!).

Теперь настало время рассмотреть код ранее упомянутого метода HandleWorkCompletion(). Как и ранее рассмотрим только ту ветвь, которая связана с обработкой синхронных вызовов.

internal virtual void HandleWorkCompletion() {

Workitem nextWork = null;

bool bNotify = false;

lock (_workItemQueue) {

if (_workItemQueue.Count >= 1) {

nextWork = (Workitem) _workltemQueue.Peek();

bNotify = true;

nextWork.SetSignaled();

}

else {

_locked = false;

}

}

if (bNotify) {

if (nextWork.IsAsync()) {

.......

}

else {

lock(nextWork) {

Monitor.Pulse(nextWork);

}

}

}

}


Данный метод вызывается по завершении обработки некоторой работы. В это время домен синхронизации еще блокирован (_locked == true).

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

Если в начале очереди находится синхронная работа (nextWork), то это означает, что имеется поток, который находится в состоянии ожидания того момента, когда работа nextWork будет готова к выполнению. Этот поток в свое время впал в состояние ожидания находясь в некоторой критической секции, в которую он попал заблокировав объект nextWork. Находясь в этой критической секции он был должен освободить эту блокировку, так как в то время работа nextWork была поставлена в очередь работ и этот поток не мог продолжить ее обработку. Теперь его пора разбудить. Это делает текущий поток, в котором выполняется код метода HandleWorkCompletion:

lock(nextWork) {

Monitor.Pulse(nextWork);

}

Текущий поток входит в критическую секцию (заблокировав nextWork) и уведомляет все заинтересованные потоки о том, что состояние объекта nextWork как-то изменилось. Эти находящиеся в состоянии ожидания потоки переходят в состояние готовности и по выходе текущего потока из данной критической секции пытаются захватить контроль над объектом nextWork. В нашем случае такой поток единственный и именно он ответственен за продолжение обработки вызова, инкапсулированного в работу nextWork. Конкретнее, это поток, выполняющий код метода HandieWorkRequest начиная со строки, следующей за строкой Monitor.Wait(work);


Как перехватчик обрабатывает асинхронные вызовы

Вновь возвращаемся к классу SynchronizedServerContextSink, который реализует интерфейс IMessageSink. В этом интерфейсе объявлен метод AsyncProcessMessage, который и должен реализовать обработку асинхронных вызовов в перехватчике входящих вызовов. Вот так этот метод реализован в классе SynchronizedServerContextSink:

public virtual IMessageCtrl AsyncProcessMessage(

IMessage reqMsg, IMessageSink replySink) {


Workltem work = new WorkItem (

reqMsg,

_nextSink,

replySink);

work.SetAsync();

_property.HandleWorkRequest(work);

return null;

}


В данном случае мы имеем два входных параметра. Первый задает вызов в форме сообщения типа IMessage (как и в случае синхронного вызова). А вот второй параметр специфичен для асинхронных вызовов. Он задает ссылку на еще один перехватчик, который ответственен за доставку уведомления о завершении асинхронного вызова. И, наконец, возвращаемое значение имеет также специфический для асинхронных вызовов тип — IMessageCtrl. В .NET интерфейс IMessageCtrl объявляет единственный метод Cancel, с помощью которого можно прервать выполнение асинхронного вызова. В данном случае метод AsyncProcessMessage всегда возвращает null, не предоставляя такую возможность.

Функциональность данного метода вполне понятна. Пришедший асинхронный вызов инкапсулируется в работу work, далее для нее устанавливается флаг асинхронности, позволяющий различать синхронные и асинхронные работы, и, наконец, данная работа направляется на обработку в свойство синхронизации _property.

То, как метод HandleWorkRequest обрабатывает синхронные запросы, уже было рассмотрено ранее. Теперь изучим ветви его кода, ответственные за обработку асинхронных запросов.


Как свойство синхронизации обрабатывает инкапсулированный асинхронный вызов, полученный от перехватчика

Ниже приведена часть кода HandleWorkRequest, которая относится к обработке именно асинхронных вызовов:

internal virtual void HandieWorkRequest(Workltem work) {

bool bQueued;

if (!IsNestedCall(work._reqMsg)) {

if (work.IsAsync()) {

bQueued = true;

lock (workltemQueue) {

work.SetWaiting();

_workltemQueue.Enqueue(work);

if ((!_locked) & &

(_workItemQueue.Count == 1)) {


work.SetSignaled();

_locked = true;

_asyncWorkEvent.Set();

}

}

}

else {

…..

}

}

else {

work.SetSignaled();

work.Execute();

}

}


Итак, если вызов не вложенный, происходит следующее. Блокируется очередь работ (текущий поток входит в критическую секцию)

lock (workltemQueue) {

……

}

и текущая работа помечается как ожидающая в очереди work.Setwaiting();

Потом эта работа становится в очередь работ _workltemQueue.Enqueue(work);

и, если домен синхронизации не заблокирован и данная работа в очереди единственна, инициируется ее выполнение. Для этого после установки флага готовности работы к выполнению и блокировки домена событие _asyncWorkEvent переводится в состояние signaled. Это приведет к тому, что свободный рабочий поток из пула потоков начнет выполнять метод DispatcherCallBack, в процессе чего данная работа и будет извлечена из очереди и отправлена на выполнение:

if ((!_locked) &&

(_workItemQueue.Count == 1)) {


work.SetSignaled();

_locked = true;

_asyncWorkEvent.Set();

}


Если же очередь была не пуста, то только-что поставленная в эту очередь работа ждет своей очереди и будет извлечена из нее в свое время.

Если вызов вложенный, то инкапсулирующая его работа выполняется сразу же без постановки в очередь:

internal virtual void HandleWorkRequest(Workitem work) {

bool bQueued;

if (!IsNestedCall(work._reqMsg)) {

……

}

else {

work.SetSignaled();

work.Execute();

}

}


Возникающие при этом проблемы уже обсуждались при рассмотрении синхронного случая.

Теперь рассмотрим ту ветвь кода метода HandleWorkCompletion(), которая связана с обработкой асинхроннных вызовов (в асинхронном случае этот метод будет вызван из DispatcherCallBack, который будет выполняться рабочим потоком, инициированным переводом свойства _asyncWorkEvent в состояние signaled):

internal virtual void HandleWorkCompletion() {

Workltem nextWork = null;

bool bNotify = false;

lock (_workItemQueue) {

if (_workItemQueue.Count >= 1) {

nextWork = (Workltem) _workltemQueue.Peek();

bNotify = true;

nextWork.SetSignaled();

}

else {

_locked = false;

}

}

if (bNotify) {

if (nextWork.IsAsync()) {

_asyncWorkEvent.Set ();

}

else {

……

}

}

}


Критическая секция

lock (workItemQueue) {

……

}

уже была рассмотрена ранее.

Пусть теперь в начале очереди находится асинхронная работа (nextWork). В этом случае событие asyncWorkEvent устанавливается в состояние signaled и на этом вся подготовка к обработке новой работы завершается.


Перехват исходящего вызова

Формирование перехватчика исходящих вызовов

Напомним, что с каждым контекстом может быть связано несколько цепочек перехватчиков. Формирование связанного со свойством синхронизации перехватчика входящих вызовов было рассмотрено в предыдущем разделе. Теперь рассмотрим формирование перехватчика исходящих вызовов.

Класс SynchronizationAttribute реализует интерфейс IContributeClientContextSink.

Благодаря этому факту, при формировании нового контекста синхронизации автоматически вызывается метод GetClientContextSink, объявленный в данном интерфейсе, который и формирует перехватчик исходящих вызовов для данного контекста.

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

Ниже приводится кодметода GetClientContextSink из Rotor:

public virtual IMessageSink GetClientContextSink (

IMessageSink nextSink) {


InitlfNecessary();

SynchronizedClientContextSink propertySink =

new SynchronizedClientContextSink (

this,

nextSink);

return (IMessageSink) propertySink;

}


Этот код аналогичен коду метода GetServerContextSink, в связи с чем комментарии опущены.

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

Класс SynchronizedClientContextSink наследует классу InternalSink и реализует интерфейс IMessageSink. Его основная функциональность определяется двумя методами интерфейса IMessageSink: SyncProcessMessage и AsyncProcessMessage, обрабатывающими соответственно синхронные и асинхронные исходящие вызовы.


Перехват исходящих синхронных вызовов


Случай реентерабельного контекста

Начнем со случая реентерабельного контекста (домена). Вот соответствующая ветвь кода метода SyncProcessMessage:

public virtual IMessage SyncProcessMessage(

IMessage reqMsg) {


IMessage repiyMsg;

if (_property.IsReEntrant) {


_property.HandleThreadExit();

replyMsg = _nextSink.SyncProcessMessage(reqMsg);

_property.HandleThreadReEntry();

}

else {

……

}

return replyMsg;

}


Прежде всего нужно уведомить свойство синхронизации (_property)

_property.HandleThreadExit();

о том, что выполняется вызов за пределы текущего контекста. Это позволит свойству синхронизации инициировать выполнение очередной работы. Рассмотрим код соответствующего метода HandleThreadExit класса SynchronizationAttribute:

internal virtual void HandleThreadExit() {

HandleWorkCompletion();

}

Код для HandleWorkCompletion уже рассматривался. В результате его выполнения будет проверено состояние очереди работ. Если она не пуста, то очередная работа будет помечена флагом готовности к выполнению. В противном случае домен синхронизации будет разблокирован, что просто означает возможность выполнения вновь поступившего синхронного вызова без записи в очередь. Далее в случае наличия готовой к выполнению работы ее выполнение инициируется. В случае асинхронной работы для этого достаточно перевести событие _asyncWorkEvent в состояние signaled, а в случае синхронной — разбудить занятый ее выполнением процесс путем вызова Monitor.Pulse (nextWork), где nextWork — ссылка на готовую к выполнению синхронную работу.

Теперь вызов reqMsg в форме сообщения пересылается следующему перехватчику в цепочке перехватчиков исходящих вызовов для данного контекста. Текущий поток блокируется в ожидании ответа.

replyMsg = _nextSink.SyncProcessMessage(reqMsg);

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

Вся вышеописанная логика запускается следующим вызовом:

_property.HandleThreadReEntry();

Ниже приводится код для метода HandleThreadReEntry класса

SynchronizationAttribute:

internal virtual void HandleThreadReEntry() {

Workltem work = new Workltem(null, null, null);

work.SetDummy();

HandieWorkRequest(work);

}

Здесь создается фиктивная работа, помечается флагом фиктивности и ставится в очередь в процессе выполнения кода HandleWorkRequest.

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

internal virtual void HandleWorkRequest(Workitem work) {

bool bQueued;

if (!IsNestedCall(work._reqMsg)) {

if (work.IsAsync()) {

……

}

else {

lock(work) {

lock(_workltemQueue) {

if ((!_locked) &&

(_workltemQueue.Count == 0)) {

_locked = true;

bQueued = false;

}

else {

bQueued = true;

work.Setwaiting();

_workltemQueue.Enqueue(work);

}

}

if (bQueued == true) {

Monitor.Wait(work);

if (!work.IsDummy()) {

……

}

else {

lock(_workltemQueue) {

_workItemQueue.Dequeue();

}

}

}

else {

if (!work.IsDummy()) {

……

}

}

}

}

}

else {

……

}

}


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

Если домен был заблокирован, то фиктивная работа помечается как стоящая в очереди и записывается в очередь работ. Далее связанный с этой работой поток засыпает

Monitor.Wait(work);

и спит до тех пор, пока фиктивная работа не будет готова к выполнению, продвинувшись в очереди на первое место.

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

if (!work.IsDummy()) }

……

}

else {

lock(_workltemQueue) {

_workItemQueue.Dequeue();

}

}

Иными словами, фиктивная работа просто удаляется из очереди, после чего выполнение метода SyncProcessMessage завершается возвращением результата replyMsg и разбуженный поток продолжает выполнять основной вызов.


Случай нереентерабельного контекста

Теперь рассмотрим ветвь метода SyncProcessMessage класса SynchronizedClientContextSink, относящуюся к случаю нереентерабельного контекста:

public virtual IMessage SyncProcessMessage (

IMessage reqMsg) {

IMessage replyMsg;

if (_property.IsReEntrant) {

……

}

else {

LogicalCallContext cctx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];


String lcid = cctx.RemotingData.LogicalCalllD;


bool bClear = false;

if (lcid == null) {

lcid = Identity.GetNewLogicalCalllD ();

cctx.RemotingData.LogicalCalllD = lcid;

bClear = true;

}


bool bTopLevel=false;

if (_property.SyncCallOutLCID==null) {


_property.SyncCallOutLCID = lcid;

bTopLevel = true;

}


replyMsg = _nextSink.SyncProcessMessage(reqMsg);


if (bTopLevel) {

_property.SyncCallOutLCID = null;


if (bClear) {

LogicalCallContext cctxRet =

(LogicalCallContext)

replyMsg.Properties[Message.CallContextKey];

cctxRet.RemotingData.LogicalCalllD = null;

}

}

}

return replyMsg;

}


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

Прежде всего нужно узнать идентификатор исходящего вызова. Для этого получаем доступ к его контексту вызова

LogicalCallContext cctx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];

и, затем, к самому идентификатору

String lcid = cctx.RemotingData.LogicalCallID;

Может оказаться, что в данный момент исходящий вызов (reqMsg) еще не имеет идентификатора. В таком случае присвоим ему новый еще не использованный идентификатор

bool bClear = false;

if (lcid == null) {

lcid = Identity.GetNewLogicalCalllD();

cctx.RemotingData.LogicalCalllD = lcid;

bClear = true;

}

Здесь статический метод GetNewLogicalCallID класса Identity генерирует новый идентификатор, который и запоминается в контексте вызова.

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

bool bTopLevel=false;

if (_property.SyncCallOutLCID==null) {


_property.SyncCallOutLCID = lcid;

bTopLevel = true;

}

Ну а теперь передаем исходящий вызов следующему перехватчику исходящих вызовов и ждем ответа

replyMsg = _nextSink.SyncProcessMessage(reqMsg);

Ну а теперь (после получения ответа) надо за собой почистить (если мы что-то поменяли в контексте вызова и в свойстве синхронизации):

if (bTopLevel) {

_property.SyncCallOutLCID = null;


if (bClear) {

LogicalCallContext cctxRet =

(LogicalCallContext)

replyMsg.Properties[Message.CallContextKey];

cctxRet.RemotingData.LogicalCalllD = null;

}

}


Здесь мы обнуляем свойство SyncCallOutLCID свойства синхронизации (если мы его только что сами установили) и обнуляем идентификатор для полученного ответа replyMsg (если мы сами задавали этот идентификатор для исходящего вызова).


Перехват исходящих асинхронных вызовов

Для обработки исходящих асинхронных вызовов перехватчик использует следующий код:

public virtual IMessageCtrl AsyncProcessMessage(

IMessage reqMsg,

IMessageSink replySink) {


IMessageCtrl msgCtrl = null;


if (!_property.IsReEntrant) {


LogicalCallContext cctx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];


String lcid = Identity.GetNewLogicalCalllD();

cctx.RemotingData.LogicalCalllD = lcid;


_property.AsyncCallOutLCIDList.Add(lcid);

}


AsyncReplySink mySink =

new AsyncReplySink(replySink, _property);


msgCtrl = _nextSink.AsyncProcessMessage (

reqMsg,

(IMessageSink)mySink);

return msgCtrl;

}


В качестве входных параметров задаются сам вызов reqMsg и перехватчик, на который вызывающая сторона ожидает получения уведомления о завершении процесса выполнения асинхронного вызова.

В случае нереентерабельного контекста исходящему асинхронному вызову назначается новый идентификатор и этот идентификатор сохраняется в списке _asyncLcidList исходящих асинхронных вызовов, поддерживаемому свойством синхронизации (доступ через свойство AsyncCallOutLCIDList). Заметим, что в случае асинхронных вызовов нет нужны сохранять один и тот же идентификатор по всей цепочке вызовов, в связи с чем здесь не проверяется наличие идентификатора, а сразу же назначается новый:

if (!_property.IsReEntrant) {


LogicalCallContext cctx =

(LogicalCallContext)

reqMsg.Properties[Message.CallContextKey];


String lcid = Identity.GetNewLogicalCallID();

cctx.RemotingData.LogicalCalllD = lcid;

_property.AsynCallOutLCIDList.Add(lcid);

}


Зачем вообще сохраняется список идентификаторов исходящих асинхронных вызовов? И почему он обновляется только для нереентерабельного случая?

Единственно, для чего этот список необходим (и именно в случае нереентерабельного контекста) — для определения того, является ли новый входящий вызов вложенным (см. код методов IsNestedCall и HandleWorkRequest). При изложении этого вопроса ранее уже отмечалось наличие некоторых проблем, связанных с определение понятия вложенного вызова и его использования.

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

AsyncReplySink mySink =

new AsyncReplySink(replySink, _property);

Класс AsyncReplySink будет рассмотрен чуть позже.

И вот, наконец, исходящий асинхронный вызов reqMsg передается следующему перехватчику исходящих асинхронных вызовов, а для получения уведомления указывается mySink:

msgCtrl = _nextSink.AsyncProcessMessage (

reqMsg,

(IMessageSink)mySink);

return msgCtrl;

Теперь рассмотрим класс AsyncReplySink:

internal class AsyncReplySink: IMessageSink {

…..

}

В конструкторе в полях _nextSink и _property сохраняются соответственно ссылка на следующий перехватчик (в нашем случае это будет replySink) и ссылка на свойство синхронизации

internal AsyncReplySink(IMessageSink nextsink,

SynchronizationAttribute prop) {


_nextSink = nextSink;

_property = prop;

}

Как и в любом перехватчике, основными методами являются SyncProcessMessage и AsyncProcessMessage.

Вот код для обработки уведомлений, полученных в виде синхронного вызова:

public virtual IMessage SyncProcessMessage (

IMessage reqMsg) {


Workltem work = new WorkItem (

reqMsg,

_nextSink,

null);


_property.HandieWorkRequest(work);

if (!_property.IsReEntrant) {


_property.AsyncCallOutLCIDList.Remove(

((LogicalCallContext)

reqMsg.Properties[Message.

CallContextKey]).

RemotingData.LogicalCallID);

}


return work.ReplyMessage;

}


Мы не можем сразу же послать уведомление на обработку в перехватчик replySink, так как не уверены в том, что он не нарушит логики синхронизации. В связи с этим мы инкапсулируем уведомление в работу

Workltem work = new WorkItem (

reqMsg,

_nextSink,

null);

и обрабатываем его как обычную новую работу, инкапсулирующию синхронный вызов:

_property.HandieWorkRequest(work);

В зависимости от ситуации эта работа будет поставлена в очередь или будет выполняться без задержек, но в любом случае логика синхронизации не будет нарушена. Здесь наш поток блокируется до завершения обработки работы work (включая время простоя в очереди).

По завершении ее обработки удаляем соответствующий идентификатор из списка исходящих асинхронных вызовов (только в случае нереентерабельного контекста) и возвращаем результат:

if (!_property.IsReEntrant) {


_property.AsyncCallOutLCIDList.Remove(

((LogicalCallContext)

reqMsg.Properties[Message.

CallContextKey]).

RemotingData.LogicalCalllD);

}


return work.ReplyMessage;


Завершаем рассмотрением метода AsyncProcessMessage. Здесь все просто. Полагается, что уведомления о завершении асинхронных вызовов не должны посылаться в виде асинхронных вызовов. В связи с этим данный метод просто вызывает исключение NotSupportedExeption:

public virtual IMessageCtrl AsyncProcessMessage (

IMessage reqMsg,

IMessageSink replySink) {


throw new NotSupportedException();

}


Литература

1. Роберт Орфали, Дан Харки, Джери Эдвардс. Основы CORBA. М., 1999.

2. Дейл Роджерсон. Основы COM. Microsoft Corporation. 1997.

3. Эндрю Трельсен. Модель СОМ и применение ATL 3.0. 2001.

4. Guy Eddon, Henry Eddon. Inside COM+ Base Services. Microsoft Press, 1999.

5. Эш Рофейл, Яссер Шохауд. СОМ и СОМ+. Полное руководство., М., 2000.

6. Дональд Бокс. Сущность технологии СОМ. СПб, 2001.

7. Роберт Дж. Оберг. СОМ+. Технология, основы и программирование. Практическое руководство по Windows 2000 DNA. М., 2000.

8. Microsoft.NET Framwork SDK Documentation.

9. David S.Platt. Understanding COM+.Microsoft Press,1999.

10. Дэвид С. Платт. Знакомство с Microsoft.NET. Microsoft Press. Русская редакция. 2001

11. David Chappel. Exploring Kerberos, the Protocol for Distributed Security in Windows 2000. Microsoft System Journal, August 1999.

12. David Chappel. Microsoft Message Queue Is a Fast, Efficient Choice for Your Distributed Applications. Microsoft System Journal, July 1998.

13. COM+ Programmer's Guide. Microsoft Press, 2000.

14. А.Новик. Система поддержки событий в СОМ+. Технология клиент-сервер, 1999'4, www.optim.ru.

15. Tom Armstrong. СОМ+ Events. Visual C++ Developers Journal, July/August, 1999.

16. Jeff Prosise. A Hands-On Look at COM+ Events. Visual C++ Developers Journal, July/August, January/February 2000.

17. Tom Archer. Inside C#. Microsoft Press,2001

18. David Stuts, Ted Neward, Geoff Shilling. Shared Source CLI Essentials. Глава 1 (Introducing the CLI Component Model), Глава 3 (Using Types to Describe Components), Глава 4 (Extracting Types from Assemblies) и глава 7 (Managing Memory within the Execution Engine).

http://www.oreilly.com/catalog/sscliess

19. [SOP1] W.Harrison, H.Ossher. Subject-oriented programing (A critique of pure objects). In Proceedings of the 8th Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA'93), ACM SIGPLAN Notices, Vol.28, no.10, 1993, pp.411-428

20. [SOP2] Hompage of the Subject-Oriented Programming Project, IBM Thomas J.Watson Research Center, Yorktown Heights, New York, http://www.research.ibm.com/sop

21. [CF1] M.Aksit, A.Tripathi. Data Astraction Mechanisms in Sina/ST. In Proceedings of the Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA’88), ACM SIGPLAN Notices. Vol. 23, no. 11, 1988, pp.265-275

22. [CF2] Homepage of the TRESE Project, University of the Twente, The Netherlands, http: //wwwtrese. cs. utwente. nl

23. [API] K.Leiberherr. Component Enhancement: An Adaptive Reusability Mechanism for Groups of Collaborating Classes. In Information Processing'92, 12th World Computer Congress, Madrid, Spain, J. van Leeuwen (Ed.), Elsevier, 1992, pp.179-185

24. [API] Homepage of the Demeter Project.Northeastern University, Boston, Massachusetts, http://www.ccs.neu.edu/research/demeter

25. [AOP1] Gregor Kiezales, Sohn Lamping, Anurag Mendhekar, Chris Maeda, Cristina Videira Lopes, Jean-Marc Loingtier, John Irwin. Aspect-Oriented Programming. In Proceedings of the European Conference on Object-Oriented Programming (ECOOP'97). Finland, Springer Verlag, INCS 1241, June 1997

26. [AOP2] K.Czamecki. Generative Programming: Principles and Techniques of Software Engineering Based on Automated Configuration and Fragment-Based Component Models. PhD thesis, Technische Universitat Ilmenau, Germany, 1998. (Глава Aspect-Oriented Decomposition and Composition)

27. [AOP3] Dharma Shkla, Simon Fell, Chris Sells. Aspect-Oriented Programming Enables Better Code Encapsulation and Reuse. MSDN Magazine, March 2002.

28. http://www.msdn.microsoft.com

29. http://www.dotsite.spb.ru

Загрузка...