Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Что такое "технология COM" и как с ней бороться?


Служба Рассылок Subscribe.Ru

М. Безверхов
vasilisk@clubpro.spb.ru

Что такое "технология COM" и как с ней бороться?     №42


О пользе "и.о."

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

Из прошлого выпуска должно быть понятно, что физически, на уровне команд собственной программы случаи вызова внутрипроцессного (inproc) и локального (local) сервера - отличаются как день и ночь, при организации этих вызовов не то, что сильно отличается объём действий, а и сами-то действия абсолютно никак не сопоставимы. И... этого никак допустить нельзя. Нельзя потому, что нарушается один из основных принципов COM - интерфейс объекта всегда должен оставаться неизменным, не зависеть от того кто кого и каким образом вызывает. Т.е., высказывая это же на "машинном языке" - команды вызова метода объекта в клиенте должны быть одними и теми же, расположен ли сервер локально или внутри собственного процесса. Аналогичное можно сказать и про сервер. Налицо - противоречие, которое на стороне именно клиента и именно сервера решить невозможно. Но у нас между клиентом и сервером есть ещё и промежуточный слой - сама среда COM на которую можно попытаться возложить эту задачу. Остаётся только понять, что именно нужно ей поручить.

Последовательное изучение - благодарное занятие, поскольку всегда можно обратиться к предыдущему изученному. И сейчас мы к нему обратимся - вспомним самые-самые основы, самый простейший случай - вызов метода inproc-сервера. Физически это делается просто, как мы знаем это не что иное, как локальный вызов метода vtbl, т.е. по своей сути - вызов локальной процедуры, расположенной в DLL. Клиент подготавливает список параметров в стеке, извлекает по известному смещению в vtbl адрес начала процедуры и делает call dword ptr <адрес>. Команда call помещает адрес возврата в стек, так что когда метод, завершаясь, исполнит команду ret управление в клиенте будет передано туда, куда следует - на следующую за call команду. Напоминать как должен происходить "вызов процедуры" между двумя разными потоками, я надеюсь, излишне - это изложено в прошлом выпуске. Но вот вопрос, который хочется задать - а нельзя ли как-нибудь совместить первое и второе? И - каким образом?

Для того, чтобы компонент не терял способности взаимодействовать inproc-способом он, очевидно, ни в коем случае не должен отказываться от механизма вызова локальной процедуры по адресу из vtbl, как и изображено на рис. 10, на котором показан сервер (объект сервера)экспонирующий один интерфейс - vtbl, и клиент, ссылающийся на метод этого интерфейса:


Клиент-серверное inproc-взаимодействие

рис 10. Клиент-серверное inproc-взаимодействие

А для того, чтобы компонент мог взаимодействовать local-способом он должен в своём процессе выполнять совсем другие действия. Только вот "в своём процессе" это не совсем то же самое, что "в своём модуле", поскольку вынесение всей функциональности манипуляции потоками в отдельную DLL никаких основ не нарушает - её ведь заведомо можно оформить "с другой стороны" как inproc-сервер? В самом деле, пусть у нас первоначально было inproc-взаимодействие. Есть клиент, сервер, определён интерфейс... Затем мы разделяем эти компоненты, помещая каждый в свой собственный процесс. При этом, чтобы для клиента "ничего не изменилось" мы должны будем обеспечить ему очень своеобразный inproc-сервер, который просто будет манипулировать потоками так, как описано в прошлом выпуске. А на стороне сервера, мы, естественно, должны будем обеспечить своеобразного inproc-клиента, который, принимая команды манипуляции потоками, транслирует их в inproc-вызовы методов. И ни "исходный сервер", ни "исходный клиент" в даном случае никакого изменения не претерпят - если их снова поместить в один процесс, то они как взаимодействовали, так и будут взаимодействовать. Существо проделанной операции изображено на рис. 11:


Клиент-серверное local-взаимодействие

рис 11. Клиент-серверное local-взаимодействие

Здесь компоненты "заместитель сервера" и "заместитель клиента" - есть DLLи, находящиеся в соответствующем процессе и взаимодействующие между собой каким-нибудь способом - сам способ их взаимодействия к COM не относится и может быть любым, удобным для данной операционной системы.

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

