Глава 27. Модель COM/DCOM
- Создание удаленного объекта
- Доступ к удаленному объекту
- Обеспечение безопасности доступа к удаленному объекту
Основой как ActiveX, так и OLE является модель многокомпонентных объектов (COM).
Модель C OM
СОМ устанавливает абстракции и правила, необходимые для определения объектов и интерфейсов; в ее состав входит также программное обеспечение, реализующее ключевые функции. Хотя СОМ не является ни большой, ни слишком сложной, она определенно отличается как от других архитектур предоставления программных сервисов, так и от традиционных объектно-ориентированных подходов. Поэтому разработчикам придется взглянуть по-новому на, казалось бы, знакомые вещи.
Описание объ ектов СОМ
Как уже говорилось, программы, созданные с использованием СОМ, предоставляет свои сервисы через один или несколько СОМ-объектов. Каждый такой объект является экземпляром некоторого класса и поддерживает определенное количество интерфейсов, обычно не менее двух. В состав каждого интерфейса входит один или более методов — функций, которые могут вызываться клиентом объекта. Например, один из гипотетических объектов, обсуждавшихся в предыдущей лекции, поддерживает как корректировщик орфографии, так и словарь синонимов, предоставляя доступ к своим сервисам через два разных интерфейса. Интерфейс корректировщика орфографии содержит LookUpWord, AddToDictionary и RemoveFromDictionary, тогда как интерфейс словаря синонимов состоит лишь из одного метода ReturnSynonym. Чтобы вызывать любой из этих методов, у клиента объекта должен быть указатель на интерфейс, содержащий соответствующий метод.
Интерфейсы
Каждый поддерживаемый объектом интерфейс, по сути дела, — контракт между этим объектом и его клиентами. Они обязуются: объект — поддерживать методы интерфейса в точном соответствии с определениями последнего, а клиент — корректно вызывать методы. Чтобы контракт был работоспособным, объект и его клиенты должны договориться о способе явной идентификации каждого интерфейса, об общем способе спецификации — или описания — методов интерфейса, а также о конкретной реализации интерфейса.
Идентификация интерфейса
У каждого интерфейса СОМ два имени. Одно из них предназначено для людей — строка символов. Другое имя сложнее — оно предназначено для использования в основном программным обеспечением. Легко воспринимаемое человеком символьное имя не является гарантированно уникальным — допускается (хотя это и не распространенная практика), чтобы это имя было одинаковым у двух интерфейсов. Имя же, используемое программами, уникально — это позволяет точно идентифицировать интерфейс.
По соглашению читабельные имена большинства СОМ-интерфейсов начинаются с буквы I (от interface). Различные технологии, основанные на СОМ, определяют интерфейсы с разными именами, но все они обычно начинаются с буквы I и пытаются хотя бы немного описать назначение интерфейса. Например, интерфейс корректировщика орфографии, описанный выше, мог бы называться ISpellChecker, а интерфейс словаря синонимов — IThesaurus.
Простые дружественные имена, вроде приведенных выше, удобны при упоминании интерфейса в разговоре или при выборе имен переменных и типов для указателей интерфейсов. Однако они не годятся, когда клиент должен точно указать, какой именно интерфейс объекта ему нужен . Например, если две группы независимо разрабатывают два разных интерфейса, может случиться, что для обоих интерфейсов будет выбрано имя ISpellChecker. Клиент, знающий только об одном из этих интерфейсов и запрашивающий у некоторого объекта указатель ISpellChecker, может получить неверный указатель, если объектом реализован в действительности другой интерфейс. А если объекту нужно реализовать оба интерфейса, его клиенты окажутся в затруднительном положении, так как не будут в точности знать, указатель какого интерфейса они получат, запрашивая ISpellChecker.
Выход прост: создатель любого интерфейса должен присвоить ему уникальное имя — глобально уникальный идентификатор
(globally unique identifier — GUID ). GUID интерфейса называется идентификатором интерфейса (interface identifier — IID). GUID — это 16-байтовая величина, обычно генерируемая программой-утилитой. Каждый может запустить такую утилиту на любом компьютере и гарантированно (со всех практических точек зрения) получит GUID, который будет отличаться от всех остальных.Проблема сводится к тому, чтобы гарантировать уникальность каждого GUID во времени и пространстве. Уникальность во времени достичигается за счет того, что каждый GUID содержит метку времени, указывающую, когда он был создан, что гарантирует отличие друг от друга всех GUID, сгенерированных на данной машине. Для обеспечения уникальности в пространстве у каждого компьютера, который может быть использован для генерации GUID, должен быть уникальный идентификатор. В качестве такого идентификатора программа генерации GUID использует уникальное значение, уже имеющееся на большинстве компьютеров: адрес платы сетевого интерфейса. Если в компьютере не установлен сетевой адаптер, то из различных случайных характеристик данной системы генерируется фиктивный идентификатор машины. Но и тогда маловероятно, что идентификаторы двух машин
окажутся одинаковыми.Хотя человеку и трудно работать с GUID, последние отлично подходят для назначения гарантированно уникальных имен интерфейсов — тех, что используются программами. Люди обычно выбирают для указания интерфейсов простые читабельные имена, а не GUID. Не стоит забывайть, однако, что у каждого интерфейса фактически два имени — читабельное и IID (который, конечно, по сути GUID) и что скомпилированное и работающее программное обеспечение практически всегда пользуется последним.
Спецификация интерфейса
Объект и клиент должны иметь заранее согласованный способ описания интерфейса, т.е. способ определения методов, из которых состоит интерфейс, а также параметров этих методов. СОМ не предписывает, как это должно быть сделано . СОМ-объект может описывать свои интерфейсы с помощью чистого C++, или некоторого псевдо-С++, или каким-то еще способом, который может быть согласован между создателем объекта и создателями его клиентов. Важно другое: СОМ-объект обязан точно следовать стандарту двоичного интерфейса СОМ (о нем будет рассказано далее).
И все же для определения интерфейсов удобно иметь стандартный инструмент. В СОМ такой инструмент есть — язык описания интерфейсов (Interface Definition Language — IDL). IDL СОМ является в значительной степени расширением IDL Microsoft RPC, а тот в свою очередь заимствован из IDL OSF DCE (Open Software Foundation Distributed Computing Environ- j ment). С помощью IDL можно составить полную и точную спецификацию интерфейсов объекта СОМ. Например, ниже приведена спецификация на IDL для гипотетического интерфейса корректировщика орфографии ISpellChecker:
[ object, uuid(E7CDODOO-1827-11CF-9946-444553540000) ] interface ISpellChecker : Unknown { import "unknwn.idi" HRESULT LookUpWord([in ] OLECHAR word[31], [out] boolean * found); HRESULT AddToDictionary([in] OLECHAR word[31]): HRESULT RemoveFromOictionary([in] OLECHAR word[31]); }
Как можно заметить, IDL очень похож на C++. Спецификация интерфейса начинается со слова object, указывающего, что будут использоваться расширения, добавленные СОМ к оригинальному IDL DCE. Далее следует IID интерфейса — некоторый GUID. В DCE, откуда была заимствована эта идея, GUID называется универсально уникальным идентификатором (universal unique identifier — UUID). Так как в основе IDL СОМ лежит IDL DCE, то в описании интерфейса используется термин UUID. Иначе говоря: UUID — всего лишь другое имя для GUID.
Далее идет имя интерфейса — ISpellChecker, за ним — двоеточие и имя другого интерфейса — IUnknown. Такая запись указывает, что ISpel
lChecker наследует все методы, определенные для IUnknown, т.е. клиент, у которого есть указатель на ISpellChecker, может также вызывать и методы IUnknown. IUnknown, как будет рассказано далее, является критически важным интерфейсом для СОМ, и все остальные интерфейсы наследуют от него. (Как было объяснено в предыдущей лекции, СОМ поддерживает только наследование интерфейса, но не наследование реализации. Хотя объект СОМ волен при определении своего интерфейса наследовать от любого из существующих интерфейсов, этот новый объект унаследует только само определение, но не реализацию существующего интерфейса. На практике наследование интерфейсов используется в СОМ нечасто. Вместо этого объект обычно поддерживает каждый необходимый ему интерфейс по отдельности (кроме lUnknown, от которого наследуют все интерфейсы). В отличие от C++ СОМ поддерживает лишь одиночное наследование, позволяя интерфейсу наследовать только от одного предка. Множественное наследование, т.е. наследование от нескольких интерфейсов одновременно, не поддерживается. Программисты на C++ могут свободно применять множественное наследование C++ для реализации объектов СОМ, однако оно не может быть использовано для спецификации интерфейсов на IDL.)Далее в спецификации интерфейса идет оператор import. Так как данный интерфейс наследует от IUnknown, то некоторой программе, читающей определение интерфейса, может потребоваться найти описание IDL для IUnknown. Оператор import указывает такой программе, какой файл содержит нужное описание.
Вслед за оператором import в описании интерфейса идут три метода: LookUpWord, AddToDictionary и RemoveFromDictio-nary, — а также их параметры. Все три возвращают HRESULT — стандартное возвращаемое значение , указывающее, был ли вызов обработан успешно. Параметры в IDL могут быть сколь угодно сложными, использовать такие типы, как структуры и массивы, являющиеся производными своих аналогов в C++. Каждый параметр помечен [in] или [out]. Значения параметров [in] передаются при вызове метода от клиента объекту, тогда как значения [out] передаются в обратном направлении. (Параметры, значения которых передаются в обоих направлениях, могут быть помечены как [in, out].) Подобные метки могут помочь читателю лучше понять интерфейс, однако их основное назначение в том, чтобы программа, обрабатывающая спецификацию интерфейса, точно определила, какие данные и в каком направлении копировать.
Подобная простая спецификация — все, что необходимо для заключения контракта между объектом СОМ и его клиентом. Хотя интерфейс и не обязан задаваться именно таким способом, но следование ему может значительно облегчить жизнь разработчика. Кроме того, неплохо иметь одну стандартную схему для спецификации интерфейсов объектов СОМ.
Обратите внимание на одно важное обстоятельство, связанное со спецификациями интерфейса: после того как интерфейс опубликован и начал где-то работать, после того как его задействовали в выпущенной версии какого-либо программного обеспечения, изменять его, по правилам СОМ, нельзя . Если создатель интерфейса хочет добавить новый метод, изменить список параметров метода или внести какие-либо другие изменения, это также недопустимо. Интерфейсы должны быть фиксированными.
Добавление новой или изменение существующей функциональности требует определения полностью нового интерфейса. Такой интерфейс может наследовать от старого, но тем не менее является совершенно отличным от него и имеет новый и другой IID. Создатель программного обеспечения, поддерживающего новый интерфейс, может продолжать поддерживать и старый, но это не является требованием. Создатель программы имеет право прекратить поддержку старого интерфейса (хотя обычно это плохая идея), но ему категорически запрещено изменять этот интерфейс .
Реализация интерфейса
Чтобы вызвать метод, клиенту необходимо точно и подробно знать, как это делать. Спецификация интерфейса, подобная приведенной выше, описывает лишь одну важную часть процесса. Но СОМ также определяет и другое: она задает стандартный двоичный формат, который каждый СОМ-объект должен поддерживать для каждого интерфейса. Наличие стандартного двоичного формата означает, что любой клиент может вызывать методы любого объекта независимо от языков программирования, на которых написаны клиент и объект.
Клиентский указатель интерфейса фактически является указателем на указатель внутри объекта. Последний в свою очередь указывает на таблицу, содержащую другие указатели. Эта виртуальная таблица (viable) содержит указатели на все методы интерфейса.
Следует заметить: методы ISpellChecker изображены на рисунке как элементы 4, 5 и 6 виртуальной таблицы. Что представляют собой первые три метода? Это методы, определенные интерфейсом IUnknown. Поскольку ISpellChecker наследует от IUnknown, у клиента должна быть возможность вызова методов lUnknown через указатель на ISpellChecker. Чтобы это стало возможным, виртуальная таблица ISpellChecker должна содержать указатели на эти три метода. Фактически, так как каждый интерфейс наследует от IUnknown, виртуальная таблица любого СОМ-ин-терфейса начинается с указателей на три метода IUnknown. Подобная двоичная структура имеет место для всех интерфейсов, поддерживаемых любым объектом.
Формат интерфейса СОМ моделирует структуру данных, генерируемую компилятором C++ для класса этого языка (класс задает тип объектов, а двоичная структура генерируется для объектов класса.). Это сходство означает, что СОМ-объекты очень легко создавать на C++. Хотя СОМ-объекты можно писать на любом языке, поддерживающем описанные стандартные двоичные структуры, в СОМ, честно говоря, имеется некоторый уклон в сторону реализации на C++ (что не должно удивлять, если учесть популярность C++).
При вызове клиентом метода интерфейса выполняется проход по описанной структуре (с помощью указателя на виртуальную таблицу извлекается указатель на метод, который в свою очередь извлекает код, фактически предоставляющий сервис) и исполняется соответствующий код. Если клиент написан на C++, этот проход невидим для программиста, поскольку C++ и так делает это автоматически. Вызов методов СОМ из программы на С несколько сложнее. Тот, кто пишет клиент на С, должен знать, что необходимо пройти по цепочке указателей, и кодировать вызов соответствующим образом. Результат в любом случае один и тот же: исполняется метод в объекте.
IUnknown — фундаментальный интерфейс
Каждый объект СОМ должен поддерживать интерфейс IUnknown — в противном случае он не будет объектом СОМ. IUnknown содержит только три метода: QueryInterface, AddRef и Release. Так как все интерфейсы наследуют от IUnknown, его методы могут быть вызваны через любой из указателей на интерфейсы объекта. Тем не менее IUnknown является отдельным самостоятельным интерфейсом с собственным IID, так что клиент может запросить указатель непосредственно на IUnknown. На диаграммах lUnknown обычно изображается над объектом.
Назначение IUnknown::QueryInterface
Обычно свой первый указатель на интерфейс объекта клиент получает при создании объекта (см. ниже "Создание объектов СОМ"). Имея первый указатель, клиент может получить указатели на другие интерфейсы объекта, методы которых ему необходимо вызывать. Для этого клиент просто запрашивает у объекта эти указатели с помощью IUnknown::QueryInterface .
Чтобы воспользоваться QueryInterface, клиент вызывает его с помощью любого из имеющихся у него в данный момент указателей на интерфейсы объекта. Клиент передает IID нужного ему интерфейса как параметр метода. Например, пусть у клиента уже имеется указатель на интерфейс А, и требуется пс лучить указатель на интерфейс В. Клиент запрашивает данный указатель вызовом QueryInterface через указатель А, задавая в качестве параметра IID интерфейса В (шаг 1). Если объект поддерживает В, то
он возвращает указатель на этот интерфейс (шаг 2), и клиент может теперь может вызывать методы В (шаг 3). Если же объект не поддерживает В, он возвращает NULL.Вообще говоря, самый важный элемент СОМ — QueryInterface. Именно эта простая схема решает очень важную и сложную проблему — контроль версий . Вообразите себе мир, в котором создание программ из СОМ-объектов — обычное дело. Объекты, составляющие такие приложения, создаются множеством организаций, каждая из которых модернизирует свои объекты независимо от остальных. Как все это будет работать, если новые возможности добавляются в разные объекты в разное время? Как установить новую версию объекта с расширенными возможностями, не повредив программам, использующим только старые возможности? И как после модернизации клиента под новые возможности обеспечить автоматическое начало их использования этим клиентом? Ответ на все эти вопросы дает QueryInterface.
Лучше всего продемонстрировать это на примере. Допустим, имеется некий набор инструментов обработки текста, реализованный в виде СОМ-объекта, поддерживающего интерфейс ISpellChecker. Если установить такой объект на компьютер, текстовый процессор (и другие клиенты) сможет его использовать. Чтобы получить доступ к сервисам объекта, текстовый процессор запрашивает указатель на ISpellChecker через QueryInterface. Так как объект поддерживает этот интерфейс, то возвращает соответствующий указатель, и текстовый процессор вызывает методы ISpellChecker. Все работает замечательно.
Теперь допустим, что фирма, продающая этот объект — инструментарий для обработки текста, — решила добавить поддержку словаря синонимов, доступ к которой можно получить че-рез интерфейс IThesaurus. Таким образом, следующая версия объекта поддерживает как ISpellChecker, так и IThesaurus. После того, как установить на машине эту новую версию, все будет работать так же, как и раньше. Текстовый процессор, как обычно, запрашивает указатель на ISpellChecker и успешно пользуется его методами (вспомним, СОМ запрещает изменение интерфейсов.) То, что объект теперь поддерживает еще и IThesaurus, совершенно неизвестно "ограниченному" текстовому процессору, так как он не поддерживает работы со словарем синонимов. Следовательно, старый текстовый процессор никогда не запросит у объекта указатель на этот интерфейс
.Предположим теперь, что на машине установлена новая версия текстового процессора, поддерживающая работу со словарем синонимов. Когда в следующий раз пользователь вызовет старый текстовый процессор, он, как обычно, запустит объект — инструментарий для обработки текста и запросит указатель на интерфейс ISpellChecker. Однако новая версия текстового процессора обладает информацией, достаточной для того, чтобы запросить указатель на IThesaurus. Так как версия объекта, которая поддерживает данный интерфейс, была установлена ранее, нужный указатель будет возвращен, и текстовый процессор сможет воспользоваться новой функцией. Итак, в итоге установлена новая версия инструментария для обработки текста, не нарушающая при этом работы существующих его клиентов, а также обеспечено автоматическое использование этими клиентами функций новой версии, после того как сами клиенты были обновлены!
Ну а как быть тем, кто установил новую версию текстового процессора, но еще не приобрел новую версию инструментария для обработки текста? Все также замечательно работает за исключением того, что текстовый процессор не предоставляет таким пользователям возможностей словаря синонимов. Текстовый процессор запускает объект-инструментарий и через QueryInterface успешно получает указатель на ISpellChecker. Однако, запрашивая указатель на IThesaurus, он получает в ответ NULL. Если текстовый процессор написан с учетом подобной возможности, он отключает пункт меню Thesaurus. Поскольку объект, реализующий IThesaurus, отсутствует, постольку у пользователя не будет доступа к функциям словаря синонимов. Как только пользователь потратится на модернизированный объект — инструментарий для обработки текста, этот пункт меню будет активизирован без каких-либо изменений в текстовом процессоре.
Рассмотрим еще один пример. Что, если создатель объекта — инструментария для обработки текста — пожелает изменить или расширить функциональные возможности объекта по корректировке орфографии? Это влечет за собой изменение или добавление новых методов, которые будут видимы клиенту объекта. Однако СОМ не разрешает изменять интерфейсы, поэтому существующий интерфейс ISpellChecker трогать нельзя. Вместо этого создатель объекта должен определить новый интерфейс, скажем, ISpellChecker2, и включить в него необходимые новые или измененные методы. Объект по-прежнему поддерживает ISpellChecker, но теперь он также будет поддерживать и ISpellChecker2. Добавление в объект поддержки ISpellChecker2 ничем не отличается от добавления поддержки любого нового интерфейса. Как и все СОМ-интерфей-сы, новый имеет уникальный IID, который клиент, знающий о новом интерфейсе, может использовать для запроса указателя через QueryInterface. Как и в предыдущем случае с IThesaurus, клиенты, ничего не знающие о происшедшей модернизации, никогда не запросят указатель на ISpellChecker2, и не ощутят никакого воздействия со стороны изменений — они будут продолжать использовать ISpellChecker, как прежде.
Querylnterface и требование неизменности интерфейсов СОМ позволяют программным компонентам, разрабатываемым независимыми организациями, обновляться по отдельности и тем не менее продолжать нормальную совместную работу . Это соображение трудно считать несущественным, и именно поэтому создатели СОМ иногда называют QueryInterface важнейшим элементом модели.
Подсчет ссылок
Чтобы воспользоваться объектом СОМ, клиент должен явно инициировать начало работы экземпляра этого объекта (как описано ниже в разделе "Создание объектов СОМ"). Здесь возникает естественный вопрос: "Когда завершается работа объекта?" Кажется, очевидное решение — возложить на клиента, запустивший объект на выполнение, еще и обязанность сообщить объекту, когда тот должен остановиться. Однако данное решение не работает, так как данный клиент может со временем оказаться не единственным, кто этот объект использует . Весьма распространена практика, когда клиент запускает выполнение объекта, получает указатели на его интерфейсы и затем передает один из них другому клиенту. Последний может использовать указатель для исполнения методов в том же самом объекте, а также в свою очередь передать указатель другим клиентам. Если бы первый клиент мог "убивать" экземпляр объекта по своему желанию, то положение остальных клиентов было бы незавидным — исчезновение объекта в тот момент, когда его сервисы используются, в лучшем случае огорчительно.
В то время как один объект может использоваться несколькими клиентами одновременно, никто из них не в состоянии узнать, когда все остальные завершатся. Так что разрешить клиенту убивать объект напрямую — небезопасно. Только сам объект может знать, когда он может безопасно завершить свою работу, и только в том случае, если все клиенты сообщают объекту, что они завершили работу с ним. Такой контроль объекты осуществляют с помощью механизма подсчета ссылок (reference counting), поддерживаемого двумя методами интерфейса IDnknown.
Каждый исполняющийся объект поддерживает счетчик ссылок . Всякий раз, выдав вовне указатель на один из своих интерфейсов, объект увеличивает счетчик ссылок на 1. (Вообще объект может поддерживать отдельные счетчики ссылок для каждого интерфейса.) Если один клиент передает указатель интерфейса другому клиенту, т.е. увеличивает число пользователей объекта без ведома последнего, то клиент, получающий указатель, должен вызвать с помощью этого указателя AddRef. (Для простоты обычно в данном случае говорят "вызвать AddRef для указателя".) В результате объект увеличивает свой счетчик ссылок. Независимо от того, как он получил указатель на интерфейс, клиент всегда обязан вызвать для этого указатель Release, закончив с ним
работу. Исполнение этого метода объектом состоит в уменьшении числа ссылок на 1. Обычно объект уничтожает сам себя, когда счетчик ссылок становится равным 0.Подсчет ссылок может вызывать проблемы. Если не все клиенты следуют правилам, то экземпляр объекта может либо существовать неопределенно долго, либо, что еще хуже, быть преждевременно удаленным. И все-таки подсчет ссылок выглядит единственным работающим способом управления временем жизни объектов в многоликой и динамичной среде, которую позволяет создать СОМ.
Классы
Всякий СОМ-объект является экземпляром некоторого класса, и каждому классу может быть присвоен GUID — идентификатор класса (CLSID) . Клиент может передавать этот CLSID библиотеке СОМ для создания экземпляра класса, как описано ниже в разделе "Создание объектов СОМ". Однако наличие CLSID не обязательно для всех классов — объекты некоторых классов не создаются с помощью библиотеки СОМ, поэтому такие классы не обязаны иметь CLSID. Абсолютно допустимо существование в любой данный момент времени одного, двух или многих активных объектов некоторого класса.
Связь между классом объекта и интерфейсами, которые этот объект поддерживает, не очень прочна. Естественно было бы предположить, что объект данного класса поддерживает определенный набор интерфейсов и что добавление к объекту нового интерфейса изменяет его класс. Однако это не обязательно — добавление новых интерфейсов к объекту без изменения его класса не запрещается СОМ. Вместо этого основным назначением CLSID является идентификация некоторого фрагмента кода для библиотеки СОМ, чтобы можно было загружать и активизировать объекты данного класса. В СОМ класс объектов идентифицирует некую реализацию группы интерфейсов, а не просто саму эту группу . Предположим, что объект — инструментарий для работы с текстом — решили реализовать два разных производителя и оба созданных ими объекта поддерживают как ISpellChecker, так и IThesaurus. Хотя эти объекты и поддерживают одинаковые набор интерфейсов, они относятся к разным классам с разными CLSID, так как их реализации различны.
Серверы объектов СОМ
Каждый объект СОМ реализован внутри некоторого сервера, содержащего код, который реализует методы интерфейсов объекта, а также контролирует данные объекта, пока тот активен. Один сервер может поддерживать (и зачастую поддерживает) более одного объекта некоторого класса и даже поддерживать несколько классов. Рассмотрим три основные типа серверов:
- Сервер "в процессе" (in-process): объекты реализуются в динамически подключаемой библиотеке, и, таким образом, исполняются в том же процессе, что и клиент.
- Локальный сервер (out-process): объекты реализованы в отдельном процессе, исполняющемся на той же машине, что и клиент.
- Удаленный сервер : объекты реализованы в DLL либо в отдельном процессе, которые расположены на удаленном по отношению к клиенту компьютере. Возможность создания таких серверов поддерживает Распределенная СОМ (DCOM).
С точки зрения клиента, объекты, реализованные любой из трех разновидностей серверов, выглядят одинаково; доступ к методам объектов клиент по-прежнему осуществляет через указатели интерфейсов . При необходимости он может проводить различие между разными типами серверов, но это не обязательно. Запуск объекта, получение указателей на его интерфейсы, вызов их методов и освобождение указателей выполняются клиентом одинаково независимо от того, каким сервером реализован объект: "в процессе", локальным или удаленным.
СОМ и многопоточность
В традиционном простом процессе в один и тот же момент времени выполнялось только одно действие. Другими словами, у процесса был только один поток управления (thread of execution). Однако иногда полезно обеспечить выполнение процессом нескольких задач одновременно (или хотя бы чтобы они казались выполняющимися одновременно). С этой целью у процесса может быть более одного потока управления, т.е. он становится многопоточным (multithreaded). Многопоточный процесс может повысить производительность, например, в тех случаях, когда в компьютере установлено несколько процессоров и процесс может назначать потоки на разные процессы. Многопоточность может также пригодиться в распределенной среде, где процесс одной машине выполняет запрос к другой. Вместо того, чтобы пассивно ждать, пока вторая машина отработает запрос, вызывающий процесс может использовать отдельный поток для выполнения полезной работы, пока запрос не будет выполнен. Многопоточность вносит дополнительные сложности и в программирование. Теперь программист должен учитывать возможность возникновения конфликтов внутри процесса, когда, например, два потока пытаются изменять одну переменную. Корректная обработка таких ситуаций требует дополнительных усилий. Библиотеки, используемые многопоточными программами, также должны быть многопоточными, иначе могут возникнуть странные и трудные для локализации ошибки. (Одна из причин сложности локализации таких ошибок в том, что их трудно воспроизвести. Так как детали выполнения потока могут изменяться от одного запуска программы к другому, точные обстоятельства проявления ошибки могут возникать лишь от случая к случаю.)
На некоторых платформах, где СОМ использовалась первоначально, — Microsoft Windows 3.x и Macintosh — вопрос потоков не возникает. Так как ни одна из этих операционных систем не поддерживает потоки, то и опасности, связанные с ними, отсутствуют. Но Microsoft Win
dows NT и Microsoft Windows 95, как и другие платформы, поддерживающие СОМ, допускают создание многопоточных процессов, поэтому для эффективного использования СОМ в таких средах учитывать вопросы, связанные с потоками, необходимо.Первым подходом, применявшимся для обеспечения создания многопоточных объектов СОМ, была модель комнат (apartment model). Основная идея заключается в том, что хотя процесс и может быть многопоточным, отдельные объекты СОМ — нет . Каждый поток выступает как "комната", и каждый объект СОМ живет только в одной такой комнате (т.е. одном потоке). Методы объекта могут вызываться только этим потоком — вызовы из других потоков помещаются в очередь и затем последовательно обрабатываются потоком, в котором "живет" объект.
Модель комнат, несомненно, полезна, но не лишена недостатков. Возможность наличия в процессе нескольких потоков удобна, но еще большие выгоды можно получить, обеспечив параллельный доступ к любому объекту СОМ из многих потоков. Соответствующая поддержка — свободные потоки (fr
ee threading), или просто многопоточность — появилась с выходом в 1996 году Windows NT версии 4.0. При использовании свободных потоков внутри данного СОМ-объекта может выполняться несколько потоков одновременно. Программист, пишущий код для такого объекта, должен позаботиться и о многопоточной безопасности, но если это сделано, СОМ более не ограничивает выполнение методов объекта единственным потоком.Создание объектов СОМ
До этого места в изложении материала предполагалось, что клиент уже как-то получил первый указатель на один из интерфейсов исполняющегося объекта. Конкретный способ получения клиентом этого указателя не обсуждался. В действительности способов несколько. Например, указатель может быть передан другим клиентом, либо клиент может получить его от моникера. Тем не менее для каждого объекта имеется некоторой клиент, создающий его и получающий самый первый указатель интерфейса . В конечном счете данный процесс основывается на использовании функций библиотеки СОМ.
Библиотека СОМ
В любой системе, поддерживающей СОМ, обязательно имеется некоторая реализация библиотеки СОМ. Эта библиотека содержит функции, предоставляющие базовые сервисы объектам и их клиентам. Но гораздо важнее то, что библиотека предоставляет клиентам способ запуска серверов объектов. Доступ к сервисам библиотеки СОМ осуществляется через вызовы обычных функций, а не методов интерфейсов СОМ-объектов. Обычно имена функций библиотеки СОМ начинаются с "Со" — например, CoCreateInstance.
Поиск серверов
Запрашивая создание объекта, клиент передает библиотеке СОМ идентификатор класса данного объекта, используя который, библиотека должна найти сервер этого класса. Здесь не обойтись без некоего системного реестра — таблицы, отображающей CLSID в местоположение исполняемого кода сервера . Классы всех объектов, которые будут создаваться на данной машине посредством библиотеки СОМ, должны быть зарегистрированы в такой базе данных.
СОМ реализована на разных системах, и точный формат системного реестра может быть разным. Microsoft Windows и Microsoft Windows NT используют стандартную системную таблицу — она так и называется: Реестр (Registry). Другие реализации СОМ могут использовать другие схемы, которые, однако, должны включать:
- CLSID, выступающий как ключ для поиска записи;
- указание типа доступного сервера ("в процессе", локальный или удаленный);
- для серверов "в процессе" и локальных (т. е. для выполняемых на той же машине, что и клиент) должно быть задано полное имя DLL или исполняемого файла сервера; для удаленных серверов (доступных через DCOM) — указание, где искать исполняемый файл сервера.
Обычно приложение добавляет записи к этой таблице при установке. После этого объекты приложения могут создаваться и использоваться клиентами.
Классы и экземпляры
Прежде чем перейти к рассмотрению процесса создания объекта библиотекой СОМ, стоит поразмышлять над тем, что это такое. Создать объект — значит, начать исполнение экземпляра класса данного объекта. По крайней мере в случае первого экземпляра класса это подразумевает запуск сервера данного класса. Фактически основная задача библиотеки СОМ в том, чтобы запустить именно сервер, а не сам объект.
Так как библиотеке СОМ известен только CLSID, она в состоянии запустить лишь некий абстрактный экземпляр объекта. CLSID достаточен для поиска кода методов объекта, но не для поиска его данных . Как создать не просто некий объект данного класса, но конкретный экземпляр, содержащий данные объекта, — т.е. инициализированный объект?
Одной библиотеки СОМ для этого мало. СОМ требует, чтобы объект инициализировал сам себя по указанию клиента — это отдельная операция, выполняемая после того, как объект запущен (см. раздел "Инициализация объектов СОМ"). Таким образом, в чистом виде СОМ требует двухэтапного процесса загрузки и инициализации объекта. (Тем не менее есть способ сразу указать и класс, и данные. Эта технология — моникеры. Моникер в состоянии скрыть от клиента все детали, являясь единым указателем и методов, и данных некоторого экземпляра объекта.)
Создание одного объекта
Рассмотрим самый простой способ создания одного неинициализированного экземпляра объекта. Вначале клиент вызывает функцию библиотеки СОМ CoCreateInstance. Кроме других параметров, данный вызов задает CLSID объекта, который должен быть создан, а также IID некоторого интерфейса, поддерживаемого объектом. Далее библиотека СОМ по CLSID находит в системном реестре запись, соответствующую классу данного объекта. (Точнее, библиотека СОМ предоставляет выполнение этой задаче Диспетчеру управления сервисами, или SCM – Service Control Manager.) Эта запись содержит информацию о местоположении сервера, способного создать экземпляр класса объекта. После того как сервер найден, SCM запускает его.
Вместе с CLSID и IID первого интерфейса, указатель которого необходим клиенту, параметры CoCreateInstance позволяют также клиенту указать, какой тип сервера должен быть запущен библиотекой СОМ — например, "в процессе" или локальный. Клиент имеет право сказать, что тип сервера ему безразличен, либо задать любую комбинацию допустимых типов серверов.
Запущенный сервер создает экземпляр класса объекта и возвращает указатель на запрошенный интерфейс библиотеке СОМ. Последняя в свою очередь передает данный указатель клиенту, который затем может выполнять вызовы методов этого интерфейса. Так как результатом данного процесса является создание неинициализированного объекта, то клиент обычно запрашивает интерфейс, через который объект может быть инициализирован, хотя это и не обязательно.
До этого места подразумевалось, что создаваемый объект реализован в сервере "в процессе" или локальном, т.е. будет выполняться на той же машине, что и клиент. Что же происходит в случае удаленного сервера? Как создается удаленный экземпляр объекта?
Поддержка удаленных объектов обеспечивается DCOM. Процесс во многом аналогичен созданию локального объекта: клиент выполняет тот же вызов библиотеки СОМ, SCM просматривает системный реестр и т.д. Если же указан удаленный сервер, СОМ установит для создания экземпляра объекта связь с удаленной машиной. Как и все межмашинные коммуникации в DCOM, данный запрос
выполняется вызовом удаленной процедуры. Просмотрев свой реестр, удаленная система находит исполняемый файл сервера и создает экземпляр объекта. Так же, как и в случае локального сервера, возвращается указатель на интерфейс, после чего клиент может вызывать методы вновь созданного объекта. Для клиента запуск объекта выполняется одинаково независимо от того, каким сервером реализован объект: "в процессе", локальным или удаленным; данное различие должно учитываться клиентом, лишь когда он сам считает это необходимым.Независимо от типа запускаемого сервера СОМ устанавливает правила защиты, определяющие, какими клиентами может быть запущен тот или иной сервер. Кроме того, СОМ задает интерфейсы и функции библиотеки СОМ для поддержки контроля прав доступа, хотя точная их реализация зависит от операционной системы. Доступ к распределенным сервисам защиты для удаленных серверов определяется DCOM.
Создание нескольких объектов одного класса: фабрики классов
Если клиенту нужен только один объект, то проще всего создать его с помощью CoCreateInstance. И все же случается, что клиенту может понадобиться много экземпляров объектов одного и того же класса. Чтобы их создание выполнялось эффективно, клиент может получить доступ к фабрике класса (class factory) — объекту, способному создавать другие объекты. Каждая фабрика класса знает, как создавать объекты одного конкретного класса (хотя название "class factory" не вполне удачно — ведь эти фабрики создают экземпляры классов, а не классы). Фабрики классов — полноценные объекты СОМ: доступ к ним осуществляется через интерфейсы, они поддерживают IUnknown и т.д . И все же они необычные объекты, так как могут создавать другие объекты СОМ.
Дело в том, что все объекты, с которыми мы встречались до сих пор, созданы фабрикой класса. Даже когда клиент просто вызывает CoCreateInstance, реализация этой функции в библиотеке СОМ создает объект с помощью фабрики класса. CoCreateInstance скрывает эти детали от клиента. Но на самом деле она использует методы интерфейса IClassFactory, описываемые ниже.
Интерфейс IClassFactory
Чтобы называться фабрикой класса, объект должен поддерживать интерфейс IClassFactory . Этот замечательно простой интерфейс содержит только 2 метода:
В некоторых случаях интерфейс IClassFactory слишком прост. На сегодня имеется новый интерфейс IClassFactory2, добавляющий новые возможности. Так как IClassFactory2 наследует от IClassFactory, в его состав входят методы Createlnstance и LockServer, Однако он поддерживает и еще несколько методов, связанных с лицензированием. Используя эти методы, можно разрешить создание новых объектов только лицензированным клиентам — таким, на чьих компьютерах установлена легальная, предположительно оплаченная копия программного обеспечение. Так как данная возможность особенно полезна для управляющих элементов ActiveX.
Использование фабрики класса
Чтобы получить доступ к фабрике класса, клиент вызывает функцию библиотеки СОМ CoGetClassObject . Этой функции передается CLSID класса объектов, которые будут создавать фабрики, а не CLSID самой фабрики. Клиент задает также IID интерфейса, нужного ему для работы с фабрикой. Конечно, обычно это IID интерфейса IClassFactory. Кроме того, как и в случае с CoCreateInstance, клиент может также задать тип сервера, который должен быть запущен для фабрики и ее объектов. Если для фабрики запрашивается, например, сервер "в процессе", то и объекты, созданные фабрикой, тоже будут выполняться данным сервером "в процессе".
Продемонстрируем использование фабрики класса. Допустим, клиент уже вызвал CoGetClassObject, библиотека СОМ запустила фабрику класса и возвратила указатель на интерфейс IClassFactory этой фабрики. Получив указатель, клиент вызывает метод IClassFactory::Createlnstance данного интерфейса (шаг 1). Среди параметров этого вызова клиент передает IID интерфейса, указатель на который ему необходим. В ответ фабрика класса создает объект (шаг 2) и возвращает клиенту указатель на заданный интерфейс (шаг 3). Теперь клиент может использовать возвращенный ему указатель для вызовов методов интерфейса (шаг 4).
Эмуляция
Выше обсуждался метод IUnknown::QueryInterface и его возможности по автоматическому контролю версий. Добавление нового интерфейса к существующему объекту не вызывает проблем у его старых клиентов, так как они никогда не запрашивают указатель на этот интерфейс. Новые же клиенты запрашивают этот указатель, используют сервисы нового интерфейса.
Однако здесь присутствует потенциальная проблема. Предположим, существующий класс заменили другим, поддерживающим все интерфейсы старого, а также и дополнительные. Иначе говоря, новый класс является полиморфным первому: клиент может использовать его так же, как и старый класс. Допустим, однако, что у нового класса другой CLSID. Существующие клиенты написаны так, что создают объекты старого класса, используя старый CLSID. Если последний будет полностью устранен, прежние клиенты не будут работать. Необходимо нечто, позволяющее таким клиентам, не меняясь, использовать новый класс.
Это нечто называется эмуляцией. Идея проста: пусть клиент вызывает CoCreateInstance со старым CLSID, но на самом деле будет создаваться экземпляр нового объекта. Для поддержки этого СОМ предоставляет функцию CoTreatAsClass с двумя параметрами: старым и новым CLSID. После вызова соответствующей функции результатом попыток создания объектов с использованием старого CLSID будет создание объектов с новым CLSID. (Реализация этого вызова обычно осуществляется путем записи отношения эмуляции между двумя CLSID в системный реестр.) Объекты нового класса поддерживают и все интерфейсы старого, поэтому существующие клиенты продолжают работать как прежде.
Данный механизм может применяться и для создания абстрактных компонентов вроде объекта — корректора орфографии, описанного выше. Например, текстовые процессоры могли бы использовать только один CLSID для идентификации объекта — корректора орфографии, поддерживающего стандартизированный интерфейс ISpellChecker. Но так как CLSID в СОМ задает некоторую реализацию интерфейса, то объекты — корректоры орфографии
разных производителей будут иметь разные CLSID, хотя оба поддерживают один и тот же интерфейс. В таком случае можно определить стандартный CLSID, который будет просто обозначать "корректор орфографии". Текстовый процессор будет всегда использовать для создания объекта — корректора орфографии именно этот CLSID. Чтобы на данной системе запускался конкретный корректор, с помощью функции CoTreatAsClass задается отображение CLSID абстрактного корректора орфографии в CLSID выбранного объекта-корректора.Инициализация объектов СОМ
Как уже говорилось, клиент запрашивает создание объекта, задавая его CLSID и IID одного из интерфейсов. В ответ фабрика класса создает некий абстрактный экземпляр данного класса. По сути, данный процесс обеспечивает объектам доступ к методам. Однако у объекта есть не только методы, но и данные. Скажем, когда клиент обращается к банковскому счету, ему обычно нужен конкретный счет. Таким образом, чтобы завершить создание экземпляра объекта, необходимо в общем случае загрузить его данные (вроде остатка на счете), а не только методы.
В СОМ клиент обычно приказывает вновь созданному объекту инициализировать самого себя. Чтобы это стало возможно, данные объекта должны быть перманентно сохранены, т.е. объект должен быть способен сохранить свои данные на время своей неактивности . Одним из очевидных мест хранения перманентных данных объектов является файл на диске.
Первый интерфейс, запрашиваемый клиентом при создании объекта, обычно является одним из тех, что содержат функцию инициализации объекта.
Для этой цели предназначены стандартные интерфейсы IPersistFile, IPersistStorage и IPersistStream. Каждый из них содержит методы, позволяющие клиенту приказать объекту загрузить перманентные данные последнего (т.е. выполнить самоинициализацию). Эти интерфейсы отнюдь не являются единственными, с чьей помощью можно выполнить инициализацию, однако все они применяются очень широко.Повторное применение объектов СОМ
Одна из основных задач СОМ — продвижение повсеместного и эффективного повторного использования существующего кода. Позволяя создавать повторно применимые компоненты с четко определенными интерфейсами, СОМ обеспечивает для этой цели инфраструктуру.
Многие, если не большинство, объектно-ориентированные технологии в качестве основного механизма повторного использования существующего кода применяют наследование реализации (когда новый объект наследует фактическую реализацию методов существующего объекта). Однако создатели СОМ полагают, что такой тип наследования является непрактичным для объектной системы
, предназначенной для крайне неоднородной среды. (Отсутствие в СОМ наследования реализации ни в коей мере не оказывает влияния на использование этой техники в поддерживающих ее языках программирования типа C++. Реализации СОМ-объектов могут использовать наследование реализации как обычно. Не поддерживается лишь наследование реализации от другого СОМ-объекта. Это не противоречие: C++ — язык реализации объектов, тогда как СОМ — технология, позволяющая создавать компонентное программное обеспечение и многое другое.)Несмотря на изоляцию, обеспечиваемую интерфейсами, изменения базовых объектов могут вызвать непредсказуемые эффекты в объектах, наследующих от них реализацию. Это может стать проблемой в мире, где базовые объекты и объекты, наследующие от них, создаются, выпускаются и обновляются независимо. СОМ предоставляет два других механизма повторного применения: включение и агрегирование.
Включение и агрегирование — простые концепции. Обе предоставляют способ повторного применения, и в основе обеих лежит некоторая взаимосвязь объектов. В терминологии СОМ "внешним" (outer) называется объект, повторно использующий сервисы "внутреннего" (inner). Внешний объект выступает просто как клиент внутреннего, либо их взаимосвязь может быть несколько более тесной.
Включение
При включении (containment), которое также называют делегированием (delegation), внешний объект выступает как обычный клиент внутреннего . Внешний объект вызывает методы внутреннего объекта для выполнения своих собственных функций, однако эти методы остаются недоступными клиенту внешнего объекта непосредственно. Вместо этого, когда клиент вызывает метод одного из интерфейсов внешнего объекта, исполнение данного метода может включать в себя вызов некоторого метода какого-либо из интерфейсов внутреннего. Другими словами, интерфейс внешнего объекта содержит методы, вызывающие методы внутреннего.
Реализация включения столь же проста, как и реализация клиента, использующего любой объект СОМ. Внутренний объект не должен быть для этого написан как-то по-особому и фактически даже не в состоянии различить включение и прямое использование его клиентом. (С точки зрения внутреннего объекта, внешний — обычный клиент.) Благодаря своей простоте, включение является очень широко распространенным механизмом повторного применения в СОМ.
Агрегирование
Если включение так легко реализовать, почему бы не использовать эту технику для повторного применения объектов СОМ всегда? Повторное применение объекта всегда может быть реализовано посредством включения — как правило, этого достаточно. Как правило, но не всегда. Точнее, включение — не всегда самое эффективное решение
.Предположим, объекту нужно предоставлять клиентам некоторый интерфейс, и уже имеется другой объект, который этот интерфейс реализует. Первый объект мог бы реализовать все методы данного интерфейса так, чтобы они не делали ничего более, кроме вызова соответствующих методов внутреннего объекта. Это, конечно, заработает, и все же такой подход не слишком эффективен. Если аналогичный процесс повторяется в цепочке из многих объектов, каждый из которых делегирует вызов другому, то вызов метода может оказаться очень неэффективным, поскольку чтобы добраться до его фактической реализации, необходимо пройти сквозь несколько объектов.
Эту проблему устраняет агрегирование (aggregation). Оно позволяет внешнему объекту представлять в качестве собственных интерфейсы, на самом деле реализованные внутренним объектом . Когда клиент запрашивает у внешнего объекта указатель на подобный интерфейс, этот объект возвращает указатель на интерфейс внутреннего, агрегированного объекта. (Методы внутреннего объекта добавляются, или агрегируются, к методам внешнего объекта.) Клиент ничего об этом не знает: возвращенный интерфейс обеспечивается для него только одним
известным ему объектом, а именно внешним. Агрегирование повышает эффективность, но, как и включение, абсолютно невидимо для клиента.Однако агрегирование не невидимо для участвующих в нем объектов. В отличие от включения агрегирование требует поддержки со стороны внутреннего объекта. Для этой цели он должен быть особым образом написан; в противном случае объект можно повторно использовать только путем включения. Что же такого особенного в агрегировании, что требует поддержки со стороны внутреннего объекта? Проблемы вытекают из операций, поддержка которых обязательна для всех объектов — операций, определенных в IUnknown. Два основных вопроса при реализации агрегирования — это обеспечение правильного подсчета ссылок и корректной работы Ou
erylnterface.Чтобы понять причины этих проблем, обратимся снова к рисинку. Внешний объект предоставляет интерфейс А и, конечно, поддерживает IUnknown. Внутренний, агрегируемый объект поддерживает интерфейсы В иIUnknown. Так как внешний объект агрегирует внутренний, а не просто включает его, то интерфейс В доступен клиенту внешнего объекта непосредственно.
Допустим, у клиента есть указатель интерфейса В. С точки зрения клиента, этот интерфейс предоставляется ему тем же объектом, что и интерфейс А. Так что у клиента должна быть возможность получить указатель интерфейса А вызовом Ouerylnterface через указатель на интерфейс В. Но откуда внутренний объект знает, что внешний поддерживает интерфейс А? И если клиент вызывает IUnknown: :AddRef через указатель на интерфейс В, то как об этом узнает внешний объект? В конце концов, с точки зрения клиента, существует лишь один объект, так что каждый из этих вызовов должен быть успешным.
Решение обеих проблем очевидно. Любой внутренний объект должен делегировать вызовы методов своего IUnknown методам IUnknown внешнего объекта (агрегирующего его) . Следовательно, внутреннему объекту нужно как-то передать указатель на интерфейс IUnknown внешнего. Данный указатель, известный под несколько загадочным названием управляющий IUnknown (controlling unknown), передается как параметр либо CoCreateInstance, либо IClassFactory::CreateInstance при создании агрегируемого объекта. Если соответствующий параметр NULL (самый распространенный случай), то объект знает, что он не агрегируется, и будет обрабатывать все вызовы методов IUnknown самостоятельно. Если не NULL, новый объект будет функционировать только как агрегированный внутренний объект некоторого внешнего объекта — того, что передал ему свой управляющий IUnknown. В последнем случае вызовы методов IUnknown внутреннего объекта делегируются методам IUnknown внешнего объекта, т.е. управляющему IUnknown.
Чтобы все здесь работало правильно, требуется преодолеть и другие сложности. Достаточно сказать, что реализация агрегирования требует определенной работы. И все же ни понимание, ни реализация агрегирования не покажутся сложными, если приложить хоть чуточку усилий. И они окупятся сторицей — ведь агрегирование может существенно повысить производительность при повторном применении объектов.
Распределенная COM (DCOM)
С самого начала СОМ разрабатывалась с учетом обеспечения поддержки распределенных сред, т.е. способности клиента создавать объекты на других машинах и вызывать их методы по сети. Эти планы стали реальностью в 1996 году после выпуска распределенной СОМ (Distributed СОМ — DCOM). DCOM позволяет клиенту создавать и использовать объекты как на удаленных системах, так и на локальной. Более того, клиент может даже не осознавать различия между этими двумя случаями. Подобно тому как клиенты СОМ имеют прозрачный доступ к объектам в динамических библиотеках и локальных процессах, DCOM обеспечивает прозрачный доступ к объектам в удаленных процессах. Фактически самое трудное в достижении подобной прозрачности — это обеспечить взаимодействие объектов, исполняющихся в разных процессах независимо от того, выполняются эти процессы на одной машине или нет. В этом смысле, с точки зрения проектирования, DCOM — довольно незначительное расширение оригинальной СОМ.
Возможность запускать удаленные объекты и вызывать их методы — важное достижение, но требуется большее. В частности, нужен способ контроля за тем, кто имеет право создавать объекты на данной машине, и обеспечение безопасного доступа к этим объектам по сети, которая может быть наполнена потенциальными врагами. С этой целью в основу DCOM положен набор сервисов контроля доступа. Приложения (включая программы, созданные до DCOM) могут использовать DCOM и работать вполне безопасно без добавления какого-либо кода, связанного с защитой. С другой стороны, приложения, знающие о новых средствах DCOM контроля доступа, могут задействовать их явно.
Несмотря на отдельные сложные моменты, DCOM вообще проста для понимания. Она добавляет к знакомым основам СОМ всего 3 основных элемента: способ создания удаленного объекта, протокол вызова методов этого объекта и механизмы обеспечения безопасного доступа к нему.
Создание удаленного объекта
Сервисы создания объектов — одни из важнейших сервисов, предоставляемых СОМ. Клиенты обычно создают объекты, вызывая библиотеку СОМ или через моникеры. Эти подходы работают и в DCOM, хотя и с некоторыми новыми особенностями. Рассмотрим различные варианты создания объектов, доступные клиентам.
Использование CoCreatelnstance
Независимо от того, где исполняется объект, клиент обычно создает его и затем получает указатели на необходимые интерфейс. Для большинства описанных ранее объектов — реализованных сервером "в процессе" или локальным сервером — это можно сделать, вызвав CoCreateInstance, а затем с помощью QueryInterface запросить указатели на нужные интерфейсы. Клиент может создать объект на удаленной машине, вызвав ту же самую функцию, т.е. клиенту даже не требуется знать, что объект выполняется на другом компьютере. Чтобы создать удаленный объект, клиент вызывает CoCreateInstance, как обычно, передавая CLSID вместе с IID, указывающим первый интерфейс, указатель на который ему нужен.
Однако для удаленного объекта необходимо задать дополнительный элемент — машину, на которой он должен быть создан. Уже упоминалось, что для объекта, создаваемого на той же машине, системный реестр отображает CLSID в имя DLL или исполняемого файла, который должен быть загружен для данного класса. А при создании объекта на удаленной машине системный реестр может отображать CLS1D в имя машины, на которой этот объект должен создаваться. Для создания удаленного объекта устанавливается связь с удаленной машиной, в ее реестре отыскивается данный CLSID, и на этой удаленной машине запускается соответствующий сервер. Если удаленный объект реализован в DLL, то запускается суррогатный процесс, просто загружающий DLL (данная возможность не поддерживается в первой версии DCOM, но ее планируется реализовать как можно быстрее.). Иначе запускается процесс объекта, как и в случае локального сервера.
Рассмотрим несколько упрощенную картину создания удаленного объекта с помощью CoCreateInstance. Клиент вызывает библиотеку СОМ для создания объекта с CLSID X, запрашивая указатель на интерфейс А этого объекта. Запись в реестре для CLSID X на клиентской машине содержит имя другого компьютера. DCOM предоставляет несколько вариантов идентификации удаленных машин в зависимости от сетевых протоколов, применяемых для доступа к удаленной системе. DCOM поддерживает доменные имена, используемые TCP/IP (типа elvis.acme.corn), а также адреса IP (Internet Protocol), имена NetBIOS и имена, применяемые NetWare IPX/SPX. Независимо от способа идентификации устанавливается связь с удаленной машиной, и там создается объект с учетом информации о CLSID Х реестра удаленной машины. Удаленная машина запустит сервер, а затем попросит его фабрику класса создать объект и вернуть указатель на интерфейс А. Этот указатель далее возвращается клиенту как обычно. Для клиента все выглядит аналогично процессу создания нового объекта локально.
Как уже упоминалось, CoCreateInstance вызывает CoGetClass Object, чтобы получить фабрику данного класса, а затем вызывает метод этой фабрики IClassFactory::CreateInstance для создания объекта на локальной машине (см. раздел "Создание нескольких объектов одного класса: фабрики классов"). Подобный процесс применяется и при создании объекта на удаленной машине, хотя бы с точки зрения программиста. На самом же деле этот процесс был оптимизирован для повышения производительности, и все эти действия выполняются за один цикл взаимодействия "запрос-ответ" с удаленной машиной.
Использование CoCreatelnstanceEx
Применение CoCreateInstance для создания удаленного объекта не всегда наилучший вариант. Сколь велико ни было бы быстродействие сети, доступ к объекту на удаленной машине всегда будет медленнее, чем доступ к объекту на локальной машине. Даже для высокоскоростной сети лучше максимально сократить объем пересылаемых по ней данных. Таким образом, обеспечение производительности, удовлетворяющей пользователей (и администраторов сетей), требует минимизации количества запросов, необходимых для подготовки к использованию удаленного объекта. Важно при этом избежать излишних вызовов QueryInterface для удаленного объекта.
С этой целью DCOM предоставляет функцию CoCreateInstanсеЕх, альтернативную
CoCreateInstance. Как и CoCreateInstance, CoCreateInstanceEx позволяет клиенту задать CLSID класса объекта, который он хочет запустить. Но если СоСrеаteInstance допускает указание только одного IID, задающего первый нужный интерфейс, то CoCreateInstanceEx дает клиенту возможность задать список IID. После запуска объекта CoCreatelnstanceEx запрашивает у него указатели на все интерфейсы из этого списка и возвращает их клиенту одновременно . Вместо того, чтобы заставлять клиент многократно вызывать QueryInterface для получения указателей на интерфейсы объекта, одновременный возврат всех этих указателей может значительно ускорить процесс. И хотя CoCreatelnstanceEx создана для работы с удаленными объектами, нет причин, по которым клиенты не могли бы использовать ее для эффективного создания экземпляров объектов, реализованных локальными серверами и серверами "в процессе".CoCreateInstanceEx также имеет параметр, позволяющий клиенту указать машину, на которой должен быть создан объект . Вместо того, чтобы полагаться в определении удаленной системы на локальный реестр, клиент может динамически выбирать удаленную машину во время создания объекта. Имя машины задается, как и в предыдущем случае, —доменным именем, адресом IP или в другом формате, поддерживаемом сетевыми протоколами. Поскольку CoCreateInstanceEx, как и CoCreateInstance, использует CoGetClassObject для получения указателя на интерфейс соответствующей фабрики класса, постольку имя машины необходимо передать при вызове CoGetClassObject. Для этого используется
зарезервированный ранее параметр, так что необходимость в новой функции CoGetClassObjectEx отпадает.Объединение создания и инициализации
После того, как объект создан клиентом, следующим шагом обычно является инициализация объекта путем выдачи ему команды загрузить перманентные данные . Например, клиент может вызвать метод IPersistFile::Load вновь созданного объекта для загрузки данных из файла. Если объект хранит свои перманентные данные, используя структурированное хранилище, клиент может вызвать IPersistStorage::Load, передав указатель на соответствующее хранилище.
Этот двухэтапный процесс создания и затем инициализации при использовании удаленного объекта не претерпевает никаких изменений — клиент по-прежнему выполняет оба шага и волен делать это традиционным способом, вызвав сначала СоCreateInstance или CoCreateInstanceEx и обратившись затем к соответствующему методу интерфейса IPersist для инициализации объекта. Но если объект выполняется на удаленной машине, то выполнение этих шагов требует серии циклов "запрос-ответ", что может быть неприемлемо медленно. Чтобы улучшить ситуацию, DCOM предоставляет клиентам две альтернативные функции, каждая из которых создает и инициализирует объект за один прием. Если объект выполняется локально, то использование этих функций представляет собой главным образом лишь дополнительное удобство (хотя некоторый выигрыш в производительности достигается и здесь за счет сокращений числа вызовов между процессами), но для удаленных объектов реализация этих функций оптимизирована.
Первая функция — CoGetInstanceFromFile — создает новый объект и инициализирует его данными из файла. Параметры функции включают машину, на которой создается объект, CLSID, имя файла и, подобно CoCreateInstanceEx, список IID нужных клиенту интерфейсов. Если CLSID не задан, функция пытается определить его по имени файла так, как это делает файловый моникер. Использование этой функции аналогично вызову CoCreateInstanceEx и последующему обращению к методу IPersistFile::Load объекта. Вторая функция — CoGetInstanceFromIStorage — работает сходным образом за исключением того, что ей передается указатель на IStorage (он задает соответствующее хранилище), а не имя файла. Вызов второй функции аналогичен вызову CoCreateInstanceEx с последующим обращением к методу IPersistStorage::Load объекта.
Хотя обе функции позволяют указать машину, на которой должен быть создан объект, ни для той, ни для другой эта информация не обязательна. Если имя машины не задано, место создания объекта зависит от нескольких факторов. Когда в локальном реестре данный CLSID связан с именем удаленной машины, как было описано выше, объект создается на указанном компьютере. А если запись для класса в реестре содержит значение ActivateAtStorage, то объект создается на той машине, где расположен заданный файл или хранилище
.Возможно также, что машина, на которой вызывается CoGetInstanceFromFile или CoGetInstanceFromIStorage, вообще не имеет информации о данном классе — локальный реестр может не содержать записи для соответствующего CLSID. Тогда предпринимается попытка создать объект на той машине, где находится указанный файл или хранилище. Если и в реестре той машины нет никакой информации о данном класса, вызов функции завершается с ошибкой. В противном случае объект создается на той же машине, где находится файл или хранилище, как если бы в локальном реестре было значение ActivateAtStorage.
Использование моникера
Чтобы создать удаленный объект, клиент может воспользоваться и моникером — объектом, который знает, как создать и инициализировать один экземпляр другого объекта. Моникеры могут выполнять создание экземпляров и инициализацию объектов как на локальных, так и на удаленных машинах . Для клиента моникера невидимо, исполняется созданный моникером объект локально или удаленно. Место, где выполняется объект, может быть невидимо и самому моникеру.
Когда клиент вызывает для моникера IMoniker::BindToObject, последний обычно вызывает CoCreateInstance с CLSID, полученным из своих перманентных данных. Затем моникер инициализирует вновь созданный объект, используя информацию своих перманентных данных — например, имя файла. Если для CLSID, передаваемого моникером CoCreateInstance, в реестре указана удаленная машина, то объект будет создан на этой машине. При этом сам моникер не узнает, что он создал удаленный объект.
Но моникер может быть в курсе того, что создает объект на удаленном компьютере. При вызове клиентом метода моникера IMoniker::BindToObject есть вероятность, что перманентное хранилище объекта, указываемого моникером, находится на удаленной машине и что в реестре клиентской машины для класса объекта указано ActivateAtStorage. В этом случае моникер создает объект на той машине, где находится перманентное хранилище объекта, подобно CoGetInstanceFromFile и СоGetInstanceFromIStorage.
Файловый моникер содержит имя файла, которое обычно задает и местонахождение перманентных данных объекта (файл) и класс объекта (определяется по расширению имени файла или, возможно, по содержимому файла). Если данный файл хранится на удаленном файл-сервере, а не на локальной машине и если в реестре локальной машины присутствует ActivateAtStorage, моникер создаст объект на файл-сервере, а не на локальном компьютере.
Моникер URL
содержит URL, определяющий местонахождение перманентного хранилища объекта. Если в локальном реестре для класса объекта, идентифицируемого этим моникером URL, задано ActivateAtStorage, то вызов метода моникера IMoniker::BindToObject создаст объект на компьютере, указанном URL, а не на машине, где исполняются моникер и/или его клиент. Затем, моникер приказывает объекту загрузить его перманентные данные по информации, заданной URL. И, подобно CoGetInstanceFromFile и CoGetInstanceFromIStorage, файловый моникер или моникер URL автоматически пытаются создать объект на той же машине, где находится его перманентное хранилище, если в локальном реестре моникером не найдено информации о классе объекта.Доступ к удаленному объекту
Возможность удаленного создания объекта СОМ — первое, что нужно для работы в распределенной среде. С точки зрения клиента, доступ к такому удаленному объекту осуществляется так же, как и к локальному, — все абсолютно одинаково. Однако скрывающаяся за этим реальность гораздо сложнее — ведь для доступа к удаленным методам используются механизмы, совершенно отличные от механизмов работы с локальными объектами.
Объектный RPC
После запуска удаленного объекта и получения указателей его интерфейсов клиент может вызывать методы этих интерфейсов. Вызов метода объекта "в процессе", по сути, означает непосредственное обращение к нему через виртуальную таблицу (а в случае диспинтерфейса и через IDispatch::Invoke). Обращение к методу объекта, реализованного в локальном сервере, требует участия заместителя, заглушки и некоторого механизма коммуникаций между процессами.
Вызов метода объекта, реализованного в удаленном сервере, также использует заместитель и заглушку, но в данном случае клиенту необходимо выполнить вызов удаленной процедуры (RPC) сервера. Протоколов RPC хватает в избытке, и Microsoft решила не создавать новый, но адаптировать существующий. Этот протокол — Microsoft называет его MS RPC — заимствован из OSF DCE (Microsoft, однако, не заимствовала у OSF реализацию протокола; MS RPC представляет собой другую реализацию DCE RPC, созданную на основе общедоступной спецификации). Как сам MS RPC, так и его модификация для DCOM — объектный RPC (Object RPC) — посылают по сети информацию в том же формате, что и DCE RPC (т.е. используют ту же структуру пакета). Хотя ORPC включает ряд новых соглашений по взаимодействию клиента с сервером, добавляет несколько новых типов данных и использует некоторые поля пакета особым образом, сама структура пакетов осталась прежней.
DCE RPC и MS RPC на самом деле включают в себя два разных протокола, которые поддерживаются и ORPC. Один из них — CN или СО — используется поверх протоколов с установлением логических соединений (connection-oriented protocols), таких как TCP (Transmission Control Protocol). Поскольку CN подразумевает, что нижележащий протокол гарантирует надежную доставку данных, то он не проверяет точность передачи . Другой протокол — DG или CL — используется поверх транспорта без установления логического соединения (также называемого протоколом дейтаграмм), такого как UDP (User Datagram Protocol). Рассматривая нижележащий протокол как совершенно ненадежный, DG реализует собственные механизмы, гарантирующие надежную доставку данных. Для выдающего запрос клиента оба протокола выглядят совершенно одинаково, хотя нижележащие транспортные протоколы ведут себя абсолютно по-разному. Эти различия скрываются соответствующим протоколом RPC.
Независимо от используемого протокола клиент должен обладать информацией связывания (binding information) с пунктом назначения, прежде чем он выполнит вызов ORPC. В составе этой информации обычно сетевой адрес удаленной машины (например, адрес IP) и указание, какая комбинация протоколов должна использоваться (например, CL RPC и UDP). Информация связывания может включать точку назначения транспорта (transport endpoint) — ее часто называют портом. — задающую конкретный процесс на удаленной машине. Информацию связывания удобно представлять как строковое связывание (string binding) — символьной строкой, содержащей всю необходимую информацию.
Информацию связывания с данной удаленной системой клиент может получить по-разному. Например, имя машины, передаваемое функции CoCreateInstanceEx, может быть использовано для определения по крайней мере части информации связывания. Кроме того, возможна передача информации связывания (или ссылки на нее) от одного объекта другому. Чтобы понять, как это работает и почему это важно, сначала необходимо разобраться с ролью, которую играют OXID и разрешатели OXID.
OXID и разрешатели OXID
Сервер, реализующий один или несколько объектов СОМ, доступных клиентам на других машинах, можно рассматривать как экспортер объектов (object exporter). Разрешая удаленным клиентам доступ к своим объектам, сервер в некотором смысле "экспортирует" эти объекты, обеспечивая их межмашинное использование. Если сервер — однопоточный процесс с одним или несколькими объектами, то в качестве экспортера выступает сервер в целом. Если же сервер — многопоточный процесс с одной или несколькими комнатами (apartment), содержащими различные объекты, то экспортером является каждая из комнат. В любом случае каждому экспортеру объектов присваивается 8-байтовое значение — идентификатор экспортера объектов (OXID — Object Exporter Identifier) . Заметьте: у одного процесса с несколькими комнатами может быть несколько OXID.
Для доступа к удаленному объекту клиент вначале должен получить его OXID (как это делается, объясняктся в следующем разделе.) Имея OXID, клиент может использовать разрешатель OXID для отображения OXID в информацию связывания с объектом. На любой машине, поддерживающей DCOM, имеется разрешатель OXID (OXID resolver), и каждый разрешатель OXID поддерживает интерфейс lObjectExporter (название этого интерфейса, уходящее своими корнями в историю DCOM, несколько неудачно, так как он не реализуется экспортером объектов). Несмотря на свое СОМ-подобное имя, IObjectExporter не является интерфейсом СОМ. На самом деле это интерфейс RPC, и обращение к нему выполняется через вызовы чистого RPC, а не вызовы ORPC. В его составе 3 "метода": ResolveOXID, SimplePing и ComplexPing.
Каждый разрешатель OXID поддерживает таблицу OXID и их строковых связывании. Как минимум, данная таблица содержит запись для каждого исполняющегося на данной машине объекта, имеющего клиента вне своего процесса или комнаты. Она обычно включает и некоторые OXID и строковые связывания для объектов, исполняющихся на других машинах. Чтобы понять почему, рассмотрим, как осуществляется передача указателей на интерфейсы между машинами.
OBJREF: передача указателей интерфейсов
Как и вызов локального метода, вызов удаленного метода может содержать параметры. Но разные машины иногда используют для представления одних и тех же данных разные форматы. Например, многие системы для представления символов применяют ASCII, тогда как мэйнфреймы IBM применяют для этого код EBCDIC. А разные компьютеры используют разные форматы представления целых и вещественных чисел. Для обеспечения взаимодействия машин, применяющих разные форматы данных, выполняется маршалинг параметров вызова ORPC с использованием сетевого формата NDR (Network Data Representation). NDR — стандартная часть DCE RPC (и, конечно же, MS RPC) — обеспечивает эффективный способ передачи параметров практически любых типов СОМ IDL между машинами, использующими разные представления этих типов данных. Необходимая трансляция параметров из NDR в локальное представление выполняется машиной, принимающей вызов.
Однако один тип параметров, часто передаваемый в вызовах ORPC, не имеет прямой поддержки NDR — это указатели на интерфейсы. Объекты часто передают указатели на интерфейсы своим клиентам, а клиент имеет право передать имеющийся у него указатель на интерфейс любому другому объекту . Когда указатель на интерфейс ссылается на объект в том же процессе, проблем не возникает: указатель передается, как есть. Когда указатель на интерфейс ссылается на объект в другом процессе на той же машине, передается ссылка на данный интерфейс внутри соответствующего процесса. Но когда указатель на интерфейс ссылается на объект, расположенный на другой машине, то, что должно быть передано, является весьма сложной конструкцией — объектной ссылкой (object reference — OBJREF).
Согласно протоколу DCOM в состав OBJREF входят:
После передачи OBJREF от одного объекта другому принимающий объект может вызывать методы с помощью указателя на интерфейс, представляемого данной OBJREF.
Но как объект, получающий указатель на интерфейс, определяет, как связаться с объектом, заданным этим указателем? Точнее, откуда берется информация связывания с объектом, не являющаяся частью самой OBJREF? Есть два варианта. Если любой объект на локальной машине уже связывался недавно с объектом, имеющим тот же OXID, что и объект, на который ссылается указатель, то данный OXID уже есть в таблице локального разрешателя OXID. Если же информация связывания в таблице отсутствует, ее
надо запросить у машины, на которой исполняется объект, заданный данной OBJREF.Чтобы получить необходимую для использования новой OBJREF информацию связывания, DCOM на машине клиента, получившего OBJREF, обращается за помощью к локальному разрешателю OXID, передавая ему эту OBJREF. (Все это, конечно, скрыто от программиста и является частью инфраструктуры, обеспечиваемой DCOM.) Разрешатель OXID выделяет OXID из OBJREF и пытается отыскать его в собственной таблице OXID. Если объект с тем же OXID уже используется кем-то на этой машине, то в таблице будет запись для данного OXID с необходимым строковым связыванием. Предположим, объект А получил OBJREF интерфейса, поддерживаемого объектом X, расположенным на другой машине. Чтобы получить информацию связывания для осуществления вызовов с помощью нового указателя, реципиент передает OBJREF своему локальному разрешателю OXID. Разрешатель OXID может сразу возвратить нужную информацию, если она уже есть в его таблице.
Однако допустим, что OBJREF ссылается на интерфейс удаленного объекта Q, для которого у локального разрешателя OXID информации нет. Тогда разрешатель OXID извлекает из OBJREF информацию связывания с разрешателем OXID на машине, где исполняется объект Q, и с помощью ее связывается с этим разрешателем . Для этого локальный разрешатель OXID вызывает метод ResolveOxid интерфейса lObjectExporter разрешателя OXID на удаленной машине. В качестве параметра этого вызова передается OXID, выделенный из только что полученной OBJREF. Данный вызов возвращает строковое связывание для данного OXID, которое затем добавляется локальным разрешателем в свою таблицу. Вновь полученный указатель на интерфейс теперь можно использовать.
Здесь возникает естественный вопрос: почему в составе OBJREF передается не строковое связывание самого объекта, но связывание разрешателя OXID на машине объекта? Ведь тогда клиентскому разрешателю OXID вообще не пришлось бы связываться с разрешателем OXID объекта — полученная OBJREF уже содержала бы все необходимое для связи с объектом.
Чтобы понять, почему архитекторы DCOM приняли иное решение, вспомним, что DCOM позволяет клиенту работать с удаленным объектом с помощью разных протоколов: TCP/IP, UDP/IP, IPX/SPX и др. Для каждого поддерживаемого объектом протокола может потребоваться загрузка отдельной DLL, которой обычно необходимы дополнительные потоки для ожидания поступления запросов по данному протоколу. Так что объекту, работающему с несколькими протоколами, это может дорого обойтись. Не удивительно, что архитекторы DCOM стремились снизить накладные расходы, загружая протоколы только тогда, когда это необходимо, и пытаясь обойтись их минимальным количеством.
С этой целью объекты используют отложенную регистрацию протоколов (lazy protocol registration). Другими словами, объект загружает необходимый для протокола код, лишь когда клиент захочет работать с ним по данному протоколу. Например, когда разрешатель OXID объекта получает от клиентской машины вызов IObjectExporter::ResolveOxid, этот запрос выполняется с помощью протокола, скажем, TCP/IP. Разрешатель OXID на машине объекта может определить, загружен ли уже объектом код TCP/IP. Если нет, разрешатель приказывает объекту загрузить соответствующий код, после чего клиент и объект могут взаимодействовать по TCP/IP. Если затем другой разрешатель OXID
запросит связь с тем же объектом по UDP/IP, объект получит указание загрузить код и этого протокола. В то время как разрешатели OXID обязаны ожидать вызовы по всем протоколам, поддерживаемым данной машиной, конкретный объект загружает код только протоколов, явно запрошенных его клиентами. Данный подход иногда требует дополнительного обмена данными при установлении соединения, но он позволяет объектам избежать напрасной траты ресурсов на не используемые ими протоколы.Роль SCM
При создании объекта на той же машине, что и клиент, библиотека СОМ этой машины предоставляет выполнение основной работы по запуску соответствующего сервера диспетчеру управления сервисами (Service Control Manager — SCM). Когда же объект создается на удаленной машине, SCM клиентской машины должен в свою очередь делегировать выполнение этой задачи SCM удаленной машины. Для обеспечения стандартной схемы, позволяющей SCM связываться со своим аналогом на удаленной машине, все SCM поддерживают интерфейс IActivation.
Иначе говоря, SCM запрашивает создание объекта у SCM на другой машине через интерфейс IActivation. Очень простой интерфейс IActivation содержит единственную операцию — RemoteActivation, используемую для активизации объектов, создаваемых любыми описанными ранее способами.
Оптимизация IUnknown
Каждый объект СОМ обязан поддерживать IUnknown — это не менее справедливо для объектов, доступ к которым осуществляется по сети. Но если бы каждый вызов клиентом метода lUnknown транслировался непосредственно в вызов ORPC объекта, результатом почти наверняка была бы неприемлемая производительность. В связи с этим DCOM обеспечивает оптимизацию этих важных методов.
В основе этой оптимизации лежат объекты OXID (OXID objects), каждый из которых поддерживает интерфейс IRemUnknown. Как уже отмечалось, OXID идентифицирует группу объектов, с которыми можно связаться через данное строковое связывание. Все такие объекты представлены одним объектом OXID и, следовательно, одним IRemUnknown (см. рисунок). Все удаленные вызовы методов всех IUnknown таких объектов вначале обрабатываются интерфейсом IRemUnknown соответствующего объекта OXID.
Интерфейс IRemUnknown аналогичен, но не эквивалентен IUnknown. Как и IUnknown, он содержит 3 метода: RemQueryInterface, RemAddRef и RemRelease. Но в отличие от методов IUnknown они позволяют выполнять групповые запросы. Например, одним вызовом RemQueryInterface можно запросить несколько указателей на интерфейсы нескольких объектов. Аналогично RemAddRef и RemRelease позволяют одновременно увеличивать и уменьшать счетчики ссылок
нескольких интерфейсов нескольких объектов (доступ к каждому из которых осуществляется с помощью одной и той же информации связывания).Клиенты не используют интерфейс IRem Unknown непосредственно. Вместо этого они, как всегда, вызывают методы обычного IUnknown. Реализация этих методов стандартным заместителем группирует вызовы и затем выполняет (минимально возможное количество раз) удаленные вызовы IRemUnknown . Например, вызовы клиентом AddRef и Release обычно не транслируются в вызовы IRemUnknown::RemAddRef и IRemUnknown::RemRelease один к одному. Вместо этого каждый из этих методов IRemUnknown вызывается не более одного раза — после первого вызова AddRef и после последнего вызова Release. Значит, объект могут использовать несколько клиентов, и в то же время его счетчик ссылок может быть равен 1. И никаких проблем: ведь объект не будет разрушен пока его счетчик ссылок не равен 0. С точки зрения объекта и его клиентов, все работает аналогично локальному случаю, но в распределенной среде это решение гораздо эффективнее.
Хотя оптимизация вызовов IUnknown, выполняемая инфраструктурой СОМ, несомненно полезна, иногда и сам клиент может действовать более разумно. Например, когда ему нужны указатели на несколько интерфейсов данного удаленного объекта, быстрее всего получить их за один вызов. Для этой цели DCOM определяет интерфейс IMultiQI, указатель на который клиент получает обычным способом — вызывая QueryInterface через указатель на любой интерфейс. Но IMultiQI обычно реализуется локальным заместителем, а не удаленным объектом, так что создается иллюзия, что его поддерживает любой объект. Этот простой интерфейс содержит один метод (кроме унаследованных от IUnknown) — QueryMultipIeInterfaces. Клиент может передать ему список идентификаторов интерфейсов и в ответ получить указатель на каждый из них. Вместо того, чтобы заставлять клиента выполнять несколько удаленных вызовов, вся информация получается в результате одного запроса к удаленному объекту.
Тестовый опрос
Все объекты СОМ используют подсчет ссылок для определения момента, когда они могут безопасно себя разрушить (см. раздел "Подсчет ссылок"). Но в случае объекта, выполняющегося на удаленной машине, есть одна проблема: что будет, если исполнение клиента завершится внезапно? Когда объект и его клиент выполняются на одной машине, объект может быть уведомлен о внезапной смерти клиента непосредственно. Но в распределенном случае ситуация несколько сложнее.
К счастью, эта проблема давно решена: клиент может производить периодический тестовый опрос (pinging) каждого используемого им объекта путем выдачи ему вызова ORPC. Если в течение достаточно длительного времени от данного клиента не приходит подобный тест, клиент считается "погибшим", и объект может предпринять соответствующие действия. Тестовый опрос может служить простым способом поддержки подсчета ссылок в распределенной среде.
Но тестовый опрос может быть и невероятно неэффективным. Вообразите, например, что на машине работают 10 клиентов и у каждого из них по 30 указателей на интерфейсы 10 разных объектов, причем последние все расположены на второй машине. Наивный (и очень дорогой) метод тестового опроса может требовать от каждого клиента опрашивать все интерфейсы каждого объекта по отдельности. Разумнее, объединив эти опросы, обеспечить гораздо более эффективное использование сети.
Так и поступает DCOM. Вместо того, чтобы требовать от каждого клиента опрашивать все объекты индивидуально, пакеты тестового опроса отправляются и принимаются разрешателями OXID. Разрешатель OXID определяет, какие интерфейсы относятся к одному и тому же объекту и какие объекты находятся на одной машине. Затем он определяет набор тестового опроса (ping set), включающего в себя все интерфейсы этих объектов, обращаясь к IObjectExporter::ComplexPing разрешателя OXID удаленной машины. После этого, разрешатель OXID может выполнять один тестовый опрос другого разрешателя для всего набора с помощью IObjectExporter::SimplePing. Это гораздо эффективнее посылки пакета опроса каждым клиентом каждому объекту и уж тем более — тестового опроса каждого интерфейса каждого объекта.
Обесп ечение безопасного доступа к удаленному объекту
Объектный RPC предоставляет клиентам способ создания удаленных объектов и вызова их методов. Но возможность доступа к объектам на других системах повышает риск создания или использования таких объектов процессами или личностями, не имеющими соответствующих прав. Для минимизации подобного риска DCOM определяет стандартный способ доступа к сервисам защиты и их использования.
Здесь имеют место две проблемы. Первая заключается в контроле над тем, кто имеет право запускать серверы различных классов на данной удаленной машине — это сфера действия защиты активизации (activation security). Вторая проблема, состоящая в том, чтобы гарантировать контроль прав на вызовы клиентами методов уже исполняющихся объектов, известна как защита вызовов (call security). DCOM предоставляет решения обеих проблем.
Защита активизации
Параметры реестра машины в точности определяют, кто имеет право запуска серверов на данном компьютере. Более общая установка разрешает или запрещает удаленную активизацию вообще . Если она отключена, ни один удаленный клиент не сможет запускать серверы или подсоединяться к какому-либо объекту на данной машине. Возможно также определение защиты активизации на уровне класса, что позволяет контролировать, какие удаленные клиенты имеют право на запуск сервера некоторого класса. Перечень имеющих разрешение на это содержит список управления доступом (access control list — ACL). И наконец, к классам, для которых не установлена защита активизации на уровне класса, может применяться защита активизации по умолчанию. Как и при защите активизации на уровне класса, защита активизации по умолчанию определяет гех, кто имеет право на запуск сервера в данной системе, с помощью ACL.
Всегда, когда используется ACL, необходимо определить личность пользователя или объекта, выполняющего запрос. Но это приводит к другому вопросу: что такое личность объекта? Или что на жаргоне контроля прав доступа означает слово принципал (principal)? По сути, принципал — это некто (например, пользователь) или нечто (например, выполняющийся процесс), имеющий (-ее) учетную запись (account) в данной среде независимо от его природы. Регистрируясь в системе, пользователь идентифицирует своего принципала, вводя свой идентификатор пользователя и пароль. Предположим, после регистрации пользователь запустил некоего клиента, который создал объект на другой машине. Кем является принципал этого объекта? Ответ на данный вопрос важен, так как принципал объекта может определять, что может делать этот объект.
DCOM предоставляет несколько ответов, зависящих от конфигурационной информации класса . Вновь создаваемый объект может быть сконфигурирован для исполнения как определенный принципал, аналогично сервису Microsoft Windows NT. Но он может выполняться как тот же принципал, что и создавший его клиент, или как принципал интерактивного пользователя, запустившего клиент (если они различаются). Если для данного класса в реестре ничего не задано, вновь созданному объекту по умолчанию присваивается принципал клиента, создавшего его.
Защита вызовов
После создания объект готов к приему вызовов своих методов от клиентов. Когда последние выполняются на других компьютерах, можно с пользой задействовать ряд сервисов контроля доступа. Вот самые важные:
- аутентификация позволяет удостовериться, что пользователь именно тот, за кого себя выдает. С помощью аутентификации объект достоверно определяет личность клиента. Если поддерживается взаимная аутентификация, то и клиент может быть уверен в том, что сервер является именно тем за кого себя выдает;
- авторизация позволяет определить, что клиент имеет npаво делать. Для принятия подобного решения объект может использовать любую схему. Например, в Microsoft Windows NT объект может воспользоваться встроенной поддержкой ACL;
- целостность данных гарантирует, что принятые по сети данные не были изменены при пересылке. В отсутствие этого сервиса некто мог бы "выловить" пакет из сети, изменить его и послать первоначальному реципиенту, причем последний не заметил бы подмены;
- секретность данных гарантирует, что пересылаемые и сети данные не будут прочитаны при передаче. Обычно для этого используется шифрование данных, что может существенно снизить производительность.
Сервисы защиты могут обеспечиваться различными механизмами. Например, механизмы защиты, включенные в состав Windows NT, поддерживают все 4 упомянутых сервиса. Альтернативой является Kerberos — система защиты, разработанная в Массачусетском технологическом институте (Massachusetts Institute of Technology). Kerberos обеспечивает аутентификацию, целостность и секретность данных, но не решает вопросы авторизации (но Kerberos поддерживает взаимную аутентификацию — средство, отсутствующее в составе механизмов распределенной защиты Windows NT). Другие механизмы защиты предоставляют другие комбинации сервисов защиты.
В связи с большим количеством механизмов, обеспечивающих защиту вызовов, архитекторы DCOM столкнулись с проблемой: какой из них поддерживать? Было решено не выбирать какой-то один, но определить общие интерфейсы, способные работать со многими из существующих вариантов. Тем не менее первая версия DCOM, выпущенная в 1996 году, поддерживает только механизмы защиты Windows NT. В следующих версиях планируется включить поддержку Kerberos.
Независимо от используемого базового механизма, интерфейсы защиты вызовов DCOM определяют 2 основные возможности: автоматическую и поинтерфейсную защиты . В первом случае клиентский или серверный процесс устанавливает уровень защиты по умолчанию для всех вызовов этого процесса или вызовов, выполняемых им. Во втором — разные установки защиты можно задать для разных интерфейсов одного объекта или для разных объектов. Можно использовать и оба варианта одновременно, установив значения по умолчанию с помощью автоматической защиты и выполнив затем тонкую настройку, используя поинтерфейсную защиту.
Автоматическая защита
Будет ли процесс клиентом, выдающим запросы, или объектом, принимающим запросы, или и тем и другим, он может задать значения по умолчанию автоматической защиты вызовом CoInitializeSecurity. Вызов этой функции достаточно сложен, но позволяет процессу быстро и полностью задать свои требования по защите. Вот его наиболее интересные параметры:
- ACL определяет, кто имеет право вызывать методы любого объекта данного процесса, а также кому это явно запрещено;
- уровень аутентификации процесса определяет, как часто выполняется аутентификация (один раз при установке соединения, один раз для каждого вызова, или один раз для каждого пакета), а также должны ли использоваться средства обеспечения целостности и секретности данных. Вызовы, выполняемые данным процессом, будут исполняться на заданном уровне защиты, и все вызовы извне, использующие уровень ниже заданного, будут отвергаться;
- список сервисов аутентификации вызовов извне.
Вызвав эту единственную функцию, процесс может установить значения широкого диапазона параметров защиты. Но, решив не прибегать к ней, процесс все равно наследует значения по умолчанию для данной системы. В защищенной зоне системного реестра администратор может задать общемашинные значения по умолчанию, применяемые ко всем процессам, не вызывающим CoInitializeSecurity самостоятельно . Эти установки по умолчанию включают уровень аутентификации и ACL, управляющий тем, кто имеет доступ к процессу без собственного ACL.
Поинтерфейсная защита
Автоматическая защита — грубый инструмент. Она определяет один набор параметров защиты для всего, что делает процесс, будь он клиентом или сервером. Автоматическая защита, кроме того, проста в использовании, и для многих приложений — это именно то, что нужно. Но не каждый процесс может обойтись одной автоматической защитой. Например, клиент может пожелать использовать разные уровни защиты для вызовов разных объектов или даже разных интерфейсов одного объекта. Серверу, реализующему несколько объектов, может потребоваться применять к каждому из них или к каждому из их интерфейсов особые схемы авторизации. Поинтерфейсная защита обеспечивает поддержку подобных тонких разграничении.
В отличие от автоматической защиты, когда один вызов устанавливает параметры как серверной, так и клиентской защиты, Поинтерфейсная защита выглядит для клиента и сервера по-разному. Чтобы использовать ее, клиент должен получить указатель на интерфейс IClientSecurity, тогда как серверу понадобится
для этого указатель на интерфейс IServerSecurity. Методы этих интерфейсов позволяют клиенту и серверу определить конкретные нужные им сервисы и уровни защиты.С точки зрения клиента, каждый объект поддерживает IClientSecurity. Клиент получает указатель на этот интерфейс обычным образом с помощью QueryInterface . На самом деле эта внешне очевидная ситуация не так проста. Фактически интерфейс IClientSecurity реализован самой СОМ в библиотеке, подгружаемой к клиенту. При запросе клиентом IClientSecurity с помощью QueryInterface обращение к объекту никогда не производится. Вместо этого возвращается указатель на локальную реализацию интерфейса.
С помощью методов полученного указателя IClientSecurity клиент может установить параметры защиты для каждого интерфейса объекта. В действительности, однако, при этом устанавливаются параметры, поддерживаемые заместителем данного интерфейса внутри клиента, так что вызовы IClientSecurity не поступают к самому объекту. Среди методов IClientSecurity отметим:
После того как клиент задал параметры защиты заместителя с помощью IClientSecurity::SetBlanket, эти параметры используются при всех вызовах, выполняемых через данный заместитель. Вызовы через другие заместители осуществляются через параметры, заданные для последних. (Если параметры защиты не были заданы явно, используются значения, установленные CoInitializeSecurity либо общемашинные умолчания.) Вспомогательные функции, в которых заключены типичные последовательности вызовов, несколько облегчают установку этих параметров. Вместо того, например, чтобы запрашивать IClientSecurity, вызывать IClientSecurity::SetBlanket и затем освобождать указатель на интерфейс IClientSecurity, клиент может вызвать CoSetProxyBlanket, помещающую все эти вызовы в "удобную упаковку".
Независимо от используемых параметров защиты каждый вызов, сделанный клиентом, приводит в конце концов к исполнению кода метода, реализованного объектом. Установление личности клиента (аутентификация) выполняется СОМ, а также любое необходимое декодирование и проверка целостности принятых данных. Если какая-то из проверок не удалась, вызов отвергается. А если все в порядке, метод объекта должен сам определить, имеет ли данный клиент право на
выполнение того, что запрашивает. Другими словами, объект должен выполнить авторизацию.Чтобы определить, что имеет право делать данный клиент, надо знать, кем он является и какие параметры защиты он задал для данного вызова. Для получения этой информации код метода объекта начинает с вызова библиотечной функции CoGetCallContext, возвращающей указатель на IServerSecurity — серверный аналог IClientSecurity. Задействовав методы этого интерфейса, объект может получить информацию о клиенте, сделавшем данный вызов, а затем использовать ее для определения того, что этот клиент имеет право делать. Среди методов IServerSecurity отметим следующие:
Как и в случае IClientSecurity, есть несколько вспомогательных функций. Например, объект может вызвать CoQueryClient-Blanket, заключающую в себе вызовы CoGetCallContext, IServerSecurity: :QueryBlanket и IServerSecurity:: Release.