Распределенные вычисления и технологии Inprise. Часть 4. Использование CORBA для организации распределенных вычислений
Создание CORBA-сервера для доступа к данным с помощью Delphi 4
Создание клиентского приложения с помощью Delphi 4
Многопользовательская работа в трехзвенных информационных системах
Понятие о stub- и skeleton-объектах
Регистрация интерфейсов и реализаций
Регистрация в репозитарии интерфейсов
Регистрация в репозитарии реализаций с помощью Object Activation Daemon
Настоящая статья посвящена организации распределенных вычислений с помощью технологии CORBA (Common Object Request Broker Architecture). Спецификация CORBA разработана консорциумом OMG (Object Management Group).
Так же, как и COM, CORBA реализует концепцию объектно-ориентированного подхода и повторного использования кода не на уровне наследования реализации классов внутри одного приложения, а на уровне разных приложений и операционной системы. Однако, в отличие от COM, стандарт CORBA применим не только в случае различных версий Windows, но и для других платформ.
Спецификация CORBA определяет правила взаимодействия клиентов с объектами, реализованными в серверах. Для осуществления этого взаимодействия используется Object Request Broker, представляющий собой, в отличие от брокеров OLEnterprise или Entera (конкретных приложений), концепцию, которая может иметь несколько разных реализаций, имеющих, в свою очередь, в своем составе определенный набор сервисов. В случае Delphi обычно используется реализация Visibroker. Более подробно о сервисах CORBA, содержащихся в этой реализации, будет рассказано чуть ниже.
Создание CORBA-сервера для доступа к данным с помощью Delphi 4
Для создания сервера приложений, предоставляющего доступ к данным, нам потребуется какая-либо серверная СУБД, клиентом которой будет данный сервер приложений, и тестовые таблицы, которые будут перенесены на сервер баз данных. В рассмотренных ниже примерах используется база данных DBDEMOS, но в общем случае это может быть любая СУБД, в том числе серверная.
Создание CORBA-сервера начнем с создания обычной формы (можно небольшого размера, так как основное ее назначение — быть индикатором запущенного сервера приложений) со значением свойства FormStyle, равным fsStayOnTop (рис. 1), чтобы не потерять это окно среди других открытых окон.
Можно разместить ее где-нибудь в углу экрана. Появление на экране главной формы в общем случае не является обязательным. Тем не менее коль скоро она создана, поместим на нее компонент TLabel и установим значение его свойства Caption равным «0» (в дальнейшем в этой метке будет отображаться число подключенных к серверу клиентов).
Сразу же создадим связанную с этой формой процедуру изменения значения счетчика подключений:
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class ( TForm ) Label1: TLabel; private FCount: Integer; { Private declarations } public procedure UrdateCount ( Incr: Integer); { Public declarations } end; var Form1: TForm1; implementation uses serv2; { $R *.DFM} { TForm1 } procedure TForm1.UrdateCount ( Incr: Integer); begin FCount := FCount + Incr; Label1.Caption := IntToStr(FCount ); end; end.
CORBA Data Module имеет принципиальное отличие от обычного модуля данных, заключающееся в том, что он представляет собой объект, обладающий CORBA-интерфейсами, доступными извне (рис. 2).
Создание CORBA Data Module начинается с запуска эксперта CORBA Data Module Wizard, в котором определяется, в частности, имя класса, под которым CORBA-объект — экземпляр данного класса — может быть опознан запрашивающими его приложениями (рис. 3).
Параметр Instancing указывает, сколько экземпляров данного класса может быть создано CORBA-сервером. Значение Instance-per-Client, используемое по умолчанию и выбранное нами в данном случае, означает, что один запущенный экземпляр сервера может создать несколько экземпляров удаленного модуля данных — по одному на каждого клиента. Значение Shared Instance означает, что один экземпляр удаленного модуля данных обслуживает нескольких клиентов (это наиболее часто встречающаяся модель обработки данных в CORBA). В этом случае следует создавать приложение так, чтобы модуль данных не хранил данные, связанные с конкретным клиентом (пример такого кода содержится в статье, посвященной созданию объектов Microsoft Transaction Server).
Параметр Threading Model указывает на то, поддерживает ли наш модуль данных многопоточность. Значение Single-threaded означает, что каждый экземпляр объекта обрабатывает в данный момент времени только один запрос клиента, поэтому обращение к данным, содержащимся в этом объекте (свойствам и др.), является безопасной операцией. Однако в этом случае следует предусматривать защиту от коллизий, связанных с модификацией глобальных переменных или объектов. Значение Multi-threaded означает, что каждое соединение, инициированное клиентом, обслуживается в отдельном потоке. В этом случае следует заботиться не только о глобальных переменных, но и о возможных коллизиях, связанных с одновременным обращением к данным объекта из разных потоков.
В данном случае можно оставить значение Single Threaded.
Далее в созданный удаленный модуль данных поместим два компонента TTable, свойство DatabaseName которых установим равным DBDEMOS, а имена равными CLIENTS.DBF (Table1) и HOLDINGS.DBF (Table 2). Свойство Active этих компонентов TTable не обязательно устанавливать равным True (они будут автоматически открыты при обращении клиента к данному серверу приложений). Добавим также компонент TDataSourse и свяжем его с компонентом Table1 (рис. 4).
В данном случае мы поместили компонент TDatabase на главную форму, а не в модуль данных. Это означает, что все пользователи данного сервера будут использовать одно и то же соединение с базой данных и, соответственно, регистрироваться под именем одного и того же пользователя. Если такое решение неприемлемо, можно поместить его и в модуль данных. В этом случае туда же следует поместить и компонент TSession и позаботиться о разных именах пользовательских сессий во избежание конфликтов между ними.
Затем установим связь Master-Detail между таблицами (в данном случае по общему полю ACCT_NBR, для чего следует выбрать соответствующий индекс).
Далее из контекстного меню компонента Table1 нужно выбрать опцию:
Export Table1 from data module
Выбор этой опции приведет к тому, что в интерфейс, предоставляемый удаленным модулем данных, будут добавлены свойства и методы для работы с компонентами доступа к данным. В этом случае мы получим CORBA-объект, хранящий данные, связанные с конкретным клиентом, что вполне допустимо, когда параметр Instancing имеет значение Instance-per-Client. В случае же если этот параметр имеет значение Shared Instance, выбирать опцию экспорта из меню не следует, так как такой CORBA-объект не должен хранить никаких данных, связанных с конкретным клиентом (такие объекты называются stateless objects). В этом случае обычно создается метод, реализующий соединение с базой данных, открытие нужной таблицы, передачу данных клиенту, закрытие таблицы и разрыв соединения с базой данных (так называемый stateless code).
Проконтролировать наличие в интерфейсе удаленного модуля данных свойств и методов для доступа клиента к компонентам доступа к данным можно просмотрев соответствующую библиотеку типов (рис. 5).
Находясь в редакторе библиотеки типов, нажмем кнопку экспорта ее в файл описания интерфейса CORBA IDL — этот файл нам пригодится в дальнейшем.
Теперь включим ссылку на главную форму приложения в текст модуля, связанного с удаленным модулем данных, и создадим обработчики событий, связанные с созданием и уничтожением удаленного модуля данных:
procedure Tcrb1.crb1Create (Sender :TObject); begin Form1.UpdateCount(1); end; procedure Tcrb1.crb1Destroy (Sender :TObject); Form1.UpdateCount(-1); end;
Далее следует сохранить проект и скомпилировать приложение.
Отметим, что, для того чтобы клиентское приложение имело доступ к CORBA-серверу, недостаточно иметь запущенный экземпляр сервера, даже если разработка клиента производится на том же самом компьютере. CORBA, в отличие от COM, не делает различий между локальным и удаленным сервером; и в том и в другом случае требуется запуск сервиса, предоставляющего доступ к серверу. В случае реализации CORBA, предложенной Visigenic, он называется Visibroker Smart Agent.
Можно легко объяснить, почему в случае CORBA не делается различий между локальным и удаленным сервером. COM — это не только технология распределенных вычислений, но и способ организации разделения функций между приложениями и библиотеками, в значительной степени составляющий основу самой операционной системы Windows. Естественно, что механизмы, осуществляющие поиск локальных COM-объектов и инициирование запуска COM-серверов, которые реализуют эти объекты, интегрированы в операционную систему и используют регистрационную базу данных самой операционной системы — реестр Windows — для поиска этих реализаций. Вообще говоря, и механизмы удаленного доступа к COM-серверам также содержатся в Windows NT и практически не отличаются от механизмов доступа к серверам локальным; просто они требуют соответствующих настроек, вызванных главным образом соображениями элементарной безопасности.
CORBA же — это, по существу, межплатформная технология. Требование поддержки многих платформ, естественно, не может быть выполнено, если в технологии распределенных вычислений используется какой-либо механизм, специфический для конкретной платформы (например, использование реестра — ведь его нет в других операционных системах). Следовательно, CORBA не может использовать и механизмы доступа к серверам и сервисам, которые доступны COM, так как они являются специфичными для Windows. Иные же способы доступа к серверам не содержатся в операционных системах и, следовательно, требуют наличия сервисов, предоставляющих такой доступ.
Итак, прежде чем приступить к разработке клиентского приложения, нужно обязательно запустить Smart Agent (он устанавливается одновременно с Delphi 4 Client/Server Suite) и лишь потом — скомпилированный сервер (желательно непосредственно из Windows).
Создание клиентского приложения с помощью Delphi 4
Теперь создадим новый проект — будущее клиентское приложение. Если он разрабатывается на том же компьютере, где создавался сервер приложений, можно добавить его к уже существующей группе проектов. В общем же случае клиентское приложение не обязано разрабатываться на том же компьютере, что и сервер (в этом случае сервер может вообще функционировать на платформе, отличной от Windows).
Создадим главную форму, поместим на нее компонент TCorbaConnection. Установим значения его свойств такими, как показано на рис. 6.
Здесь HostName — IP-адрес компьютера, на котором должен выполняться CORBA-сервер, ObjectName — имя удаленного модуля данных (CORBA-объекта; в общем случае их может быть в одном сервере несколько). Свойство RepositoryId — идентификатор конкретной реализации CORBA-объекта в формате <имя исполняемого файла>/<имя объекта>. Возможен и другой формат этой строки:
IDL:<имя исполняемого файла>/<имя объекта>:<версия>.
Теперь свойство Connected компонента CorbaConnection1 можно установить равным True (или произвести установку этого свойства равным True на этапе выполнения в момент создания формы клиентского приложения).
Затем можно поместить на форму компонент TClientDataSet, установить значение его свойства RemoteServer равным CorbaConnection1, а после поместить на форму компонент TDataSource и связать его с компонентом TClientDataSet.
Компонент TClientDataSet предназначен для хранения данных, полученных от сервера приложений, в кэше клиента и, будучи потомком компонента TDataSet, обладает подобно компонентам TTable и TQuery как навигационными методами, так и методами, осуществляющими редактирование данных. Кроме того, этот компонент обладает методами SaveToFile и LoadFromFile, позволяющими сохранять данные из кэша в файле и восстанавливать их оттуда, реализуя так называемую briefcase model — модель обработки данных, основанную на том, что «тонкий» клиент осуществляет редактирование данных по большей части при отсутствии соединения с сервером, используя лишь кэш или локальные внешние устройства, и лишь иногда соединяется с сервером приложений для передачи ему измененных данных с целью дальнейшей обработки.
Добавим на форму компоненты TDBGrid, TDBImage, TDBNavigator и свяжем их с DataSource1. Свойство DataField компонента DBImage1 установим равным IMAGE. Добавим также две кнопки (рис. 7).
Теперь можно установить нужное значение свойства ProviderName компонента TClientDataSet, выбрав его из выпадающего списка объектов, экспортированных из удаленного модуля данных сервера приложений (так как сервер приложений выбран, все его доступные через интерфейс свойства становятся видны в среде разработки). Далее нужно установить значение свойства Active компонента TClientDataSet равным True (рис. 8).
Отметим, что если пользователь редактирует данные в клиентском приложении, то они не переносятся непосредственно на сервер баз данных, а вместо этого кэшируются. Фактически компонент TClientDataSet осуществляет доступ к содержимому кэша и при необходимости инициирует добавление в него записей и перенос их обратно на сервер. Идея кэширования данных не нова: использовать его можно и в случае использования классической двухзвенной архитектуры (например, установив равным True свойство CachedUpdates компонента TTable или TQuery). Однако для «тонких» клиентов, связь которых с сервером приложений может осуществляться самыми разными способами, в том числе с помощью каналов, не обеспечивающих стабильную постоянную связь (например, с помощью модемного соединения по телефонной линии), этот способ редактирования данных является основным. На нем же основана так называемая briefcase model (модель работы с данными без постоянного соединения), когда пользователи лишь время от времени соединяются с сервером приложений для синхронизации данных, а подавляющую часть времени работают с кэшем и локальными внешними устройствами, периодически сохраняя и восстанавливая свои данные с помощью методов SaveToFile и LoadFromFile компонента TClientDataSet.
Отметим, что в Delphi 4 компонент TСlientDataSet может содержать кэшированные данные не только из одной, но и из нескольких связанных между собой на сервере приложений таблиц. В нашем случае исходные таблицы CLIENTS и HOLDINGS связаны соотношением Master-Detail на сервере приложений. В результате этого в наборе данных, кэшируемом компонентом ClientDataSet, помимо полей, соответствующих реальным полям таблицы CLIENTS, имеется поле типа TDataSetField, реально соответствующее detail-записям таблицы HOLDINGS, связанным с текущей записью таблицы CLIENTS. Иными словами, компонент TClientDataSet эмулирует так называемые вложенные таблицы, характерные для некоторых объектно-ориентированных СУБД.
Для инициации процесса реального сохранения в базе данных используется метод ApplyUpdates компонента TClientDataSet. В качестве параметра этот метод использует целое число, равное максимально допустимому числу ошибок сервера, после которого следует прекратить попытки физического сохранения данных (значение -1 означает, что следует пытаться сохранить все данные независимо от числа ошибок).
Почему предусмотрена возможность ошибок сервера? Ответ очевиден — клиентское приложение может в общем случае ничего не знать о правилах ссылочной целостности и иных ограничениях, содержащихся на сервере баз данных, и только попытка физического сохранения изменений в базе данных может выявить их нарушения (отметим, что на клиентской рабочей станции может даже не быть библиотеки BDE).
Для сохранения данных из кэша на сервере баз данных создадим обработчики событий, связанных с нажатием на кнопки Apply и Cancel:
procedure TForm1.Button1Click (Sender: TObject); begin ClientDataSet1.ApplyUpdates (-1); end; procedure TForm1.Button2Click (Sender: TObject); begin ClientDataSet1.CancelUpdates ; end;
Далее можно запустить и протестировать клиентское приложение. Обратите внимание: показания счетчика соединений на главной форме сервера приложений должны измениться, так как теперь у сервера приложений два клиента — среда разработки и запущенное приложение (рис. 9).
Если щелкнуть мышью на колонке Table 2 компонента TDBGrid, появится дополнительная форма с detail-записями, соответствующими текущей master-записи. Их также можно редактировать — при выполнении метода ApplyUpdates изменения в них будут сохранены.
При желании можно с помощью утилиты SQL Monitor, запущенной на том же компьютере, что и сервер приложений, проследить, какие именно запросы созданный нами сервер приложений направляет серверу баз данных.
Отметим, что для переноса клиентского приложения на другой компьютер следует вместе с исполняемым файлом скопировать библиотеку dbclient.dll либо в каталог Windows\System, либо в тот же каталог, где находится исполняемый файл. Библиотеку Borland Database Engine на этот компьютер устанавливать не нужно — «тонкий» клиент к ней не обращается.
CORBA-объект, так же как и COM-объект, может обладать произвольным набором свойств и методов, которые можно добавить, редактируя библиотеку типов и код реализации. В частности, можно добавить и метод, осуществляющий доступ к базе данных с помощью пароля. Реализация его может, к примеру, иметь следующий вид:
procedure TMyRDM.Login (username, password: OleVariant); begin Database1.Connected := false; Database1.params.Values [ ' Password' ] := password; Database1.params.Values [ ' User Name ' ] := username; Database1.Connected := true; Table1.Open; Table2.Open; end;
Соответствующий клиентский код имеет в этом случае вид:
procedure TForm1.ButtonLoginClick (Sender :TObject); begin Form1.ClientDataSet1.Close; Form1.MIDASConnection1.AppServer.Login(Edit1.Text, Edit2.Text); Form1.ClientDataSet1.ProviderName := ' Table1 ' ; Form1.ClientDataSet1.Open; end;
Отметим также, что создание CORBA-клиентов в виде активных форм возможно точно так же, как и создание COM-клиентов. Наиболее простым способом можно это сделать, выделив все интерфейсные элементы и сохранив их в виде шаблона (Component/Create ComponentTemplate), который позже можно поместить на вновь созданную активную форму. Далее следует в поставку ActiveX включить dbclient.dll, осуществить эту поставку на какой-нибудь Web-сервер и попробовать протестировать полученный ActiveX (рис. 10).
Как мы убедились, создание CORBA-серверов и клиентов с помощю Delphi очень напоминает создание COM-серверов и клиентов аналогичного назначения. В принципе COM-серверы можно легко превращать в CORBA-серверы, просто выбрав соответствующую опцию в контекстном меню редактора кода и используя, таким образом, одну и ту же реализацию для разных технологий распределенных вычислений.
Отметим, что создание CORBA-серверов и клиентов с помощью C++Builder осуществляется несколько по-другому и напоминает, скорее, не создание COM-серверов и клиентов, а создание DCE-серверов и клиентов с помощью кодогенераторов Entera, то есть традиционный способ генерации клиентского и серверного stub-кода на основе IDL-описания интерфейсов сервера. Об этом будет более подробно рассказано в последующих статьях данного цикла.
Многопользовательская работа в трехзвенных информационных системах
Отметим, что организация многопользовательской работы в трехзвенных системах с CORBA-сервером практически не отличается от организации многопользовательской работы в системах с COM-серверами. В обоих случаях при коллизиях следует создавать обработчик события OnReconcileError компонента TClientDataSet.
Рассмотрим часто встречающийся при многопользовательской работе случай, когда два пользователя пытаются отредактировать одну и ту же запись. При работе с сетевыми версиями настольных СУБД и при работе с серверными СУБД в такой ситуации обычно используется блокировка записи (или нескольких записей, занимающих общую единицу хранения информации, называемую страницей), не позволяющая пользователю редактировать ту запись, которая уже редактируется другим пользователем, пока последний ее не освободит.
В случае трехзвенной системы механизм блокировок, используемый в традиционной двухзвенной модели «клиент/сервер», может оказаться неприемлемым, так как при использовании briefcase model промежуток времени между редактированием записи и сохранением ее в базе данных может быть весьма длительным. Поэтому организация многопользовательской работы в трехзвенных системах отличается от привычной.
При попытке сохранения сервером приложений измененной записи в базе данных производится поиск изменяемой записи либо по ключевому полю, либо по всем полям в зависимости от значения свойства UpdateMode ответственного за этот процесс компонента TDBDataSet на сервере приложений и сравнение всех полей изменяемой записи с исходными значениями (то есть теми, которые были в кэше клиента на момент получения этой записи с сервера до того, как пользователь изменил ее в кэше). Если какие-либо поля за время, прошедшее между получением оригинала записи клиентом и попыткой сохранить изменения, были модифицированы другим пользователем, запись может быть передана обратно в клиентское приложение для дальнейшей обработки пользователем.
Так как данные, предоставляемые компонентом TClientDataSet, представляют собой содержимое кэша, вполне возможно, что несколько пользователей создадут свои локальные копии исходных записей, полученных с сервера баз данных, и каждый начнет их редактировать. Предположим, два пользователя отредактировали одну и ту же запись и пытаются сохранить ее на сервере. В этом случае тот из пользователей, кто пытался первым сохранить в базе данных свой вариант отредактированной записи, сумеет это сделать, тогда как второй пользователь должен быть уведомлен об ошибке, связанной с тем, что сохраняемая им запись уже изменена другим пользователем.
Для обработки подобных ошибок существует событие OnReconcileError компонента TClientDataSet, возникающее в случае, когда при попытке сохранения измененной записи в базе данных выясняется, что запись была изменена другим пользователем. Пример подобной обработки можно найти в репозитарии объектов.
Откроем проект клиентского приложения, созданного ранее. Далее на странице страницы Dialogs репозитария объектов выберем пиктограмму Reconcile Error Dialog. В результате к проекту будет добавлена диалоговая панель следующего вида (рис. 11).
Далее включим ссылку на модуль вновь созданного диалога в секцию uses модуля, связанного с главной формой проекта. Следует также перенести вновь созданную диалоговую панель в список Available Forms на странице Forms диалоговой панели опций проекта. В противном случае этот диалог будет появляться при запуске приложения.
Затем создадим обработчик события OnReconcileError компонента ClientDataSet1:
procedure TForm1.ClientDataSet1ReconcilleError ( DataSet: TClientDataSet; E: EReconcileError; UrdateKind: TUpdateKind; var Action: TReconcileAction); begin Action := HandleReconcileError (DataSet , UpdateKind, E) ; end;
Отметим, что в модуле, связанном с вновь созданным диалогом, уже содержится функция HandleReconcileError, ссылка на которую содержится в обработчике событий.
Скомпилируем и сохраним проект, а затем запустим две копии созданного приложения. В каждой из копий приложения внесем разные изменения в одно и то же поле одной и той же записи и попытаемся сохранить изменения в базе данных. При этом первое изменение с успехом сохранится, а в результате попытки внести второе изменение появится вновь созданная диалоговая панель (рис. 12).
В таблице в нижней части формы содержатся значения полей, вызвавшие коллизию: значение, которое данный пользователь пытается сохранить в базе данных, и исходное значение поля, сгруженное в кэш при открытии TClientDataSet. Пользователь может пропустить эту запись, оказаться от внесения своей версии значения поля в базу данных, переписать его поверх старого значения.
Отметим, что обработка коллизий, возникших при редактировании detail-записей, производится точно так же.
Если по каким-либо причинам обработка коллизий при многопользовательской работе должна быть отличной от предложенной, можно отредактировать имеющийся код (и при необходимости — саму диалоговую панель).
Понятие о stub- и skeleton-объектах
Итак, мы убедились,что создание CORBA-приложений с помощью Delphi на первый взгляд мало отличается от создания COM-приложений. Для обеспечения взаимодействия клиента и сервера, функционирующих в разных адресных пространствах или на разных компьютерах (в том числе и в разных операционных системах), как и в случае других технологий объектно-ориентированных распределенных вычислений, используются объекты, расположенные в адресных пространствах клиента и сервера и обменивающиеся данными между собой. В терминологии CORBA они называются stub и skeleton. Stub — это представитель сервера в адресном пространстве клиента (иногда для его обозначения используют и термин proxy). Skeleton — это представитель клиента в адресном пространстве сервера (рис. 13).
Клиентское приложение взаимодействует со stub-объектом, вызывая его методы (названия которых совпадают с названиями методов серверного объекта). В действительности stub-объект обращается к клиентской части Object Request Broker (ORB), которая, в свою очередь, обращается к специализированному сервису middleware — Smart Agent (он может функционировать на каком-либо из компьютеров сети), представляющему собой не что иное, как directory service, — службу, обеспечивающую поиск доступного сервера, содержащего реализацию запрашиваемого клиентом объекта.
Когда сервер найден, в его адресном пространстве создается запрошенный серверный объект, содержащий, в свою очередь, skeleton-объект, которому ORB передает запрос клиента с помощью Basic Object Adaptor (BOA). Используя эту службу, skeleton регистрирует созданный серверный CORBA-объект с помощью Smart Agent, а также сообщает о доступности, факте создания и о готовности объекта принимать запросы клиента.
Как и в случае COM, stub-объект и skeleton-объект взаимодействуют между собой с помощью маршаллинга, представляющего собой обмен данными (передаваемые данные упаковываются в так называемый marshalling packet и распаковываются после передачи в другое адресное пространство) и передачу указателей на интерфейсы и аргументы функций между этими объектами.
Оба эти объекта (stub и skeleton) в случае использования Delphi 4 создаются автоматически при определении интерфейса CORBA-объектов в редакторе библиотеки типов.
Проиллюстрируем сказанное выше на примере созданнного нами ранее CORBA-сервера. При создании CORBA-объекта (в нашем случае модуля данных) автоматически были сгенерированы два модуля. Первый из них — serv_TLB.pas (так называемый stub-and-skeleton unit) — представляет собой автоматически сгенерированный код классов stub- и skeleton-объектов:
unit serv_TLB // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // WARNING // // — — — - // // The types declared in this file were generated from date read from a // // Type Library. If this type Library is explicitly or indirectly ( via // // another type Library referring to this type Library ) re-imported, or the // // ' Refresh ' command of the Type Library Editor activated while editing the // // Type Library, the contents of this file will be regenerated and all // // manual modifications will be lost. // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // PASTLWTR : $ Revision: 1 . 11 . 1 . 75 $ // File generated on 11 . 01 . 99 16:12:40 from Type Library described below. // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // Type Lib : E:\ Program Files \ Borland \ Delphi4 \ Projects \ CORBA \ serv.tlb // IID\LCID : {42ED5200-A96E-11D2-B185-000000000000} \0 // Helpfile : // HelpString : Project1 Library // Version : 1.0 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // interface user Windows, ActiveX, Classes, Graphics, OleCtrls, StdVCL, SysUtils, CORBAObj, OrbPas, CorbaStd; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // GUIDS declared in the TypeLibrary. Following prefixes are used : // // Type Libraries : LIBID_xxxx // // CoClasses : CLASS_xxxx // // DISPInterfaces : DIID_xxxx // // Non-DISP interfaces : IID_xxxx // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // const LIBID_serv : TGUID = ' {42 ED5200-A96E-11D2-B185-000000000000} ' ; IID-Icrb1 : TGUID = ' {42 ED5201-A96E-11D2-B185-000000000000} ' ; CLASS_crb1 : TGUID = ' {42 ED5203-A96E-11D2-B185-000000000000} ' ; type // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // Forward declaration of interfaces defined in Type Library // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // Icrb1 = interface; Icrb1Disp = dispinterface; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // Declaration of CoClasses defined in Type Library // // ( NOTE : Here we map each CoClass to its Default Interface ) // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // crb1 = Icrb1; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // Interface : Icrb1 // Flags : (4416) Dual OleAutomation Dispatchable // GUID : {42ED5201-A96E-11D2-B185-000000000000} // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // Icrb1 = interface (IDataBroker ) [ ' {42ED5201-A96E-11D2-B185-000000000000} ' ] function Get_Table1 : IProvider ; safecall ; property Table1 : IProvider read Get_Table1 ; end; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // DispIntf : Icrb1Disp // flags : (4416) Dial OleAutomation Dispatchable // GUID : {42ED5201-A96E-11D2-B185-000000000000} // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // Icrb1Disp = dispinterface [ ' {42ED5201-A96E-11D2-B185-000000000000} ' ] property Table1 : IProvider readonly dispid 1; function GetProviderNames : OleVariant ; dispid 22929905; end; Tcrb1Stub = class (TDataBrokerStub, Icrb1) public function Get_Table1 : IProvider; safecall; end; Tcrb1Sceleton = class (TDataBrokerSceleton) private FIntf: Icrb1; public constructor Create (const IntanceName : string; const Impl : IUnknown) ; override; procedure GetImplementation (out Impl : IUnknown) ; override; stdcall; published procedure Get_Table1 (const InBuf : IMarshalInBuffer; Cookie : Pointer) ; end; Cocrb1 = class class function Create : Icrb1; class function CreateRemote (const MachineName : string) : Icrb1; end; Tcrb1CorbaFactory = class class function CreateInstance (const InstanceName : string) : Icrb1; end; implementation uses ComObj; { Tcrb1Stub } function Tcrb1Stub. Get_Table1: IProvider; var OutBuf : IMarshalOutBuffer; InBuf : IMarshalInBuffer; begin FStub.CreateReguest (' Get_Table1 ' , True, OutBuf); FStub.Invoke (OutBuf, InBuf ); Result := UnmarshalObject ( InBuf , IProvider) as IProvider; end; ( Tcrb1Sceleton ) constructor Tcrb1Sceleton.Create (const InstanceName : string; const Impl : IUnknown); begin inherited; inherited InitSceleton ( ' crb1 ' , InstanceName, 'IDL:serv/Icrb1:1.0', tmMultiThreaded, True); FIntf := Impl as Icrb1; end; procedure Tcrb1Sceleton.GetImplementation (out Impl : IUnknown) ; begin Impl := FIntf ; end; procedure Tcrb1Sceleton.Get_Table1 (const InBuf: IMarshalInBuffer; Cookie: Pointer ); var OutBuf : IMarshalOutBuffer; Retval : IProvider; begin Retval := FIntf.Get_Table1; FSceleton.GetReplyBuffer (Cookie, OutBuf) ; MarshalObject ( OutBuf, IProvider, Retval ); end; class function Cocrb1.Create : Icrb1; begin Result := CreateComObject (CLASS_crb1) as Icrb1; end; class function Cocrb1.CreateRemote (const MachineName : string) : Icrb1; begin Result := CreateRemoteComObject (MachineName, CLASS_crb1) as Icrb1; end; class function Tcrb1CorbaFactory.CreateInstance (const InstanceName : string) : Icrb1; begin Result := CorbaFactoryCreateStub ( ' IDL: serv/crb1Factory:1.0' , 'crb1' , InstanceName, ' ' , Icrb1) as Icrb1; end; initialization CorbaStubManager.RegisterStub ( Icrb1, Tcrb1Stub); CorbaInterfaceIDManager.RegisterInterface ( Icrb1, ' IDL: serv/Icrb1:1.0' ); CorbaSceletonManager.RegisterSceleton ( Icrb1, Tcrb1Sceleton) ; end.
В этом модуле определены skeleton-объекты для всех интерфейсов, поддерживаемых данным сервером. Все они являются наследниками класса TCorbaSkeleton и в действительности реализуют маршаллинг — обмен данными между skeleton-объектом и соответствующим stub-объектом клиента. Редактировать этот модуль не рекомендуется.
Второй автоматически сгенерированный модуль (в нашем случае serv2.pas) содержит реализацию методов сервера. Этот файл подлежит редактированию (в нем мы создавали обработчики событий, связанных с созданием и уничтожением экземпляра модуля данных):
unit serv2: interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComObj, VCLCom, StdVcl, BdeProv, DataBkr, CorbaRdm, CorbaObj, serv_Tlb, Db, DBTables; type Tcrb1 = class ( TCorbaDataModule, Icrb1) Table1 : TTable; Table2 : TTable; DataSource1 : TDataSource; procedure crb1Create (Sender :TObject); procedure crb1Destroy (Sender :TObject); private { Private declarations } public { Public declarations } protected function Get_Table1: IProvider; safecall; end; var crb1 : Tcrb1 ; implementation { $R *.DFM} uses serv1, CorbInit, CorbaVcl ; function Tcrb1.Get_Table1: IProvider; begin Result := Table1.IProvider ; end; procedure Tcrb1.crb1Create (Sender :TObject); begin Form1.UpdateCount(1); end; procedure Tcrb1.crb1Destroy (Sender :TObject); Form1.UpdateCount(-1); end; initialization TCorbaVclComponentFactory ( ' crb1Factory ' , ' crb1' , ' IDL: serv/crb1Factory:1.0' , Icrb1, Tcrb1, iMultiInstance, tmSingleThread); end.
Как и в случае редактирования библиотеки типов COM-сервера, при создании новых свойств и методов CORBA-сервера «заготовки» методов генерируются в этом модуле автоматически при выполнении операции его обновления (кнопка Refresh панели инструментов библиотеки типов). Этот же модуль отвечает и за создание для каждого из доступных клиентам интерфейсов объекта TCorbaFactory, создающего или находящего подходящий экземпляр класса реализации описанных методов и передающего его интерфейс соответствующему классу skeleton-объектов.
Назначение Smart Agent
Как было сказано выше, Smart Agent (osagent) представляет собой службу, предназначенную для поиска сервера, содержащего реализацию CORBA-объекта, запрошенного клиентом. Если существует несколько серверов, содержащих подобную реализацию, Smart Agent обеспечивает баланс загрузки, равномерно распределяя клиентов между имеющимися серверами.
Чтобы убедиться в этом, можно запустить два экземпляра созданного ранее сервера и несколько экземпляров клиентского приложения (не возбраняется запустить их на других компьютерах локальной сети) и проанализировать, как меняется значение счетчика подключений (рис. 14).
Отметим, что Smart Agent также способствует восстановлению системы после сбоев, пытаясь либо перезапустить сервер при потере клиентом соединения с ним, либо найти другой подходящий сервер из доступных. Заметим, однако, что при создании систем, устойчивых к сбоям, желательно создавать серверные объекты, не содержащие данных, связанных с конкретным клиентским приложением, и предоставляющие их с помощью вызова соответствующих методов (пример такого кода содержится в статье, посвященной созданию объектов Microsoft Transaction Server).
Подобно Entera Broker, Smart Agent должен функционировать где-либо в локальной сети (не обязательно на том же компьютере, где функционируют сервер или клиенты). Для взаимодействия Smart Agent и ORB-клиента используется протокол UDP, требующий меньше ресурсов, чем TCP/IP.
В сети может одновременно функционировать несколько таких агентов. В этом случае они могут обмениваться информацией о зарегистрированных объектах между собой. При аварийном завершении одного из агентов объекты, зарегистрированные им, автоматически заново регистрируются другими агентами.
Раннее и позднее связывание
Обычно CORBA-клиенты используют раннее, или статическое, связывание при обращении к интерфейсам объектов сервера. В этом случае производительность системы в целом выше, и, кроме того, на этапе компиляции возможен синтаксический контроль имен свойств и методов серверного объекта. Для использования раннего связывания следует добавить модуль, содержащий код классов stub- и skeleton-объектов, к клиентскому приложению.
Однако возможны случаи, когда только на этапе выполнения становится известно, какие именно интерфейсы сервера требуются клиенту. В этом случае возможно так называемое позднее, или динамическое, связывание, для которого включение в клиентское приложение модуля, содержащего код классов stub- и skeleton-объектов, не требуется (именно оно и было использовано в рассмотренном выше примере).
Для реализации позднего связывания обычно нужно либо запустить сервер вручную, либо зарегистрировать интерфейсы данного сервера в репозитарии интерфейсов (Interface Repository). Подобная регистрация позволяет использовать данный сервер клиентам, написанным с помощью любых языков программирования, если таковые поддерживают так называемый dynamic invocation interface (DII) — интерфейс динамических вызовов, а также разрабатывать клиентские приложения, используя зарегистрированные интерфейсы сервера.
Интерфейс динамических вызовов позволяет клиентским приложениям обращаться к серверным объектам без использования классов stub-объектов, но производительность такой системы ниже, чем системы с использованием раннего связывания. При его использовании интерфейсы сервера должны обязательно быть зарегистрированы в репозитарии интерфейсов.
Регистрация интерфейсов и реализаций
При запуске сервера последний информирует ORB о том, какие интерфейсы он реализует. Соответствующий код автоматически добавляется к CORBA-серверу, если он создается с помощью экспертов CORBA Data Module или Corba Object репозитария объектов Delphi 4.
Обычно CORBA-серверы запускаются вручную, как в рассмотренном ранее примере. Однако при необходимости можно организовать автоматический запуск сервера или создание серверных объектов только по запросу клиента. С этой целью используется сервис, который называется Object Activation Daemon (OAD).
Автоматический запуск сервера возможен обычно в том случае, когда, во-первых, имеется регистрационная база данных, содержащая сведения о доступных реализациях подобных серверов, о том, какие серверные объекты могут быть ими созданы и какие интерфейсы в них реализованы, и, во-вторых, когда данный сервер в этой базе данных зарегистрирован. Соответственно, OAD может запустить сервер, если последний зарегистрирован в репозитарии реализаций (Implementation Repository), содержащем сведения о том, в каких приложениях реализованы те или иные объекты.
Если сведения о затребованном объекте имеются в репозитарии реализаций, ORB обращается не к серверу, а к OAD, как если бы он был сервером. Когда клиент запрашивает объект, OAD перенаправляет запрос к настоящему серверу, запуская его, если в этом есть необходимость.
Регистрация в репозитарии интерфейсов
Для регистрации интерфейсов следует создать репозитарий интерфейсов, запустив соответствующий сервер. С этой целью можно использовать сохраненный ранее IDL-файл с описанием интерфейса.
Сервер репозитария интерфейсов запускается с помощью следующей команды:
irep [-console] IRname [file.idl]
Аргументы этой утилиты представлены ниже:
- -console — запустить репозитарий интерфейсов как консольное приложение;
- IRname — имя репозитария интерфейсов;
- file.idl — IDL-файл для описания состава репозитария интерфейсов (необязательный параметр).
Зарегистрируем интерфейс созданного сервера. С этой целью воспользуемся созданным ранее IDL-файлом.
irep myrepos serv.idl
Когда сервер репозитария интерфейсов запущен, можно добавлять в репозитарий другие интерфейсы, выбирая опцию File|Load и новый IDL-файл. Можно также сохранить текущее содержимое репозитария интерфейсов в IDL-файле (опция File|Save). В этом случае при перезапуске сервера репозитария интерфейсов не потребуется вносить изменения в исходный IDL-файл (рис. 15).
Можно также регистрировать дополнительные интерфейсы с помощью утилиты idl2ir:
idl2ir [-ir IRname] {-replace} file.idl
Аргументы этой утилиты представлены ниже:
- -ir IRname — имя репозитария интерфейсов (если оно не указано, интерфейс регистрируется во всех доступных репозитариях);
- -replace — заменять описания методов новыми версиями;
- file.idl — IDL-файл, содержащий описания регистрируемых интерфейсов.
Если сервер репозитария интерфейсов запущен, интерфейсы из него удалить нельзя. Для этого нужно остановить сервер репозитария, создать новый IDL-файл и снова запустить сервер, зарегистрировав созданный IDL-файл при запуске.
Регистрация в репозитарии реализаций с помощью Object Activation Daemon
Для регистрации интерфейсов сервера с помощью Object Activation Daemon (OAD) нужно запустить его где-нибудь в сети с помощью команды
oad [options]
Список опций можно узнать, запустив OAD с ключом /? .
Собственно регистрация интерфейсов производится с помощью утилиты oadutil.
oadutil reg [options]
где
- -i <interface name> — имя IDL-файла;
- -r <repository id> — уникальный идентификатор интерфейса (например, 'IDL:serv/crb1Factory:1.0');
- -o <object name> — имя серверного объекта, поддерживающего интерфейс;
- -cpp <file name>— имя исполняемого файла сервера;
- -host <host name> — имя компьютера, где функционирует OAD (необязательный параметр);
- -d <reference data> — данные, передаваемые серверу при запуске (необязательный параметр);
- -p <policy> — способ многопользовательского доступа (shared, unshared);
- -a arg1 — аргументы командной строки сервера (необязательный параметр) ;
- -e env1 — переменные среды, необходимые для функционирования сервера (необязательный параметр).
Пример:
oadutil reg -r IDL:serv/crb1Factory:1.0 -o crb1 -cpp Serv.exe -p unshared
В результате при успешной регистрации получим примерно следующее сообщение:
- Completed registration of repository_id = IDL:serv/crb1Factory:1.0
- object_name = crb1
- reference data =
- path_name = Serv.exe
- activation_policy = UNSHARED_SERVER
- args = NONE
- env = NONE
- for OAD on host 127.0.0.1
Можно также ликвидировать сведения о сервере с помощью команды:
oadutil unreg [options]
Здесь
- -i <interface name> — имя IDL-файла;
- -r <repository id> — уникальный идентификатор интерфейса (например, 'IDL:serv/crb1Factory:1.0');
- -o <object name> — имя серверного объекта, поддерживающего интерфейс;
- -host <host name> — имя компьютера, где функционирует OAD (необязательный параметр).
После регистрации при попытке запустить клиентское приложение будет инициирован запуск сервера. Если нужно, чтобы сервер выгружался из памяти, когда он больше не нужен ни одному клиенту, Object Activation Daemon должен быть запущен с ключом —k (kill process).
8 Поставка CORBA-приложений
При поставке CORBA-серверов и клиентов следует соблюдать следующие правила:
- Библиотеки ORB, содержащиеся в каталоге Viisibroker\Bin, должны быть установлены как на компьютерах, содержащих серверы, так и на клиентских рабочих станциях.
- Если клиентское приложение использует динамический интерфейс вызовов, следует запустить сервер репозитария интерфейсов и зарегистрировать CORBA-сервер.
- Если необходимо, чтобы запуск сервера был инициирован по запросу клиента, нужно запустить Object Activation Daemon (OAD) хотя бы на одном компьютере локальной сети и зарегистрировать в нем CORBA-сервер.
- Smart Agent (osagent) следует установить хотя бы на один компьютер в локальной сети.
Помимо этого следует позаботиться о следующих переменных окружения:
- PATH — добавить каталог, в котором содержатся библиотеки ORB;
- VBROKER_ADM — указать каталог, в котором содержится репозитарий интерфейсов, OAD и Smart Agent;
- OSAGENT_ADDR — IP-адрес компьютера, где функционирует Smart Agent;
- OSAGENT_PORT — порт, используемый агентом для прослушивания запросов;
- OSAGENT_ADDR_FILE — файл с адресами экземпляров Smart Agent (если их несколько);
- OSAGENT_LOCAL_FILE — файл с информацией о сети для Smart Agent, если он функционирует на компьютере, имеющем более чем один IP-адрес;
- VBROKER_IMPL_PATH — каталог, содержащий репозитарий реализаций;
- VBROKER_IMPL_NAME — имя репозитария реализаций, принятое по умолчанию.
Следующая статья данного цикла будет посвящена созданию CORBA-серверов и CORBA-клиентов с помощью C++Builder. Как уже было сказано выше, методы работы с CORBA в этих средствах разработки имеют ряд принципиальных различий, достойных подробного рассмотрения.
Российское представительство Inprise. Тел.: 7(095)238-36-11 e-mail: elmanova@usa.net
КомпьютерПресс 4'1999