"); // --> |
Запомнить этот сайт: : Вернуться в "Библиотеку электронных учебников" Реклама в Интернет | "Все Кулички" | Интернет Аукцион Империя ПМК |
"); // --> |
Основой как ActiveX, так и OLE является модель многокомпонентных объектов (COM).
СОМ устанавливает абстракции и правила, необходимые для определения объектов и интерфейсов; в ее состав входит также программное обеспечение, реализующее ключевые функции. Хотя СОМ не является ни большой, ни слишком сложной, она определенно отличается как от других архитектур предоставления программных сервисов, так и от традиционных объектно-ориентированных подходов. Поэтому разработчикам придется взглянуть по-новому на, казалось бы, знакомые вещи.
Как уже говорилось, программы, созданные с использованием СОМ, предоставляет свои сервисы через один или несколько СОМ-объектов. Каждый такой объект является экземпляром некоторого класса и поддерживает определенное количество интерфейсов, обычно не менее двух. В состав каждого интерфейса входит один или более методов — функций, которые могут вызываться клиентом объекта. Например, один из гипотетических объектов, обсуждавшихся в предыдущей лекции, поддерживает как корректировщик орфографии, так и словарь синонимов, предоставляя доступ к своим сервисам через два разных интерфейса. Интерфейс корректировщика орфографии содержит 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 содержит только три метода: 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, так как их реализации различны.
Каждый объект СОМ реализован внутри некоторого сервера, содержащего код, который реализует методы интерфейсов объекта, а также контролирует данные объекта, пока тот активен. Один сервер может поддерживать (и зачастую поддерживает) более одного объекта некоторого класса и даже поддерживать несколько классов. Рассмотрим три основные типа серверов:
С точки зрения клиента, объекты, реализованные любой из трех разновидностей серверов, выглядят одинаково; доступ к методам объектов клиент по-прежнему осуществляет через указатели интерфейсов. При необходимости он может проводить различие между разными типами серверов, но это не обязательно. Запуск объекта, получение указателей на его интерфейсы, вызов их методов и освобождение указателей выполняются клиентом одинаково независимо от того, каким сервером реализован объект: "в процессе", локальным или удаленным.
В традиционном простом процессе в один и тот же момент времени выполнялось только одно действие. Другими словами, у процесса был только один поток управления (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, она в состоянии запустить лишь некий абстрактный экземпляр объекта. 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.
Чтобы все здесь работало правильно, требуется преодолеть и другие сложности. Достаточно сказать, что реализация агрегирования требует определенной работы. И все же ни понимание, ни реализация агрегирования не покажутся сложными, если приложить хоть чуточку усилий. И они окупятся сторицей — ведь агрегирование может существенно повысить производительность при повторном применении объектов.
С самого начала СОМ разрабатывалась с учетом обеспечения поддержки распределенных сред, т.е. способности клиента создавать объекты на других машинах и вызывать их методы по сети. Эти планы стали реальностью в 1996 году после выпуска распределенной СОМ (Distributed СОМ — DCOM). DCOM позволяет клиенту создавать и использовать объекты как на удаленных системах, так и на локальной. Более того, клиент может даже не осознавать различия между этими двумя случаями. Подобно тому как клиенты СОМ имеют прозрачный доступ к объектам в динамических библиотеках и локальных процессах, DCOM обеспечивает прозрачный доступ к объектам в удаленных процессах. Фактически самое трудное в достижении подобной прозрачности — это обеспечить взаимодействие объектов, исполняющихся в разных процессах независимо от того, выполняются эти процессы на одной машине или нет. В этом смысле, с точки зрения проектирования, DCOM — довольно незначительное расширение оригинальной СОМ.
Возможность запускать удаленные объекты и вызывать их методы — важное достижение, но требуется большее. В частности, нужен способ контроля за тем, кто имеет право создавать объекты на данной машине, и обеспечение безопасного доступа к этим объектам по сети, которая может быть наполнена потенциальными врагами. С этой целью в основу DCOM положен набор сервисов контроля доступа. Приложения (включая программы, созданные до DCOM) могут использовать DCOM и работать вполне безопасно без добавления какого-либо кода, связанного с защитой. С другой стороны, приложения, знающие о новых средствах DCOM контроля доступа, могут задействовать их явно.
Несмотря на отдельные сложные моменты, DCOM вообще проста для понимания. Она добавляет к знакомым основам СОМ всего 3 основных элемента: способ создания удаленного объекта, протокол вызова методов этого объекта и механизмы обеспечения безопасного доступа к нему.
Сервисы создания объектов — одни из важнейших сервисов, предоставляемых СОМ. Клиенты обычно создают объекты, вызывая библиотеку СОМ или через моникеры. Эти подходы работают и в DCOM, хотя и с некоторыми новыми особенностями. Рассмотрим различные варианты создания объектов, доступные клиентам.
Независимо от того, где исполняется объект, клиент обычно создает его и затем получает указатели на необходимые интерфейс. Для большинства описанных ранее объектов — реализованных сервером "в процессе" или локальным сервером — это можно сделать, вызвав 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 для создания объекта на локальной машине (см. раздел "Создание нескольких объектов одного класса: фабрики классов"). Подобный процесс применяется и при создании объекта на удаленной машине, хотя бы с точки зрения программиста. На самом же деле этот процесс был оптимизирован для повышения производительности, и все эти действия выполняются за один цикл взаимодействия "запрос-ответ" с удаленной машиной.
Применение 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 автоматически пытаются создать объект на той же машине, где находится его перманентное хранилище, если в локальном реестре моникером не найдено информации о классе объекта.Возможность удаленного создания объекта СОМ — первое, что нужно для работы в распределенной среде. С точки зрения клиента, доступ к такому удаленному объекту осуществляется так же, как и к локальному, — все абсолютно одинаково. Однако скрывающаяся за этим реальность гораздо сложнее — ведь для доступа к удаленным методам используются механизмы, совершенно отличные от механизмов работы с локальными объектами.
После запуска удаленного объекта и получения указателей его интерфейсов клиент может вызывать методы этих интерфейсов. Вызов метода объекта "в процессе", по сути, означает непосредственное обращение к нему через виртуальную таблицу (а в случае диспинтерфейса и через 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) — символьной строкой, содержащей всю необходимую информацию.
Информацию связывания с данной удаленной системой клиент может получить по-разному. Напристрацию протоколов (lazy protocol registration). Другими словами, объект загружает необходимый для протокола код, лишь когда клиент захочет работать с ним по данному протоколу. Например, когда разрешатель OXID объекта получает от клиентской машины вызов IObjectExporter::ResolveOxid, этот запрос выполняется с помощью протокола, скажем, TCP/IP. Разрешатель OXID на машине объекта может определить, загружен ли уже объектом код TCP/IP. Если нет, разрешатель приказывает объекту загрузить соответствующий код, после чего клиент и объект могут взаимодействовать по TCP/IP. Если затем другой разрешатель OXID запросит связь с тем же объектом по UDP/IP, объект получит указание загрузить код и этого протокола. В то время как разрешатели OXID обязаны ожидать вызовы по всем протоколам, поддерживаемым данной машиной, конкретный объект загружает код только протоколов, явно запрошенных его клиентами. Данный подход иногда требует дополнительного обмена данными при установлении соединения, но он позволяет объектам избежать напрасной траты ресурсов на не используемые ими протоколы.