Рассмотрим подробнее, каковы их функции и каким образом компоненты-заместители могут быть реализованы. Для определённости начнём рассмотрение со стороны пользовательского клиента. Служебный компонент, который в процессе клиента пользователя "исполняет обязанности сервера" носит название proxy. Его функции - принять inproc-вызов клиента и "каким-то образом" передать его (вызов) в процесс сервера пользователя. При этом клиент "добросовестно заблуждается" - он действительно считает proxy "настоящим сервером" - о том, что действительный сервер исполняется в другом потоке клиенту неведомо. В частности, то, что поток клиента принудительно останавливается - исключительная забота proxy, клиент ничего не знает о том, что есть какие-то синхронизаторы. Но синхронизация - не единственная проблема, решаемая proxy. Клиент "считает", что он вызывает локальный метод, т.е. предоставляет proxy стек, заполненный параметрами, передаваемымим методу. Забота proxy - извлечь этот список и переместить его каким-то образом на сторону сервера. При этом, хорошо, если передаваемые клиентом параметры - сплошь просто двоичные числа. А если клиент передаёт указатели? Которые заведомо недействительны в другом процессе, каким бы образом ни передавались? В таком случае proxy должен их преобразовать к такому виду, чтобы на стороне сервера из них можно было восстановить ссылки на те объекты, на которые указатели по своей семантике и указывают. Это явление носит название маршалинга. Сейчас мы его только упоминаем, хотя на самом деле это очень большая и разносторонняя тема, рассмотрению которой мы уделим соответствующее время.

Рассмотрим теперь сторону пользовательского сервера. Служебный компонент, который в процессе сервера "выполняет обязанности клиента" носит название stub. Его функции дополнительны к функциям proxy - он так же синхронизирует вызовы, которые получает от proxy и преобразует их в "вызовы от локального клиента", получает от proxy список параметров, распаковывает его, восстанавливает указатели и помещает список параметров в локальный стек потока сервера... Словом, под управлением stub и сервер заблуждается не менее клиента, т.е. считает stub "настоящим клиентом" не подозревая, что действительный клиент исполняется в другом потоке. Естественно, что и сервер ничего не знает о том, что "его" поток всё свое время проводит в спячке в синхронизаторе stub.

Каким образом реализуется связь между proxy и stub? Корректный ответ на него - посредством системного сервиса RPC (remote procedure call), вызовы которого оба компонента используют. Каким образом реализован сам RPC? А вот это уже - не совсем корректный вопрос. Это - зависит от обстоятельств, от возможностей конкретной операционной системы, от способов, которые выбраны для передачи сигналов и разделения данных между процессами. Все эти обстоятельства с полным правом можно назвать "системно-зависимыми" и говорить о том, что они реализованы именно так и никак иначе - довольно рискованно. Но, на что мы, конечно, должны обратить внимание - проблема собственно физической связи между клиентом и сервером в такой модели полностью инкапсулируется в proxy и stub и никак не выносится на уровень пользовательских клиента и сервера, которые всегда остаются одними и теми же. Но предположения (с той или иной погрешностью, поскольку инкапсуляция позволяет добиться главного - в последующих версиях системы предлагать более эффективные реализации связи между клиентом и сервером без того, чтобы они могли замену этой реализации ощутить) строить можно. Например, связь proxy и stub для случая взаимодействия компонентов в разных процессах одной машины реализована на синхронизаторах (а более точно - на оконных процедурах Windows). А вот связь, свойственная DCOM, т.е. обеспечивающая взаимодействие компонентов в разных процессах разных машин, сделана поверх сетевого протокола.

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

Сделаем и практическое замечание - создание proxy и stub есть процесс, если не автоматизированный, то в значительной степени механизированный в современной среде компонентной разработки. Что-либо "своё" в этом процессе придумывать приходится исключительно редко, поскольку все стандартные ситуации в системе имеют и стандартные решения. Например, компилятор MIDL, как мы потом увидим - главное "действующее лицо" в организации комплементарных статических структур, обеспечивающих взаимодействие клиента и сервера, генерирует исходные тексты proxy и stub, соответствующих заданному интерфейсу. И для создания proxy/stub нужно только откомпилировать и слинковать сгенерированный им текст.

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

предыдущий выпуск

архив и оглавление

следующий выпуск


Авторские права © 2001 - 2002, М. Безверхов
Публикация требует разрешения автора.


NHN.ru - computer banner network

http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное