Продолжается подписка на наши издания! Вы не забыли подписаться?

CORBA

Александр Цимбал

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

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

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

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

CORBA является концепцией, а не ее реализацией. Когда мы говорим "COM", то понимаем под этим скорее набор конкретных средств – элементов операционной системы, библиотек, утилит и т.п., являющихся составной частью того, что называется Microsoft Windows. Под термином "CORBA" понимается именно сложная и развитая концепция, сформулированная на уровне специального языка описаний – IDL. Реализации же этой концепции могут сильно отличаться друг от друга по различным критериям, наиболее важным в том или другом случае. VisiBroker (разработки Visigenic/Borland/Inprise/Corel) и Application Server, BEA WebLogic, Iona Orbix, Oracle Application Server и "картриджи" Oracle, IBM BOSS – все эти продукты используют те или иные возможности CORBA.

Под "стандартом" применительно к CORBA понимается то, что официально утверждено консорциумом OMG. Надо сказать, что это очень высокий уровень "легитимности", так как авторитет OMG в компьютерном мире чрезвычайно высок. OMG представляет собой некоммерческую организацию, являющуюся содружеством разработчиков программного обеспечения и его потребителей, объединивших свои усилия для создания спецификаций этой технологии. В настоящий момент в OMG состоит более 800 членов, включая всех сколько-нибудь серьезных производителей программного обеспечения (и даже c недавнего времени Microsoft). Первая спецификация CORBA появилась в 1991 г. Новые возможности официально считаются добавленными в CORBA в момент утверждения соответствующей спецификации. Как правило, в разработке спецификации участвуют крупнейшие специалисты в данной области. Разработка реализации – задача конкретной фирмы. Обычно от утверждения спецификации до появления высококачественной реализации проходит довольно много времени – иногда несколько лет. В настоящий момент стандартизовано отображение языка IDL на 6 языков программирования – Ada, C, C++, Cobol, Java и Smalltalk. Существуют также отображения на Pascal (точнее, Delphi), Perl, Python и еще несколько языков, но они не стандартизованы.

Объекты CORBA можно рассматривать как экземпляры (instances) некоторого метатипа, причем и метатип, и сами объекты существуют вне связи с конкретной программой на конкретном языке. Этот метатип в CORBA называется «интерфейсом».

Интерфейс

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

Интерфейс в CORBA – это логически сгруппированный набор методов и атрибутов. Каждому интерфейсу присваивается имя, уникальное в пределах одной распределенной системы. В отличие от СОМ в CORBA нет бинарного стандарта интерфейсов. Вместо этого существует стандартный язык описаний IDL. Так уж получилось, что языки с названием IDL существуют в трех различных технологиях – OSF/DCE, Microsoft/COM и OMG/CORBA. Эти языки во многом похожи, поскольку предназначены для одного и того же, но OMG/IDL несколько отличается от своих «однофамильцев».

За его основу был взят язык C++ (его описательная часть и директивы препроцессора), поэтому читатель, знакомый с C++, при работе с IDL будет чувствовать себя вполне комфортно.

Вот пример объявления интерфейсов на языке IDL:

exception MyException {};
interface MyBaseInterface
{
   long MyMethod_1(in long i, out string str);
   void MyMethod_2 () raises (MyException);
};
interface MyDerivedInterface : MyBaseInterface {
   octet MyMethod_3 (inout double d);
};

В настоящий момент последним стандартом CORBA является стандарт версии 2.3. В нем понятия «объект» и «интерфейс» связаны, так сказать, отношением «один к одному» – один объект не может поддерживать несколько интерфейсов. В стандарте CORBA 3.0, принятие которого ожидается к концу 2000 г, должна появиться возможность создания объектов, поддерживающих несколько интерфейсов.

С помощью приведенного выше примера определения интерфейса (и, естественно, определенного программного кода) вы можете, предположим, создать 5 объектов типа MyBaseInterface и 10000 объектов MyDerivedInterface. Каждый из этих объектов сопоставлен со своим типом и, кроме этого, имеет свой уникальный идентификатор.

Еще раз повторим – создание вышеуказанных 10005 объектов в общем случае никак не связано с «захватом» ни ресурсов компьютера (в первую очередь памяти), ни сетевых ресурсов.

Сервант

Итак, вы можете создать CORBA-объект и даже установить с ним связь. В общем случае этого совершенно недостаточно, чтобы использовать его в конкретной программе. Функциональность CORBA-объекта недоступна для клиента до тех пор, пока в программе (серверном приложении) не создан объект, который позволяет получить доступ к методам, объявленным в IDL-интерфейсе. Этот объект (реализованный на C++, Java, C, Cobol, Ada, Smalltalk или некоторых других языках) и называется «сервантом».

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

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

Часто говорят, что сервант является «инкарнацией» CORBA-объекта. Связь между сервантами и CORBA-объектами является хотя и строго формализованной, но очень гибкой. Сервант может быть создан раньше или позже CORBA-объекта; один сервант может «обслуживать» как один, так и несколько (иногда сотни тысяч и миллионы) CORBA-объектов. Явное разделение циклов жизни CORBA-объектов и их сервантов (а именно серванты потребляют реальные ресурсы) – один из столпов, на которых базируется очень высокая масштабируемость CORBA-приложений.

Объектная ссылка

Единственная сложность, связанная с пониманием смысла термина «объектная ссылка», состоит в том, что он используется в двух различных смыслах.

Есть объектная ссылка «мира CORBA», которая представляет собой закодированную информацию о CORBA-объекте. Она включает имя хоста, порта TCP/IP (или координаты Репозитария Реализаций), конечно же, уникальный идентификатор данного CORBA-объекта и множество другой информации, позволяющей клиенту установить связь с серверным объектом через границы языков программирования, операционных систем и аппаратных платформ. Операции с объектной ссылкой невозможны для клиента, за исключением того, что клиент может превратить ее в строку и записать в файл или базу данных. Впоследствии кто угодно может считать такую строку и преобразовать ее опять в объектную ссылку.

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

Концептуально переменная типа «объектная ссылка» является указателем на так называемый «proxy-объект», который существует на стороне клиента и обеспечивает выполнение удаленных вызовов. Сам proxy-объект сделан недоступным для программиста; связано это с тем, что его создание – задача не клиентского приложения, а самого ORB’а. Логически с каждым proxy-объектом сопоставлена отдельная объектная ссылка, и под копированием объектной ссылки следует понимать создание как нового proxy-объекта, так и настроенного на него нового «указателя». Разумеется, в реальных реализациях физического копирования proxy-объекта не происходит – как всегда в таких случаях, используется механизм счетчика ссылок.

Очень важно отчетливо понимать, что копирование (или уничтожение) объектных ссылок на стороне клиента влияет исключительно на клиентское приложение. Неправильное ведение счетчика ссылок в самом худшем случае приведет к продолжению физического существования в клиентском приложении ненужного proxy-объекта. Никакого отношения к серверному объекту эти действия не могут иметь в принципе. И создание, и уничтожение сервантов или серверных CORBA-объектов – задача серверного приложения. Философия CORBA состоит в том, чтобы клиент посылал сообщения «установить связь с существующим объектом» и «разорвать с ним связь», а не «создать серверный объект» и «уничтожить его». Разумеется, клиент может инициировать создание Corba-объектов вызвав у удаленного объекта специально предусмотренный для этого программистом (автором объека) метод.

Создание простейшего объекта и его использование

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

Первый этап создания CORBA-приложения – написание всех необходимых IDL-деклараций. В нашем случае IDL-код может выглядеть так:

interface MyInterface {
   long Summa (in long op1, in long op2);
};

Следующий шаг – это генерация файлов на стороне клиента и сервера с помощью компилятора idl2cpp. В качестве входа компилятор получает список idl-файлов. В нашем случае это единственный файл, содержащий вышеприведенное описание. Для файла с именем, например, SimpleIDL.idl будут сгенерированы файлы SimpleIDL_c.hh, SimpleIDL_c.cpp (для использования на стороне клиента) и SimpleIDL_s.hh, SimpleIDL_s.cpp (для использования на стороне сервера).

Создание серверного приложения

Файлы _s.* содержат код, который позволяет связать серверное приложение с CORBA. Для нас наибольший интерес представляет сгенерированный класс POA_MyInterface. Этот класс содержит объявление чисто виртуальной (абстрактной) функции Summa:

class POA_MyInterface : ...
{
public:
   ...
virtual CORBA::Long Summa(CORBA::Long _op1,
                          CORBA::Long _op2)
throw(CORBA::SystemException) = 0;
   ...
};

Поскольку класс POA_MyInterface является только основой для серверного приложения, его часто называют «скелетом» или даже «скелетоном» (skeleton).

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

class MyInterfaceImpl : public POA_MyInterface
{
public:
   MyInterfaceImpl () {}
   CORBA::Long Summa(CORBA::Long _op1,
                     CORBA::Long _op2)
        throw(CORBA::SystemException);
};

CORBA::Long MyInterfaceImpl::Summa(CORBA::Long _op1,
                                   CORBA::Long _op2)
throw(CORBA::SystemException)
{
   return _op1 + _op2;
}

Класс реализаций MyInterfaceImpl часто создается автоматически, например, с помощью экспертов, входящих в состав Borland C++ Builder или Java Builder.

Теперь осталось написать код функции main():

#include <fstream.h>
#include <corba.h>

#include "MyInterfaceImpl.h"

#pragma argsused
main(int argc, char* argv[])
{
   try
   {
       // Инициализация взаимодействия с CORBA
      CORBA::ORB_var orb =
          CORBA::ORB_init(argc, argv);
       // Создание серванта будущего CORBA-объекта
       MyInterfaceImpl* servant =
              new MyInterfaceImpl;
       // Создание временного (transient)
       // CORBA-объекта и получение объектной ссылки
       CORBA::Object_ptr oRef = servant->_this();
       // Преобразование объектной ссылки в строку
       CORBA::String_var str =
               orb->object_to_string (oRef);
       // Запись в файл
       ofstream oref_file ("MyORef.ior");
       oref_file.write (str, strlen(str)+1);
       oref_file.close();
       cout << "Waiting for client requests...";
       // Цикл ожидания запросов клиентов
       orb->run();
   }
   catch(const CORBA::Exception& e)
   {
       cerr << e << endl;
       return(1);
   }
   return 0;
}

Некоторые пояснения. Создание серверного объекта с помощью метода _this() применяется довольно редко. Получаемый таким образом объект имеет совокупность характеристик, крайне затрудняющих его использование. В разделе, посвященном объектным адаптерам, будет рассказано, как создавать «нормальные» CORBA-объекты.

Результатом вызова метода _this() является объектная ссылка. Тип MyInterface определяет proxy-объект, MyInterface_ptr (или MyInterface_var) – указатель на него. Это первый вид объектной ссылки – на уровне приложения.

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

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

Обратите внимание на то, что одно и тоже приложение может быть как клиентом, так и сервером – CORBA в этом плане не накладывает никаких ограничений.

Создание клиентского приложения

Файлы _c.* содержат все необходимое для взаимодействия клиентского приложения с серверным через инфраструктуру CORBA. Ничего дописывать не надо – используйте это код «как есть». Часто либо совокупность этих файлов, либо некоторые из них называют «заглушкой» (stub).

//—————————————————————————————————————-
#include <fstream.h>
#include <corba.h>

#include "SimpleIDL_c.hh"

#pragma argsused
main(int argc, char* argv[])
{
   int op1, op2;
   try
   {
      if (argc == 3)
      {
          op1 = atoi (argv[1]);
          op2 = atoi (argv[2]);
      }
      else
      {
          cout <<
             "Command string: client arg1 arg2\n";
          return 1;
      }
      ifstream inp_file ("MyORef.ior");
      if (!inp_file)
      {
          cout << "File MyORef.ior not found.\n";
          return 1;
      }
      // Чтение объектной ссылки в строковом
      // виде из файла
      char str[1000];
      inp_file >> str;
      inp_file.close();
      // Инициализация взаимодействия с CORBA
      CORBA::ORB_var orb =
              CORBA::ORB_init(argc, argv);
      // Получение объектной ссылки преобразованием
      // из строки
      CORBA::Object_var obj =
      orb->string_to_object (str);
      MyInterface_var oRef =
                MyInterface::_narrow (obj);
      if (CORBA::is_nil(oRef))
      {
         cout << "Failure during getting of object"
                 " reference\n";
      }
      else
      {
         // Вызов удаленного метода
         int res = oRef->Summa (op1, op2);
         cout << op1 << " + " << op2 << " = "
              << res << endl;
      }
   }
   catch(const CORBA::Exception& e)
   {
       cerr << e << endl;
       return(1);
   }
   return 0;
}

Вот, собственно, и все.

Заметим, что этот пример при использовании C++ Builder 4/5 можно бы написать с еще меньшим количеством кода, но для работы нам потребовалось бы предварительно запустить некий компонент CORBA, поставляемый вместе с VisiBroker – так называемый «smart agent». Разговор о нем еще впереди.

Создание либо клиента, либо сервера, либо их обоих на другом языке просто потребовало бы использования не компилятора idl2cpp, а компиляторов idl2java, idl2ada и т.д. Разумеется, нет никаких препятствий для написания, например, сервера на C++ для SPARC/Solaris, а клиента – на Java под Windows. Учет особенностей архитектур (например, различного порядка байтов в машинном слове) происходит автоматически – за это отвечает ORB.

Управление объектами

Созданный в предыдущем примере объект (не сервант, а сам CORBA-объект) является объектом временным. Это означает, что он не может существовать дольше, чем существует компонент CORBA, который его создал. В рассмотренном примере этот компонент явно не присутствовал в программе, и такой способ создания CORBA-объекта часто называют «неявным». В реальных программах большинство CORBA-объектов создается другим путем. Данный раздел и посвящен рассмотрению связанных с этой темой вопросов.

Объектные адаптеры

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

Стандарт CORBA позволяет иметь и использовать (причем одновременно) несколько различных объектных адаптеров. В настоящее время существуют два стандартных объектных адаптера – BOA (Basic Object Adapter) и POA (Portable Object Adapter). Использование BOA признано устаревшим, так как это не позволяет обеспечить переносимость серверных CORBA-приложений, и мы о нем говорить не будем.

POA

В реальных CORBA-приложениях используется древовидная иерархия объектных адаптеров. В корне ее находится так называемый Root POA – объектный адаптер по умолчанию. Именно он создавал наш CORBA-объект в рассмотренном ранее примере.

Программист получает доступ к Root POA c помощью стандартного кода, используемого во многих случаях:

CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);
CORBA::Object_var rpObj =
    orb->resolve_initial_references("RootPOA");
PortableServer::POA_var rootPoa  =
PortableServer::POA::_narrow(rpObj);

Почему практически всегда недостаточно RootPOA, и программисты должны создавать дополнительные POA? Причина состоит в том, что каждый POA создается с определенным набором свойств, и после этого он способен создавать объекты и серванты только определенного вида – например, только временные или только «долгоживущие» (persistent).

Дочерние POA создаются с помощью обращения к уже созданным POA как к фабрикам. Имейте в виду, что дочерний POA не наследует свойств своего базового POA – набор свойств для каждого создаваемого объектного адаптера нужно указывать явно. Например, если вы хотите создавать «долгоживущие» объекты, то сначала нужно создать соответствующий POA.

Перед созданием дочерних POA желательно (хотя и не обязательно) создать так называемый «менеджер» POA. Он отвечает за распределение клиентских запросов между сервантами, находящимися под управлением различных POA, а также за управление их (POA) циклом жизни. Фабрикой такого менеджера может являться Root POA. При создании дочерних объектных адаптеров вы указываете менеджер POA в качестве аргумента. Если вы не создали свой менеджер и вместо его имени при вызове метода создания POA указали nil, то будет неявно создан и использован менеджер по умолчанию.

PortableServer::POAManager_var poaMngr = rootPoa->the_POAManager();
CORBA::PolicyList policyList;
policyList.length(1);
policyList[0] =
rootPoa->create_lifespan_policy(PortableServer::PERSISTENT);
PortableServer::POA_var myPOA = rootPoa->create_POA(
                             "MyPOA", poaMngr, policyList);

Процесс уничтожения объектных адаптеров происходит в определенном порядке – сначала дочерние POA, затем их «родители».

Создание объекта с использованием POA

Приведем пример создания объекта с использованием myPOA. Как уже говорилось, с каждым CORBA-объектом нужно сопоставить «ключ» – идентификатор, который позволяет однозначно идентифицировать этот объект. Давайте зададим этот идентификатор явно. Для этого вызовем метод, который позволяет создать этот идентификатор на основе строки:8

PortableServer::ObjectId_var objID =
    PortableServer::string_to_ObjectId ("MyObject");

Следующие две команды создают сервант, а затем и CORBA-объект с указанным ObjectID:

MyInterfaceImpl servant;
myPOA->activate_object_with_id (objID, &servant);

Наконец, для начала обработки запросов от клиента вы должны активизировать менеджер POA:

poaMngr->activate();

Практику, когда для каждого CORBA-объекта при запуске приложения создается свой сервант, нельзя признать удачной – так можно «съесть» любое количество ресурсов. Ранее говорилось, что разработчик может создавать CORBA-объекты и без создания сервантов. Сделать это очень просто:

myPOA->create_reference_with_id(
           objID, "IDL:MyInterface:1.0");

Сопоставить с таким объектом сервант можно позднее, причем самыми различными способами (об этом будет рассказано позже).

Временные и долгоживущие объекты

Давайте теперь поговорим о том, что такое «уничтожение» объекта и как ведут себя временные объекты.

В CORBA есть команды создания объектов, но нет команды их уничтожения. Сделано это потому, что крайне трудно найти конкретный компонент CORBA, который взял бы на себя «труд» надежного и безопасного уничтожения объектов. В результате этого, под уничтожением объекта понимается некоторая последовательность действий (или отсутствие каких-либо действий), которые приводят к тому, что ни один клиент по имеющимся у него объектным ссылкам не может получить доступ к CORBA-объекту.

Рассмотрим, что происходит в случае временного объекта, созданного ранее с помощью вызова метода _this(). Объектная ссылка на этот объект, переданная клиенту через файл, содержит в себя имя хоста, на котором было запущено создавшее объект приложение, использованный порт TCP/IP, уникальный (автоматически созданный) идентификатор объекта и уникальный (автоматически созданный) идентификатор POA, который тоже является CORBA-объектом, хотя и с особенностями. Это означает, что клиент может использовать имеющуюся у него объектную ссылку только до тех пор, пока серверное приложение запущено и в нем «живет» создавший объект объектный адаптер.

При остановке и последующем запуске этого же приложения на том же компьютере будет создан новый CORBA-объект – его собственный идентификатор и идентификатор его POA будут отличаться от тех, данные для которых находятся в «старой» объектной ссылке. Это означает, что по ней получить доступ к «старому» объекту в принципе невозможно. В этом случае и говорят, что CORBA-объект больше не существует. Клиент может «выбросить» имеющуюся у него объектную ссылку – она больше никогда не понадобится.

В случае использования долгоживущих объектов дело обстоит сложнее. Даже если объект создан с помощью POA, предназначенного для создания и управления persistent-объектами, это не значит, что после остановки серверного приложения клиент сможет вызвать удаленные методы по имеющейся у него объектной ссылке. В простейшем случае, никаких проблем не будет, если серверное приложение опять запущено «вручную» на том же хосте и с использованием того же порта TCP/IP. Рассмотрение более общего случая выходит за рамки данной статьи. Автоматический запуск серверов приложений будет рассмотрен в разделе, посвященном Репозитарию Реализаций.

Свойства POA

Возможность явного указания того, является ли CORBA-объект временным или долгоживущим – это только одна из стандартных возможностей POA. Определяется она (на языке IDL) следующим образом:

module PortableServer
{
 enum LifeSpanPolicyValue { TRANSIENT, PERSISTENT };
 interface LifeSpanPolicy : CORBA::Policy
 {
      readonly attribute LifeSpanPolicyValue;
 };

Вот список всех остальных:

Режим управления заданием идентификатора объекта (IdAssignmentPolicyValue). Возможные значения – USER_ID или SYSTEM_ID. В первом случае идентификатор объекта задает сам пользователь, во втором – он автоматически генерируется POA.

Режим поддержки одним сервантом нескольких CORBA-объектов (IdUniquenessPolicyValue). Возможные значения – UNIQUE_ID (один сервант является инкарнацией только одного CORBA-объекта) и MULTIPLE_ID (один сервант может обслуживать несколько объектов).

Режим разрешения неявной активации (ImplicitActivationPolicyValue). Возможные значения – IMPLICIT_ACTIVATION (неявное создание CORBA-объекта, например, с помощью _this(), разрешено) и NO_IMPLICIT_ACTIVATION (необходимо создавать CORBA-объекты явно).

Режим создания и управления сервантами (RequestProcessingPolicyValue). Здесь нужно дать более подробные пояснения.

В принципе, возможны только два режима создания сервантов: серванты создаются в программе либо явно (как в ранее рассмотренных примерах), либо в процессе поступления клиентских запросов – возможно, даже для обслуживания одного-единственного запроса. POA обеспечивает поддержку обоих режимов.

Во-первых, POA может содержать так называемый Active Object Map (AOM), т.е. массив указателей на уже созданные серванты. «Индексом» этого массива является значение Object ID. В этот массив могут попадать как серванты, созданные явно программистом, так и серванты, динамически созданные самим объектным адаптером.

В случае динамического создания сервантов предусмотрено два отдельных режима – Activation-режим и Location-режим. Названия их, на мой взгляд, выбраны очень странным образом. Activation-режим заключается в том, что при поступлении клиентского запроса POA сначала ищет подходящий сервант в AOM, и только если таковой не найден, этот сервант динамически создается POA, а затем указатель на него помещается в AOM. Location-режим (явно вопреки своему названию) не «смотрит» в AOM – AOM вообще не поддерживается в этом режиме – а создает сервант для обслуживания пришедшего вызова, а затем уничтожает его.

Процесс динамического создания сервантов заключается в том, что POA вызывает некие написанные программистом функции. Естественно, такие функции необходимо зарегистрировать в POA – для этого предусмотрены специальные команды.

Наконец, предусмотрен некий вспомогательный, но в некоторых случаях очень удобный режим – так называемый «сервант по умолчанию». Суть этого режима состоит в том, что программист явно создает один-единственный сервант и регистрирует его как сервант, который является инкарнацией всех CORBA-объектов. Вот пример ситуации, когда такой подход вполне оправдан: для взаимодействия с базой данных вы создаете сотни, тысячи или миллионы CORBA-объектов – по одному на каждую запись, а затем создаете один сервант, не имеющий своего состояния, который обслуживает все эти объекты. Состояние каждого CORBA-объекта просто извлекается из соответствующей записи базы данных.

Итак, возможные значения опции RequestProcessingPolicyValue:

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

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

class MyInterfaceActivator_Impl : public
       PortableServer::ServantActivator
{
public:
    MyInterfaceActivator_Impl();
    virtual PortableServer::Servant incarnate(
        const PortableServer::ObjectId& oid,
        PortableServer::POA_ptr poa)
           throw(CORBA::SystemException,
                 ortableServer::ForwardRequest);
    virtual void etherealize (
        const PortableServer::ObjectId& oid,
        PortableServer::POA_ptr poa,
        PortableServer::Servant servant,
        CORBA::Boolean cleanup_in_progress,
        CORBA::Boolean remaining_activations)
           throw (CORBA::SystemException);
};

Нетрудно догадаться, что методы incarnate() и etherealize() объявлены как чисто виртуальные в стандартном классе PortableServer::ServantActivator. Ваша задача – создать их реализацию, например, так:

PortableServer::Servant
MyInterfaceActivator_Impl::incarnate(
      const PortableServer::ObjectId& oid,
      PortableServer::POA_ptr poa)
          throw(CORBA::SystemException,
                PortableServer::ForwardRequest)
{
   return new MyInterfaceImpl();
}

void MyInterfaceActivator_Impl::etherealize(
      const PortableServer::ObjectId& oid,
      PortableServer::POA_ptr poa,
      PortableServer::Servant servant,
      CORBA::Boolean cleanup_in_progress,
      CORBA::Boolean remaining_activations)
          throw (CORBA::SystemException)
{
   delete servant;
}

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

Ниже приведен код, который создает POA с поддержкой требуемого режима управления POA, два CORBA-объекта и регистрирует ранее созданный класс как менеджер сервантов (не путать с менеджером POA!):

CORBA::ORB_var orb = CORBA::ORB_init (argc, argv);
CORBA::Object_var rpObj =
    orb->resolve_initial_references ("RootPOA");
PortableServer::POA_var rootPoa  =
    PortableServer::POA::_narrow (rpObj);
PortableServer::POAManager_var poaMngr =
    rootPoa->the_POAManager();
CORBA::PolicyList policyList;
policyList.length(3);
policyList[0] = rootPoa->create_lifespan_policy
    (PortableServer::PERSISTENT);
policyList[1] =
    rootPoa->create_request_processing_policy(
        PortableServer::USE_SERVANT_MANAGER);
policyList[2] =
    rootPoa->create_servant_retention_policy(
       PortableServer::RETAIN);
PortableServer::POA_var myPOA =
    rootPoa->create_POA ("MyPOA", poaMngr,
        policyList);
PortableServer::ObjectId_var objID_1 =
  PortableServer::string_to_ObjectId ("MyObject_1");
myPOA->create_reference_with_id (objID_1,
     "IDL:MyInterface:1.0");
PortableServer::ObjectId_var objID_2 =
  PortableServer::string_to_ObjectId ("MyObject_2");
myPOA->create_reference_with_id (objID_2,
    "IDL:MyInterface:1.0");
MyInterfaceActivator_Impl* manager =
    new MyInterfaceActivator_Impl;
myPOA->set_servant_manager (manager);
poaMngr->activate();

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

Location-режим имеет много общего с Activation-режимом; тем не менее, приведем фрагменты кода соответствующего примера:

class MyInterfaceLocator_Impl :
    public PortableServer::ServantLocator
{
public:
    MyInterfaceLocator_Impl();
    virtual PortableServer::Servant preinvoke(
       const PortableServer::ObjectId& oid,
       PortableServer::POA_ptr poa,
       const char* operation,
       void*& cookie)
          throw (CORBA::SystemException,
              PortableServer::ForwardRequest);
    virtual void postinvoke(
        const PortableServer::ObjectId& oid,
        PortableServer::POA_ptr poa,
        const char* operation,
        void* cookie,
        PortableServer::Servant servant)
           throw (CORBA::SystemException);
};
PortableServer::Servant
MyInterfaceLocator_Impl::preinvoke(
        const PortableServer::ObjectId& oid,
        PortableServer::POA_ptr poa,
        const char* operation,
        void*& cookie)
           throw (CORBA::SystemException,
              PortableServer::ForwardRequest)
{
   cookie = new char[11];
   strcpy(reinterpret_cast<char*>(cookie),
          "Activation");
   return new MyInterfaceImpl();
}
void MyInterfaceLocator_Impl::postinvoke(
         const PortableServer::ObjectId& oid,
         PortableServer::POA_ptr poa,
         const char* operation,
         void* cookie,
         PortableServer::Servant servant)
            throw (CORBA::SystemException)
{
   cout << reinterpret_cast<char*>(cookie) <<
           " and deactivation" << endl;
   delete cookie;
   delete servant;
}

POA нужно создавать со следующим набором свойств:

CORBA::PolicyList policyList;
policyList.length(3);
policyList[0] = rootPoa->create_lifespan_policy(
   PortableServer::PERSISTENT);
policyList[1] =
   rootPoa->create_request_processing_policy(
      PortableServer::USE_SERVANT_MANAGER);
policyList[2] =
   rootPoa->create_servant_retention_policy(
      PortableServer::NON_RETAIN);
PortableServer::POA_var myPOA =
   rootPoa->create_POA ("MyPOA", poaMngr,
      policyList);
   ...
// создание объектов (как в предыдущем примере)
MyInterfaceLocator_Impl* manager =
   new MyInterfaceLocator_Impl;
myPOA->set_servant_manager (manager);

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

Код приложения с сервантом по умолчанию очень прост. Здесь нет никаких callback-методов, и все, что нужно сделать – это создать POA в нужном режиме, затем единственный сервант, а затем зарегистрировать этот сервант:

CORBA::PolicyList policyList;
policyList.length(3);
policyList[0] = rootPoa->create_lifespan_policy(
   PortableServer::PERSISTENT);
policyList[1] =
   rootPoa->create_request_processing_policy(
      PortableServer::USE_DEFAULT_SERVANT);
policyList[2] =
   rootPoa->create_id_uniqueness_policy(
      PortableServer::MULTIPLE_ID);
PortableServer::POA_var myPOA =
   rootPoa->create_POA ("MyPOA", poaMngr,
      policyList);
   ...
// создание объектов (как в предыдущем примере)
MyInterfaceImpl* objImpl = new MyInterfaceImpl;
myPOA->set_servant (objImpl);

Использование счетчика ссылок сервантов

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

delete servant;

подразумевает, что этот же сервант не зарегистрирован в других POA и/или к нему нет обращений из других потоков. Кроме того, даже в случае соблюдения этих условий, возможны проблемы при попытке выполнить эту команду внутри одного из методов объекта. Дело в том, что запись о серванте в Active Object Map остается до тех пор, пока не выполнен до конца хотя бы один метод этого объекта. Ошибка может произойти при последующей попытке удаления записи из Active Object Map, так как с ней уже не сопоставлен реальный сервант.

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

Наш класс реализации, если помните, наследовал класс POA_MyInterface, сгенерированный компилятором idl2cpp. В свою очередь, POA_MyInterface построен на базе стандартного класса PortableServer::ServantBase:

class  POA_MyInterface : public virtual
   PortableServer::ServantBase
{
   ...
}

В классе PortableServer::ServantBase объявлены и реализованы тривиальным образом функции _add_ref() и _remove_ref():

class PortableServer::ServantBase
{
   ...
public:
   virtual void _add_ref () {};
   virtual void _remove_ref () {};
   ...
};

При попытке удаления серванта POA ВСЕГДА выполняет команду _remove_ref(), которая логически подразумевает уменьшение счетчика ссылок на сервант с удалением его при достижении этим счетчиком значения 0. Для стандартных методов – таких, как _this() – выполняется вызов метода _add_ref(). Поскольку по умолчанию ни _add_ref(), ни _remove_ref() не делают ничего, вы можете не подозревать об их присутствии.

Если же вы хотите использовать счетчик ссылок на серванты, избежав тем самым упомянутых в начале раздела проблем, вы можете в качестве базового класса для POA_MyInterface вместо PortableServer::ServantBase указать стандартный класс PortableServer::RefCountServantBase:

class PortableSever_RefCountServantBase : public
   PortableServer_ServantBase
{
   public:
      ...
      virtual void _add_ref ();
      virtual void _remove_ref ();
};

В этом классе реализован реальный, а не фиктивный счетчик ссылок. Нужно иметь при этом в виду, что вы обязуетесь, во-первых, создавать серванты только в динамической памяти, а во-вторых, при их удалении вместо delete servant; всегда использовать servant->_remove_ref().

Установка связи между клиентом и серверным объектом

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

Еще раз напомним: в CORBA клиент считает, что все необходимые серверные объекты уже существуют, поэтому он (логически) всегда использует команду «найти» и никогда – команду «создать».

В CORBA применяются два основных стандартных способа, позволяющих сделать серверный объект доступным для клиента, то есть, передать клиенту объектную ссылку на этот объект. Один из этих способов связан с использованием Сервиса Имен (Naming Service), второй – Трейдер-Сервиса (Trader Service).

Кроме стандартных служб, многие производители разрабатывают и поставляют свои собственные (хотя и нестандартные, но зачастую очень удобные) средства поиска. Для VisiBroker’а таким средством является Location Service, который базируется на использовании Smart Agent. Поскольку VisiBroker является одной из самых распространенных (по крайней мере, в нашей стране) реализацией ORB, мы кратко рассмотрим и этот способ – более того, мы именно с него и начнем в силу простоты его использования.

Использование Inprise Location Service

Location Service представляет собой некую программную среду, которая создается в локальной сети (или в совокупности соединенных между собой локальных сетей) с целью упрощения поиска клиентом нужного серверного объекта. Для ее использования нужно:

Smart agent может:

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

При запуске smart agent’а используется фиксированный порт TCP. Для VisiBroker номер порта, используемого по умолчанию, может быть задан с помощью специальных утилит, и для хранения его значения используются специальные переменные среды. Для Windows это значение прописывается еще и в реестре.

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

prompt> Server -Dvbroker.agent.port=14010

Применительно к Location Service существует понятие «домен», т.е. совокупность smart agent’ов и приложений, использующих для связи между собой один и тот же порт. Заметим, что в одной локальной сети могут одновременно и независимо друг от друга существовать несколько доменов.

Как должен выглядеть код клиентского и серверного приложения при использовании Location Service?

Прежде всего, на стороне сервера разработчик должен выбрать режим регистрации серверных объектов в Location Service. Возможны два варианта, и для обоих создаваемый объект должен быть persistent-объектом.

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

CORBA::PolicyList policyList;
policyList.length(2);
policyList[0] = rootPoa->create_lifespan_policy(
   PortableServer::PERSISTENT);
PortableServer::POA_var myPOA =
rootPoa->create_POA ("MyPOA", poaMngr, policyList);
MyInterfaceImpl servant;
PortableServer::ObjectId_var objID =
   PortableServer::string_to_ObjectId ("MyObject");
myPOA->activate_object_with_id (objID, &servant);
poaMngr->activate();

Клиент может получить объектную ссылку следующим образом:

PortableServer::ObjectId_var objID =
   PortableServer::string_to_ObjectId("MyObject");
MyInterface_var objRef =
   MyInterface::_bind ("/MyPOA", objID);

Эта версия метода _bind содержит пять аргументов, из них два обязательных. Первый из них задает нужный POA (POA разделяются символом ’/’). Второй – идентификатор объекта.

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

Второй способ – регистрация не объектного адаптера, а каждого объекта отдельно:

CORBA::PolicyList policyList;
policyList.length(2);
policyList[0] = rootPoa->create_lifespan_policy(PortableServer::PERSISTENT);
CORBA::Any any;
// BY_POA по умолчанию
any <<= PortableServerExt::BY_INSTANCE;
policyList[1] = orb->create_policy(
   PortableServerExt::BIND_SUPPORT_POLICY_TYPE, any);
   ...

// то же, что и раньше

Код клиента выглядит даже проще:

MyInterface_var objRef =  MyInterface::_bind ("MyObject");

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

Достоинством Location Service является простота и удобство его использования. Кроме того, он обеспечивает некоторые возможности распределения нагрузки между имеющимися серверами приложений, а также устойчивость к сбоям. Особенно эффектно последняя возможность реализуется в случае запуска нескольких одинаковых серверов приложений, которые используют объекты без состояния. В этом случае как «падение» некоторых из серверов приложений, так и «гибель» нескольких smart agent’ов для клиента остаются просто незамеченными.

Использование Naming Service

Naming Service, в отличие от Location Service, является стандартным сервисом CORBA. Он позволяет сопоставлять с серверными объектами произвольные имена. Таким образом, вместо использования преобразованных к строковому виду объектных ссылок или, в лучшем случае, строковых представлений идентификаторов объектов (Object ID), программисты используют произвольные и (желательно) осмысленные имена. Теперь клиент не должен знать ничего об объекте, кроме его имени и местонахождения экземпляра службы имен, который и сопоставляет с именем реальную объектную ссылку.

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

Что понимается под «экземпляром» службы имен?

Это просто сервант некоторого CORBA-объекта, который реализует несколько стандартных интерфейсов, IDL-декларации которых и составляют CORBA-часть Naming Service. Остальная часть – это (при использовании C++) h-файлы и библиотеки, в которых хранится код стандартных методов. Как и любой сервант, экземпляр Naming Service должен быть создан и сопоставлен с конкретным CORBA-объектом, поэтому для использования возможностей Naming Service вам необходимо запустить некое приложение – фабрику соответствующего объекта и его серванта.

Так как экземпляр Службы Имен сам является CORBA-объектом, то нет никаких препятствий для того, чтобы один такой экземпляр содержал ссылку на другой. Такие Службы часто называют «объединенными» (federated).

Этот объект позволяет построить стройную систему имен. В большинстве случкаев такая систему имен представляет из себя дереве или лес, но впринципе она может являться графом более общего вида. Любая из вершин этого графа (дерева) может рассматриваться как корень для начала поиска. Naming Service CORBA не предусматривает наличия общего корня – таких корней может быть несколько. Вообще говоря, клиент должен знать, с какого корня по иерархии нужно начать поиск; в процессе поиска клиент может получить не только имена объектов, но и сопоставленный с каждым именем краткий комментарий в виде строки. Можно представить ситуацию, когда клиент принимает решение о том, какой же, собственно, объект ему нужен, на основании этой дополнительной информации, обеспечиваемой Сервисом Именования.

В общем случае структура, создаваемая с помощью стандартных методов Naming Service, очень похожа на файловую структуру. Имена объектов всегда являются «листьями» дерева. Можно провести следующую аналогию: корневые контексты – это логические диски, контексты имен – это каталоги, а имена объектов – это файлы. Как и для файловой системы, справедливы следующее: контекст имен может содержать произвольное число других контекстов и имен объектов, причем имена контекстов или объектов могут повторяться, но только если повторяющиеся имена находятся на разных путях.

В настоящий момент стандартом Naming Service является спецификация, утвержденная в 1995Кг. Вы можете найти ее, как и все другие спецификации, на сайте www.omg.org, в документе 97-12-10.pdf. Тем не менее, в самое ближайшее время будет утверждена новая спецификация, и Naming Service будет переименован в Interoperable Naming Service (INS). Его предварительная спецификация содержится в файле 98-10-11.pdf.

В связи с тем, что INS является расширением NS (за исключением так называемых «библиотек имен»), а также потому, что уже существуют реализации INS, мы в приводимых примерах будем использовать код в новом стиле.

Какие же преимущества предоставляет новая спецификация?

Во-первых, она позволяет клиенту для доступа к экземплярам служб использовать URL-имена.

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

В-третьих, она существенно повышает удобство работы с именами.

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

Данная статья, естественно, не ставит задачу подробного изложения INS – для этого потребовалось бы немало места. Мы просто приведем небольшой пример. В этом приложении создается все тот же объект, реализующий интерфейс MyInterface, но клиент получает объектную ссылку на него с помощью Naming Service.

Поставим себе следующую задачу: мы создадим небольшую иерархию имен. Корневой контекст содержит контекст с именем «OtherContext» и с комментарием «Context». Этот контекст, в свою очередь, содержит ссылку на нужный нам серверный объект. С этим объектом сопоставим имя «NamedObject» с комментарием «Object Reference».

CORBA::Object_var nsv;
// Root Context в стиле INS
CosNaming::NamingContextExt_var rootN;
try
{
   nsv = orb->resolve_initial_references("NameService");
   rootN = CosNaming::NamingContextExt::_narrow(nsv.in());
   if (CORBA::is_nil(rootN))
   {
      Cout << "Не найден root контекст" << endl;
      // обработка ошибок пропущена
   }
 
   // Создание контекста имен в стиле NS - это
   // необходимо для вызова стандартных методов
   // ... создание его имени ...
   CosNaming::Name_var nc = rootN->to_name ("OtherContext.Context");
   // ... создание нового пустого контекста в
   // контексте rootN ...
   CosNaming::NamingContext_var ct = rootN->new_context ();
   // ... сопоставление имени и нового контекста с
   // перезаписью старого имени, если такое уже было
   rootN->rebind_context (nc, ct);
   // ... и преобразование созданного контекста к
   // стилю INS
   CosNaming::NamingContextExt_var firstCtx =
         CosNaming::NamingContextExt::_narrow (ct);
   //   Создание имени, сопоставленного с объектом
   CosNaming::Name_var oc = rootN->to_name ("NamedObject.Object Reference");
   // ... и одновременное создание контекста в
   // контексте firstCtx с сопоставлением с новым
   // контекстом имени объекта, объектной ссылки (в
   // режиме перезаписи старого имени)
   firstCtx->rebind(oc, myPOA->servant_to_reference(&servant));

Метод servant_to_reference() позволяет вам получить объектную ссылку для уже существующего или вновь создаваемого в неявном режиме CORBA-объекта.

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

Теперь посмотрим, что нужно написать на стороне клиента. К счастью, там все гораздо проще:

CORBA::Object_var nsv = orb->resolve_initial_references ("NameService");
CosNaming::NamingContextExt_var rootN =
          CosNaming::NamingContextExt::_narrow (nsv.in());
if(CORBA::is_nil(rootN))
{
   Cout << "Не найден root контекст" << endl;
   // обработка ошибок пропущена
}
CosNaming::Name_var nc = rootN->to_name("OtherContext.Context"
     "/NamedObject.Object Reference");
CORBA::Object_ptr o = rootN->resolve(nc);
MyInterface_ptr oRef = MyInterface::_narrow (o);

Осталось ответить на вопрос: какой контекст возвращает метод resolve_initial_references()?

Поскольку общего стандартного единого Root-контекста, как уже говорилось, не существует, то корневой контекст должен быть задан, например, в качестве аргумента командной строки при запуске серверного и клиентского приложений. Существует два вида задания Root-контекста – «в стиле NS» и «в стиле INS».

В стиле NS запуск экземпляра Службы Имен может выглядеть так:

start NameExtF [-SVCnameroot <root_name>]  factory_name log_name, где:

<root_name> – имя контекста, который будет рассматриваться как корневой;

factory_name – имя создаваемого при запуске NameExtF CORBA-объекта;

log_name – имя файла, из которого при запуске считывается предыдущее состояние иерархии имен, а при остановке сохраняется текущее состояние.

Тот же ключ – -SVCnameroot – должен использоваться как параметр командной строки при запуске приложений, которые используют данный экземпляр службы имен.

В стиле INS и экземпляр службы имен, и параметры командной строки задаются по-другому. Предупреждаем вас: это реализация для VisiBroker еще не утвержденной спецификации, поэтому, как говорится, «возможны варианты». Пример запуска экземпляра:

nameserv -J-Dvbroker.se.iiop_tp.scm.iiop_tp.listener.port=22000

Такая команда приведет к запуску экземпляра службы имен, и имя созданного CORBA-объекта будет «NameService». Если вы хотите использовать другое имя, укажите его после списка опций.

Ключ -J говорит, что последующий параметр просто передается как аргумент командной строки при запуске виртуальной машины Java. Да, это так – Inprise старается все стандартные средства писать на Java. Это, кстати, относится и к компилятору idl2cpp. Это обеспечивает простую переносимость средств на все платформы, которые поддерживает VisiBroker.

В командной строке приложения, использующего INS, нужно указать, к какому CORBA-объекту (т.е. экземпляру службы имен) вы хотите «присоединиться» и, соответственно, какой получить Root-контекст. Вот как может выглядеть командная строка запуска приложения, использующего запущенный на том же самом компьютере с помощью вышеприведенной командной строки экземпляр Службы Имен:

Client -ORBInitRef NameService=iioploc://localhost:22000/NameService

Заметим, что ничто (кроме здравого смысла) не мешает вам регистрировать в службе имен имена временных объектов. Вообще, сама по себе задача, какие CORBA-объекты должны быть сделаны доступными, а какие – нет, не слишком проста.

Использование Trader Service

Трейдер-Сервис, в общем, гораздо интереснее Службы Имен, но и существенно сложнее. Во всех статьях, книгах, описаниях и т.п. приводится одна и та же аналогия с «Желтыми Страницами» телефонных справочников, но что делать – эта аналогия действительно верна.

В принципе, Трейдер можно рассматривать как логическое расширение концепции Службы Имен: и там, и там с объектными ссылками на нужные CORBA-объекты сопоставляется некоторая информация. Другое дело, что для Naming Service это просто строка, а для Трейдера – совокупность произвольной информации, причем часть ее может меняться динамически, в процессе функционирования системы, а не определена жестко на стадии создания. Естественным следствием такого подхода является необходимость механизма формирования запросов (что подразумевает использование специального «языка» запросов), и управления их выполнением. Как и Службы Имен, Трейдеры можно объединять в «федерации», при использовании которых одни Трейдеры становятся клиентами других; при этом, как правило, клиент рассматривает федерацию трейдеров как один трейдер.

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

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

Вот основные понятия и концепции, на которых основан Трейдер-Сервис:

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

Читатель, который захочет подробнее ознакомиться со всеми одиннадцатью интерфейсами Трейдер-Сервиса, может найти всю необходимую информацию в спецификации OMG (документ 97-12-23.pdf). Эта спецификация содержит 100 страниц (сравните с 18 для Naming Service и 38 для Interoperable Naming Service).

Автоматический запуск серверов приложений

Как уже говорилось, клиенту для установки связи с серверным CORBA-объектом не нужен ни сервант, ни работающее серверное приложение, которое способно создавать серванты. Другое дело – вызов методов удаленных объектов.

Совершенно очевидно, что универсальная технология создания распределенных систем не может полагаться на «ручной» запуск серверов приложений. Понятно, что распределенной системой, состоящей из нескольких CORBA-приложений, будет крайне тяжело управлять.

Для решения этой проблемы CORBA предусматривает специальный компонент, который позволяет автоматически (по запросу клиента) запускать нужные серверы. Этот компонент называется «Репозитарием Реализаций». В VisiBroker, например, этот Репозитарий называется Object Activation Daemon (OAD).

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

При использовании Репозитариев Реализаций для persistent-серверных объектов при попытке удаленного вызова по ранее полученной объектной ссылке используется не конкретный адрес объекта (имя хоста, порт и т.д.), а информация о «записи» в Репозитарии. Эта информация содержит все необходимое для автоматического запуска серверного приложения. Естественно, при изменении записи о серверном приложении в Репозитарии Реализаций, клиент может обращаться к разным приложениям на разных компьютерах.

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

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

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

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

В VisiBroker сначала запускается сам OAD (oad.exe), а для регистрации используется утилита oadutil.

Использование сконструированных пользователем типов данных

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

В данной статье кратко рассказано об основных сконструированных типах данных, о строках и об any. Кроме того, немного рассказано о вспомогательных классах, которые генерирует компилятор idl2cpp.

Обращаем ваше внимание на то, что часть информации в каждом из разделов относится к CORBA вообще (например, синтаксис IDL-деклараций), а часть – только к отображению IDL на C++.

Имена при отображении на C++

Для увеличения переносимости C++-программ на разные платформы и операционные системы (на уровне исходного кода) вместо конкретных имен типов C++ – short, unsigned long, wchar_t, char*, bool и пр. – используются typedef-алиасы, например, CORBA::Short, CORBA::ULong, CORBA::WChar, CORBA::String_ptr, CORBA::Boolean. Использование typedef-алиасов при создании CORBA-программ существенно упрощает их разработку, особенно потому, что наряду с такими алиасами широко используются и вспомогательные классы, например, CORBA::String_var. Типы, имена которых заканчиваются на _var или _out, являются чрезвычайно полезными и удобными, хотя почти всегда можно обойтись и без них.

Типы _ptr не представляют ничего интересного – это просто typedef-синоним для указателя:

typedef unsigned long CORBA::ULong;
typedef CORBA::ULong* CORBA::ULong_ptr;

Совсем другое дело – _var-классы.

_var-классы

_var-классы являются чисто сервисными классами – если не хотите, можете их не использовать, но дело в том, что использовать их чрезвычайно удобно. Эти классы являются оболочками вокруг типов, описанных на IDL. В сущности, их можно рассматривать как «разумные» указатели (smart pointer) – эти классы выполняют освобождение занятых ранее ресурсов при их уничтожении или при выполнении операций присваивания. Приведем пример использования строк: один вариант – в классическом стиле C++, другой – с использованием _var-класса CORBA.

Стиль C++:

f(char* Stringarg1, char* Stringarg2)
{
   char* str = new char[strlen(Stringarg1) + 1];
   strcpy (str, Stringarg);
   ...
   // утечка памяти
   str = new char[strlen(Stringarg2) + 1];
   strcpy (str,  Stringarg2);
   delete[] str; // не забудьте вызвать явно!
   ...

Первое замечание: так как строки сильно отличаются по длине друг от друга, программисты обычно используют для их хранения не статическую или стековую память (т.е. массив), а динамическую, поэтому приходится выполнять две команды – собственно выделение памяти нужного размера и копирование в нее символов строки. Это можно рассматривать как мелкое неудобство.

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

Программистам на C++ хорошо известны различные приемы, которые позволяют решить данную проблему – например, использование классов auto_ptr<> и string из STL. Поскольку идея во всех таких подходах одна – «завернуть» указатель на защищаемый ресурс в некоторую оболочку, которая и выполняет все необходимые действия – то компилятор idl2cpp просто генерирует такую оболочку. Это и есть _var-класс. Имейте в виду – _var-классы способны управлять различными ресурсами, а не обязательно памятью. Например, они часто используются для правильного ведения счетчика ссылок.

С использованием класса CORBA::String_var, тот же пример можно записать так:

Стиль CORBA для C++:

CORBA::String_var str = CORBA::string_dup ("My string");
str = CORBA::string_dup ("My anоther string");

Никакой утечки памяти здесь не будет: память, занятая в первой строке, будет автоматически освобождена как часть операции присваивания, а память, занятая во второй строке, будет освобождена деструктором класса CORBA::String_var.

Var-классы выполняют и другие очень полезные сервисные функции.

Строки

Строки CORBA бывают двух видов – «неограниченные» (unbounded) и «ограниченные» (bounded). Различие между ними состоит в том, что ограниченные строки не могут быть больше явно указанной длины. Хотя большинство реализаций отображения строк IDL на C++ игнорирует это различие, всегда следует иметь его в виду.

Синтаксис задания строк очень прост:

typedef string MyAliasForString;
typedef string<20> BoundedString_20;
typedef wstring MyAliasForWideString;

Использовать typedef в общем случае необязательно – все зависит от того, в каком месте IDL-файла появляются такие объявления.

Компилятор idl2cpp генерирует классические типы данных C++ – char* или wchar_t*, и, кроме того, объектные оболочки вокруг указателей – _var- и _out-классы. Out-классы удобно использовать при работе с методами, которые возвращают результат через список аргументов этого метода.

Некоторые особенности имеет управление динамической памятью для строк. Использование new и delete может (потенциально) привести к рассогласованию средств, используемых прикладным приложением и ORB’ом. Чтобы избежать этой проблемы, нужно использовать не new, а CORBA::string_alloc() или CORBA::string_dup(), и не delete, а CORBA::string_free().

CORBA::String_ptr str = CORBA::string_alloc (9);
strcpy (str, "My String"); // 9 символов, затем '\0’
   ...
CORBA::string_free (str);

Вы уже знаете, что удобнее написать:

CORBA::String_var str = CORBA::String_dup("My String");

Массивы

Под массивом в CORBA понимается классический массив – набор данных однотипных элементов, размер которого известен заранее и не может быть изменен. Если вам нужны массивы переменной длины, то нужно использовать так называемые «последовательности».

IDL-синтаксис определения массива:

typedef long MyLongArray[10][20];

Для массивов использование typedef обязательно.

Массивы IDL очень похожи на строки IDL тем, что компилятор idl2cpp превращает их в обычные массивы C++. Как и для строк, для массивов генерируются вспомогательные классы-оболочки – _var- и _out, а также функции создания динамических массивов – имя_массива_alloc() и имя_массива_free(). Обе они не имеют аргументов – размер массива известен на стадии компиляции IDL-файла.

Последовательности

Последовательности проще всего рассматривать как динамический массив переменной длины. Для читателей, знакомых с Delphi, может помочь аналогия с типом «array of <тип>».

Последовательности, как и строки, бывают ограниченные (bounded) и неограниченные. Элементом последовательности может быть другая последовательность, что позволяет имитировать многомерные массивы элементами «подмассив» различной длины.

IDL-cинтаксис объявления последовательностей таков:

// Простая последовательность
typedef sequence <long, 20> MyLongSequence;
// Безымянная вложенная последовательность.
// Допустимо, но неудобно ...
typedef sequence <sequence<long, 20> > S;
// Последовательность основанная на алиасе ...
// гораздо лучше:
typedef sequence<long, 20> MyNewType;
typedef sequence<MyNewType> S;

Наличие typedef не является обязательным.

IDL-последовательность при отображении на C++ приводит к генерации классов – собственно класса последовательности и вспомогательных классов.

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

typedef sequence<sequence<long> > SeqType;
interface SeqInterface
{
   // метод, возвращаюший последовательность
   SeqType test ();  
};

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

// обратите внимание на тип возвращаемого значения
SeqType* SeqInterfaceImpl::test()
{
   SeqType* res = new SeqType; // создание объекта
   res->length(2); // задание его длины
   // создание 1-го элемента
   // и задание его длины
   (*res)[0].length(5);
   // создание 2-го элемента
   // и задание его длины
   (*res)[1].length(10);
   for (int j = 0; j < 10; j++)
   {
      // инициализация элементов
      if (j < 5)
         (*res)[0][j] = (j + 1);
      (*res)[1][j] = (j + 1) * 10;
   }
   return res;
   // за освобождение памяти отвечает «клиент»
}

На стороне клиента используется (для удобства) _var-класс:

// получение объекта
SeqType_var s = oRef->test();
int sSize = s->length(); // определение его длины
for (int i = 0; i < sSize; i++)
{
   int n = s[i].length();
   for (int j = 0; j < n; j++)
      Print (s[i][j]);
}

Структуры

Структуры IDL очень похожи на структуры C – это набор полей различных типов. Структуры IDL, в отличие от структур C++, не могут управлять правами доступа к своим полям (то есть содержать такие элементы, как private или public) и содержать методы. Структуры могут быть определены рекурсивно:

interface MyInterface {};
typedef struct MyStruct
{
   typedef sequence<octet> OctetSeq;
   long           LongField;
   string         StringField;
   MyInterface    RefField;
   OctetSeq       SeqField;
   sequence<MyStruct> RecField;
} MyStruct_alias;

Как и последовательности, структуры IDL отображаются в отдельный класс C++. Как всегда, для них генерируются _var-, _out- классы и тип _ptr.

Пример использования структуры:

{
MyStruct_var msp = new MyStruct;
msp->LongField    = 1;
msp->StringField    = CORBA::string_dup("Value of string field");
msp->RefField    = ...;
...
} // автоматический вызов деструктора и,
  // соответственно, CORBA::string_free() для поля
  // StringField

valuetype (Типы-значения)

valuetype (типы-значения) заимствованы CORBA у Java-технологии RMI и, в силу ряда особенностей языка Java (возможность пересылки байт-кода по сети между виртуальными массивами Java), их использование более характерно для CORBA/Java, а не CORBA/C++, CORBA/Ada и т.п.

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

valuetype MyValueType
{
   public long longProp;
   private string stringProp;
   short MyMethod(in octet arg);
   factory MyInitMethod(in long arg);
   factory MyAnotherInitMethod(in long l,
      in string s);
};

На самом деле, тип valuetype – вещь достаточно хитрая. Для него определены операции     наследования, так называемые абстрактные интерфейсы, он может «поддерживать» (support – это служебное слово IDL) обычные интерфейсы CORBA, и многое другое. Для передачи таких объектов необходимо создать их фабрику и зарегистрировать ее в ORB – иначе ORB при выполнении маршалинга не сможет в общем случае создать объект, ведь он имеет только его состояние.

any

Объект типа any логически представляет собой совокупность двух значений: первое из них является идентификатором некоторого типа IDL, второе – значением этого типа.

Тип any реализуется по-разному в зависимости от языка программирования. Например, в C++ для представления any генерируется специальный класс с именем Any; для Java фабрикой объектов типа any является ORB.

Информация о типе значения, которое в определенный момент находится в объекте типа any, закодирована с помощью специальной информационной структуры, для управления которой в CORBA существует специальный интерфейс TypeCode. Обычно соответствие типа значения, хранящегося в any, и кода его типа (т.е. объекта типа TypeCode) достигается за счет того, что код типа недоступен пользователю и устанавливается автоматически при «занесении» значения того или иного типа в объект any.

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

TCKind – это перечисление, в котором каждому типу данных IDL (включая тип any) сопоставлен свой элемент):

module  CORBA

{
   ...
   enum TCKind
   {
tk_null,  tk_void,  tk_short, tk_long,
tk_ushort, tk_ulong, tk_float, tk_double,
tk_boolean, tk_char, tk_octet, tk_any,
tk_TypeCode, tk_Principal, tk_objref,  tk_struct,
tk_union, tk_enum, tk_string, tk_sequence,
tk_array, tk_alias, tk_except, tk_longlong,
tk_ulonglong, tk_longdouble, tk_wchar, tk_wstring, tk_fixed, tk_value, tk_value_box, tk_native, tk_abstract_interface
   };
   ...
};

Для описания структуры каждого из типов существует свой набор данных. Для многих типов он пуст – вполне достаточно просто наличия TCKind. Это такие типы, как octet, short или any.

Для других типов нужно хранить достаточно много данных. Например, для описания структуры объединения хранятся:

имя объединения;

TypeCode его дискриминатора;

значение дискриминатора, имя поля и TypeCode – для каждого из полей;

репозитарный идентификатор объединения.

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

TypeCode создается компилятором idl2cpp автоматически для любого типа данных (как стандартного, так и созданного программистом), если для него существует IDL-декларация. Если же вы хотите создать TypeCode динамически, т.е. в процессе работы программы и без IDL, то для этого предусмотрены специальные функции. Так может выглядеть создание синонима для интерфейса SimpleInterface:

CORBA::TypeCode_ptr alias_tc =
   orb->create_alias_tc ("LOCAL:MyRepositoryID",
      "SIAlias", _tc_SimpleInterface);

Для записи и чтения в или из any либо предусмотрены стандартные функции, либо они генерируются компилятором idl2cpp. Для выполнения этих операций переопределяются операции <<= и >>=.

Для структуры, описанной на IDL следующим образом:

struct MySruct { long l; };

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

CORBA::Any_var v;
v <<= (CORBA::Long)1;
CORBA::Long l;
v >>= l;

char* str = "String";   // не динамическая строка
v <<= str;
char * s_copy;
v >>= s_copy;

v <<= CORBA::string_dup ("Dynamic string");
v >>= s_copy;

MyStruct ms;
ms.l = 1;
any <<= ms;

MyStruct ms_copy;
v  >>=  ms_copy;

Объект типа any всегда берет на себя управление памятью для своих данных.

Определить, какой тип данных в текущий момент находится в any, можно с помощью метода type():

CORBA::TypeCode_var type_code = v->type();
switch (type_code->kind())
{
   case tk_long :
   ...
}

Статическое и динамическое связывание

Под статическим связыванием понимается организация удаленных вызовов, для которой все необходимые действия подготавливаются на этапе обработки IDL-файлов компилятором idl2... . В предыдущих разделах мы имели дело именно со статическим связыванием. Естественно, это наиболее эффективный и удобный способ, и единственное, что для него требуется – наличие IDL-объявлений (или их эквивалента для некоторых технологий Java) во время разработки программы. Код упаковки метода и его аргументов содержится в файле заглушки (stub), точнее, в классе, имя которого совпадает с именем IDL-интерфейса.

Для прикладных задач статического связывания вполне достаточно, но для универсальных программных средств, в том числе «мостов» к другим технологиям (например, к COM), необходим более гибкий подход. Набор средств, обеспечивающих выполнение всех необходимых действий на стороне клиента, называется DII (Dynamic Invocation Interface), на стороне сервера – DSI (Dynamic Skeleton Interface).

Не надо думать, что в CORBA существуют два разных «потока» взаимодействия – статический и динамический. Сервер не знает, какой способ использовался на стороне клиента при формировании запроса, а клиенту не интересно, как именно сервер (точнее, компоненты CORBA на стороне сервера) обеспечивает вызов реального метода.

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

interface MyInterface {
   long m (in long i);
};

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

Вот как в этом случае можно было бы использовать DII:

int i = 10; // значение нашего аргумента
CORBA::Object_var  mo = orb->bind ("IDL:MyInterface:1.0");
// задание имени метода...
CORBA::Request_var req  = mo->_request("m");
// ...типов и значений его аргументов...
CORBA::NVList_ptr  args = req->arguments();
CORBA::Any param;
param <<= (CORBA::Long)i;
args->add_value ("i", param, CORBA::ARG_IN);

// ...установка типа результата...
CORBA::NamedValue_ptr result = req->result();
CORBA::Any_ptr resultAny     = result->value();
resultAny->replace (CORBA::_tc_long, &result);
// ...вызов метода...
req->invoke();
// ...и, наконец, получение результата
int r = *(CORBA::Long*)resultAny->value();

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

При вызове производится контроль параметров на сервере, и неправильный динамический вызов приведет к возникновению стандартного исключения Corba.

Обработка ошибок

Обработка ошибок – один из важнейших аспектов любой технологии программирования. Все современные системы и языки используют варианты одной модели, которая называется «обработка исключительных ситуаций с завершением». С ней хорошо знакомы программисты на C++, Java или Delphi. Кстати, термин «с завершением» в ее названии не значит, что программа будет завершена – просто в общем случае продолжить выполнение с той точки, где была возбуждена исключительная ситуация, невозможно.

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

exception MyException_1 {};
exception MyException_2
{
   long code;
   string description;
};
interface MyInterface
{
   void MyMethod_1 ();
   void MyMethod_2 () raises (MyException_1);
   void MyMethod_1 () raises (MyExcpetion_1,
      MyException_2);
};

При отображении на C++ (или Java, или Delphi) все происходит довольно естественным образом. Для исключений генерируются отдельные классы, и вы просто «отлавливаете» соответствующий тип исключения в операторе catch.

Впрочем, у системы обработки ошибок CORBA есть один серьезный недостаток – в ней не поддерживается наследование исключений. Другими словами, все ваши исключения совершенно не зависят друг от друга. Ничего не поделаешь – CORBA готова поддерживать языки без встроенной системы обработки исключений.

Кроме исключений, определенных пользователем, в CORBA существует около тридцати так называемых «системных исключений». Вы не должны (и не можете) включить ни одно из них в raises-список вашего метода, да в этом и нет необходимости – считается, что любой удаленный метод способен возбуждать любые системные исключительные ситуации.

Системные исключения имеют одну очень интересную особенность – они в качестве поля содержат признак того, произошло ли исключение ДО выполнения каких-либо действий на стороне сервера, или оно произошло уже ПОСЛЕ.

При отображении на C++ все классы исключений пользователя являются производными от стандартного класса CORBA::UserException, а все системные – от CORBA::SystemException, так что вы можете немного систематизировать обработку исключений.

Для иллюстрации приведем небольшой пример. Его IDL-описание выглядит так:

exception MyException
{
   string msg;
};
interface Server_ORB_Exc
{
   long op (in long arg) raises (MyException);
};

Код метода на стороне сервера:

CORBA::Long Server_ORB_ExcImpl::op(CORBA::Long arg)
   throw (CORBA::SystemException, MyException)
{
   long res = arg + 1;
   if (arg == 0)
      throw(CORBA_BAD_PARAM(0,
            CORBA::COMPLETED_YES));
   if (arg < 0)
   {
      MyException me ("Negative value");
      throw me;
   }
   return res;
}

Код на стороне клиента:

int i = ...
try
{
   res = oRef->op(i);
}
catch (MyException& me)
{
   cout << me.msg;
}
catch (CORBA_BAD_PARAM& bp)
{
   cout <<  "CORBA::BAD_PARAM exception";
   cout << getCompletedStatus (bp);
}
catch (CORBA_SystemException& se)
cout <<  "CORBA::SystemException raised";
   cout << getCompletedStatus (se);
}
catch (...)
{
   ...
}

const char* getCompletedStatus(
   CORBA_SystemException& e)
{
   if (e.completed() == CORBA::COMPLETED_NO)
      return "COMPLETED_NO";
   if (e.completed() == CORBA::COMPLETED_YES)
      return "COMPLETED_YES";
   // Иначе CORBA::COMPLETED_MAYBE
   return "COMPLETED_MAYBE";
}

Метаданные объектов

Обычно прикладному программисту не приходится использовать средства, описанные в данном разделе, но они могут быть очень полезны, если вы вынуждены применять, например, DII. Речь идет о сохранении в специальных базах данных всей той информации, которая может содержаться в IDL-файлах. Такие базы данных называются Репозитариями Интерфейсов (Interface Repository, IR).

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

Как в любой другой базе данных, доступ к «записи» осуществляется по «ключу». Роль такого ключа играют Репозитарные Идентификаторы (Repository ID, RID). В большинстве случаев эти RID для элементов IDL-объявлений генерируются компилятором с IDL. Мы уже встречались с ними: например, по следующему IDL-объявлению будут сгенерированы следующие RID:

#pragma prefix "my.prefix"
// RID - IDL:my.prefix/MyModule:1.0 для
module MyModule
{
   // RID - IDL:my.prefix/MyModule/MyInterface:1.0 для
   interface MyInterface
   {
      // IDL:my.prefix/MyModule/MyInterface/MyMethod:1.0
      void MyMethod ();
     };
};

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

Разумеется, перед тем, как информация может быть считана, она должна быть помещена в IR. Повторяем еще раз – этот процесс не стандартизован OMG. Применительно к VisiBroker, этот процесс выглядит так: сначала запускается фабрика CORBA-объекта – она называется irep. В качестве параметра командной строки указываются опции и имя (имена) IDL-файлов, содержимое которых считывается в данную базу данных. Вы можете потом добавлять в Репозитарий содержимое и других IDL-файлов.

Если Репозитарий доступен, то можно, используя объектную ссылку, вызвать метод _get_interface(), который считывает из Репозитария информацию, относящуюся к данному интерфейсу:

MyInterface_ptr oRef = ...
CORBA::InterfaceDef_ptr myInterf =
   oRef->_get_interface();

Затем, используя методы класса InterfaceDef, можно получить имя интерфейса, его RID, список методов, их аргументы, исключения и т.п.:

CORBA::InterfaceDef::FullInterfaceDescription*
   fidp = myInterf->describe_interface();
cout << fidp->name;
cout << fidp->id;
   ...

Существует и более общий способ извлечения любой информации из указанных Репозитариев Интерфейсов, а также текстовые и GUI-утилиты работы с ними. Предусмотрена стандартная утилита idl2ir, которая помешает в указанный IR содержимое IDL-файлов.

Управление потоками

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

Правда, если подходить строго формально, то в стандарте, собственно, нигде прямо не говорится о конкретных аспектах обеспечения многопоточности. Мы уже говорили о свойстве ThreadPolicy для POA, которое может принимать одно из двух значений – SINGLE_THREAD_MODEL и ORB_CTRL_MODEL, но этого с точки зрения анализа свойств многопоточности реализации крайне мало.

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

Второе значение – ORB_CTRL_MODEL – означает только то, что POA может обрабатывать запросы в произвольном режиме.

Кроме того, многопоточность каждой CORBA-реализации связана не только с поведением POA, но и с режимом доставки запросов на уровне ORB.

В настоящий момент все эти проблемы отданы на откуп разработчикам реализации CORBA. C VisiBroker’ом дело обстоит следующим образом: предусмотрены два многопоточных режима: Thread pool (по умолчанию) и Thread-per-session.

Thread pool означает, что запросы от разных клиентов обслуживаются первым попавшимся свободным потоком из заранее созданного пула потоков. После завершения обработки запроса поток возвращается в пул.

Thread-per-session обеспечивает обслуживание всех запросов от одного клиента специально выделенным для этого потоком. Если клиентское приложение само имеет несколько потоков (использующих одно соединение с сервером) и посылает запросы асинхронно, или сервер просто не успевает обрабатывать приходящие один за другим запросы, он выстраивает их в очередь. Разумеется, другие клиенты обслуживаются параллельно.

Если использовать аналогию с потоковыми моделями COM, то в общем случае VisiBroker поддерживает аналоги режимов Single Thread и Multiple Thread Apartment (MTA, по умолчанию). Аналога режима STA не поддерживается. Режим STA (см. "Управление потоками" в COM) можно сэмулировать, создавая для каждого объекта (вернее серванта) свой POA с политикой Single Thread. Режим Thread-per-session, до некоторой степени применимый вместо STA, все же не дает полной гарантии потокобезопасности.

Серьезной проблемой является обеспечение переносимости программ, использующих средства синхронизации потоков и защиту ресурсов – здесь нет никаких проблем только с Java. Что касается, например, C++, то, очевидно, такие средства различны в различных операционных системах, и ни о какой переносимости некоторых фрагментов программ не может быть и речи. В принципе, у разработчиков есть единственный выход – использовать средства синхронизации более высокого уровня предоставляемых реализациями CORBA или переносимыми библиотеками при условии поддержки таких средств каждой целевой платформой. Например, VisiBroker предоставляет стандартные средства, такие, как VISMutex, для всех поддерживаемых платформ, но это работает только при использовании VisiBroker’а.

Масштабируемость

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

Предпосылки обеспечения масштабируемости:

Вот основные средства обеспечения масштабируемости:

Устойчивость к сбоям

Обеспечение устойчивости к сбоям – довольно слабое место CORBA с той точки зрения, что в настоящий момент (и на ближайшие 2-3 года) оно слабо формализовано на уровне стандарта. Вследствие этого, в CORBA существуют отдельные фрагменты, но не комплексная система обеспечения такой устойчивости. Конечно, различные поставщики программного обеспечения предлагают те или иные решения (например, уже упоминавшиеся smart agents в Visibroker), но они не являются переносимыми и не носят системного характера.

Существует документ (Request For Proposal, RFP), в котором намечены основные направления комплексной системы обеспечения устойчивости к сбоям, но ее доводка и реализация, видимо, займет немало времени.

Из сказанного не следует, что CORBA не обеспечивает некоторого (и приемлемого) уровня устойчивости. Его основой является явное отсутствие жесткой связи между клиентами и серверами. Сбой на клиенте (или на каналах связи) крайне слабо отражается (если вообще отражается) на состоянии сервера. Это означает, что проблемы на линиях связи или клиентских местах – это проблема отдельного клиента, а не снижение уровня работоспособности системы в целом.

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

Для решения клиентских проблем обычно применяется резервирование серверных объектов и серверов приложений. В CORBA эти подходы также оговорены, по крайней мере, на уровне концепции: клиент, в общем случае, не знает, и знать не должен, где находятся нужные ему серверные объекты. Крайне досадным обстоятельством является отсутствие качественных реализаций Сервиса Долговременного Хранения (Persistence Service), который должен обеспечить прозрачное для разработчика сохранение состояния CORBA-объектов в долговременных хранилищах. Наличие такой реализации позволило бы выполнять автоматическое перенаправление вызовов клиентов к различным копиям объектов с состоянием. Впрочем, многие эксперты считают, что такая задача не решается вне рамок компонентной модели CORBA (ожидается к концу 2000 года).

Повысить жизнеспособность распределенной системы может служба асинхронного обмена  сообщениями (Messaging Service, в терминах CORBA). На сегодняшний день существует вариант конкретного решения (так называемый submission), но он еще не утвержден OMG.

Асинхронное взаимодействие между объектами

Предложение по реализации Messaging Service было разработано и направлено в OMG в мае 1998 г. В разработке принимали участие (по алфавиту) BEA Systems, Expersoft, Inprise, IBM, IONA, Novell, Oracle, PeerLogic и другие фирмы. Документ содержит 150 страниц, и в данной статье мы можем привести только его основные положения. Утверждение этой спецификации ожидается в рамках принятия CORBA 3.0. О Messaging Service немного будет рассказано в следующем разделе.

В дополнение к двум уже существующим в CORBA способам взаимодействия, «синхронному» и «отложенному» (deferred), спецификация определяет еще два: «асинхронный», когда приложение выполняет вызов и ORB асинхронно отправляет его клиенту, а затем то же приложение обрабатывает отклик, и «time-independent (TI)», когда отклик может обрабатываться другим клиентским приложением, а не тем же, которое выполнило запрос. Соответственно, в документации используются две аббревиатуры – AMI (Asynchronous Method Invocation) и TII (Time-Independent Invocation).

AMI

В режиме AMI предусмотрены два способа взаимодействия:

TII

TII можно рассматривать как расширение AMI-модели – просто ReplyHandler и/или Poller становятся persistent-объектами. Poller может быть передан, например, другому приложению для обработки отклика. Правда, такое расширение требует внесения определенных изменений в ядро CORBA, в том числе в протокол GIOP, так как подразумевает вызов неактивных (и не активируемых автоматически) объектов и такой же возврат результата. Естественно, для этого требуется наличие дополнительного компонента (Router) и специального протокола обмена (Interoperable Routing Protocol).

Общая схема взаимодействия в режиме TII

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

В случае Callback-модели все выполняется просто: клиент создает нужную реализацию ReplyHandler, создает этот CORBA-объект и передает объектную ссылку на него в качестве аргумента удаленного вызова. Сервер при завершении обработки выполняет удаленный вызов методов callback-объекта. Поскольку при этом используются обычные средства CORBA (активация сервера приложений, POA, активация сервантов), то отклик может быть обработан любым подходящим приложением.

Poller-схема несколько сложнее. Все начинается, естественно, с выполнения клиентом удаленного вызова, точнее, его message-ориентированной версии. ORB создает value-объект Poller, в котором «содержится» невидимый объект типа ReplyHandler, ссылка на который сопоставляется с также создаваемым ORB’ом объектом типа PersistentRequest. Затем ORB возвращает Poller-объект клиенту (как результат вызова).

На следующем шаге ORB выполняет те же действия, что и при использовании callback-схемы – с применением ReplyHandler-объекта, который получает отклик от сервера. Этот объект переходит в состояние ожидания запроса от Poller-объекта, который извлекает необходимую информацию и передает ее клиенту. Как взаимодействовать с Poller-объектом, решает клиент.

Единственная тонкость заключается в том, что «сердце» схемы взаимодействия – PersistentRequest – должен быть доступен для любого клиента, который захочет использовать объект Poller. Хорошим решением является, например, реализация PersistentRequest-объекта корпоративным объектом типа Router.

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

Отдельными проблемами является расширение возможностей Сервиса Транзакций и Сервиса Безопасности. В частности, предложено понятия «unshared transaction», используемой в случае TII, но обсуждение этих вопросов выходит за пределы данной статьи.

Обеспечение безопасности

Система обеспечения безопасности CORBA (Security Service – Сервис Безопасности) спроектирована так, чтобы не мешать разработчику в процессе создания приложений. Теоретически, созданные вами объекты должны взаимодействовать с обеспечивающим безопасность ORB без вашего участия. ORB обеспечивает все необходимое для объектов, которые совсем ничего не знают о существовании Security Service и даже не имеют соответствующих интерфейсов. Это позволяет объектам легко пересекать границы сред, использующих различные механизмы обеспечения безопасности.

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

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

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

Под «обеспечением безопасности» в CORBA понимается решение (или попытка решения) очень многих проблем:

К счастью, разработчикам не нужно изучать 250-страничную спецификацию – использование этого Сервиса задача не программиста, а системного администратора.

CORBA 3.0 гораздо более строго, чем CORBA 2.3, оговаривает проблемы обеспечения безопасности при использовании Internet, включая обеспечение прав апплетов Java при попытке обращения к CORBA-серверам. Сейчас для решения этой проблемы нужно использовать дополнительные средства (например, Inprise Gatekeeper).

До недавнего времени главной проблемой для разработчиков, создающих реальные проекты, являлась доступность реализаций Сервисов Безопасности за пределами США. В настоящее время фирма PrismTech в рамках проекта OpenFusion распространяет реализацию Сервиса Безопасности фирмы Gradient.


Copyright © 1994-2016 ООО "К-Пресс"