Занятие 5

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

1. В модуле Cellular_System, описанном в файле Cellular_System.idl, добавлена операция findIdByNumber, пропущенная ранее, и в целях унификации вызовов изменен один из параметров операции SMSSent:

module Cellular_System
{
  interface CellularSystem
  {
    #pragma version CellularSystem 1.0
    // Найти id абонента по его номеру
    unsigned long long findIdByNumber(in string number);
. . .
    // Уведомление о посылке текстового сообщения
    oneway void SMSSent(in unsigned long long id,
                in unsigned long long callee, in
           Utility::TimeMark timeMark);
	. . .
  };
};

2. Добавлена операция копирования канала данных в описание интерфейса DataChannel, располагающееся внутри файла Radio_Subsystem.idl:

  interface DataChannel
  {
    #pragma version DataChannel 1.0
	. . .
    void copyChannel(in DataChannel toCopy);
  };

Менеджеры сервантов

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

Спецификация CORBA 2.3 очень четко расписывает работу и составляющие части менеджеров сервантов. Там упоминается пустой интерфейс ServantManager, являющийся прародителем двух других — ServantActivator и ServantLocator. Они-то и используются разработчиками для реализации собственных менеджеров сервантов, причем применение того или иного зависит от политики, установленной для POA. Если программист, создавая POA, планирует хранить ссылки в таблице активных объектов, то он будет использовать политику RETAIN (установлена для нового POA по умолчанию). В этом случае следует создавать свой менеджер сервантов, реализуя в нем интерфейс ServantActivator. При политике NON_RETAIN, которая не дает POA хранить ссылки на активные объекты, нужно реализовать интерфейс ServantLocator. Еще одно важное условие: созданный менеджер сервантов должен быть локальным по отношению к его POA, т. е. располагаться в том же адресном пространстве. И разумеется, предполагается, что установлена политика USE_SERVANT_MANAGER.

ServantActivator

Интерфейс ServantActivator включает в себя всего лишь два метода, служащих функциями обратного вызова, — incarnate() и ethercalize(). Это значит, что они вызываются адаптером объектов в определенных ситуациях.

Метод incarnate() будет вызван, как только поступит запрос для объекта с заданным идентификатором. Если вы хотите создать объект или, скажем, воспользоваться объектом, находящимся в пуле, то следует поместить код, реализующий эти замыслы, именно в метод incarnate(). Для успешного выполнения этой задачи у данного метода есть два важных параметра. Один из них — oid — является идентификатором, отличающим каждый экземпляр объекта. Другим параметром передается ссылка на адаптер объектов POA, в рамках которого должен находиться объект. Метод incarnate() должен возвращать ссылку на тип Servant. Если создание объекта невозможно, возбуждается стандартное исключение CORBA::ForwardRequest:

Servant incarnate (
  in ObjectId	oid,
  in POA  adapter)
  raises(ForwardRequest);

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

Обратный процесс деактивации объекта сопровождается вызовом метода etherealize():

void etherealize(
  in ObjectId	oid,
  in POA	adapter,
  in Servant	serv,
  in boolean	cleanup_in_progress,
  in boolean	remaining_activation);

Первые два параметра абсолютно схожи с параметрами метода incarnate(), а третий является ссылкой на сервант деактивируемого объекта. Если этот сервант связан с разными объектами, то параметр remaining_activation будет равен TRUE. Следовательно, уничтожать его нельзя. Предпоследний параметр cleanup_in_progress говорит о причине вызова etherealize(). При значении TRUE вам будет ясно, что причина кроется в вызовах deactivate() или destroy() с параметрами etherialize_objects, равными TRUE.

ServantLocator

Этот интерфейс будет интересен тем, кто планирует использовать объекты, живущие короткий срок, или так называемые объекты без состояния (stateless), т. е. такие атрибуты, которые не имеют значения. Зачастую нет смысла хранить на них ссылки, поэтому для POA выбирается политика NON_RETAIN. Когда приложение обращается за услугами объекта, менеджер сервантов (в данном случае ServantLocator) активирует подходящий CORBA-объект, который обрабатывает запрос клиента и теряется. Естественно, ServantLocator может сам хранить ссылки или следить за пулом объектов, освобождая POA от сложностей.

Как и в случае с ServantActivator, интерфейс ServantLocator имеет всего лишь два метода: preinvoke(), вызываемый при получении запроса от клиента, и postinvoke(), к которому система обращается после завершения обработки запроса. Однако после каждого запроса ссылка на активированный объект теряется, поэтому эти два метода «дергаются» при каждом запросе. Не стоит считать бессмысленной или расточительной такую реакцию, ведь, например, при передаче банковских транзакций требования к безопасности столь велики, что даже потеря времени и ресурсов не будет слишком высокой ценой. И уж лучше создавать новый объект на каждый запрос, чем допустить «троянского коня».

Метод preinvoke() описывается следующим образом:

Servant preinvoke(
  in ObjectId	oid,
  in POA		adapter,
  in CORBA::Identifier	operation,
  out Cookie		the_cookie)
  raises(ForwardRequest);

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

Метод postinvoke() описывается так:

void postinvoke(
  in ObjectId	oid,
  in POA	adapter,
  in CORBA::Identifier	operation,
  in Cookie	the_cookie
  in Servant	the_servant);

Все параметры вам уже знакомы кроме последнего, через него передается ссылка на сервант объекта.

И снова практика

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

Обратимся к точке входа — методу main(). Как видно из фрагмента текста, для начала сервер проверяет количество аргументов командной строки. Если их меньше двух, то происходит аварийное завершение с выдачей сообщения о правильном запуске:

...
    if(args.length !=  2)
    {
      System.err.println(?Usage: vbj ? +
      ?ru.pcworld.Servers.CellServer  ? +
      ? <# of DataChannel objects>?);
      System.exit(-1);
    }
...

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

...
    try
    {
      // Инициализация брокера объектных запросов
      org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args,
null);
      // Ссылка на корневой POA
      POA rootPOA = POAHelper.narrow(
              orb.resolve_initial_references(?RootPOA?));
      // Настройка политик POA соты
      org.omg.CORBA.Policy[] cellPolicies = {
        rootPOA.create_lifespan_policy(
                         LifespanPolicyValue.PERSISTENT),
        rootPOA.create_id_assignment_policy(
                         IdAssignmentPolicyValue.USER_ID)};
      // Создание дочерних POA для соты
      POA cellPOA = rootPOA.create_POA(
        ?cell_poa?, rootPOA.the_POAManager(), cellPolicies);
      // Настройка политик POA пула каналов связи
      org.omg.CORBA.Policy[] dcpPolicies = {
        cellPOA.create_request_processing_policy(
          RequestProcessingPolicyValue.USE_SERVANT_MANAGER)};
      // Создание дочерних POA для пула каналов связи
      POA dcpPOA = cellPOA.create_POA(
?data_channel_pool_poa?,
                    cellPOA.the_POAManager(),dcpPolicies);

Обратите внимание, что создаются два POA: один для серванта объекта соты, другой — для пула каналов данных. Политики для вновь создаваемых POA выбраны следующим образом. При внезапном останове сота должна сохранить свое состояние, поэтому она сделана долгоживущей (PERSISTENT). Политика USER_ID позволяет самим задавать имя объекта. С POA каналов данных дело обстоит еще проще: достаточно указать политику USE_SERVANT_MANAGER, чтобы сообщить о намерении использовать менеджер сервантов. Последний позволит нам изначально не активировать все объекты каналов передачи данных в пуле, а делать это постепенно, по мере поступления запросов. Следующие строки на Java как раз демонстрируют создание пула:

      // Заполнение пула ссылками на каналы передачи данных
      DataChannel[] dcPool =
             new DataChannel[Short.parseShort(args[1])];
      for(short i = 0; i < dcPool.length; i++)
         dcPool[i] = DataChannelHelper.narrow(
           dcpPOA.create_reference(DataChannelHelper.id()));

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

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

...
      // Создается экземпляр серванта
      ru.pcworld.CellImpl cellServant =
               new ru.pcworld.CellImpl(
                   uniqueId, ?Mir PK - RUS?, dcPool, orb);

Менеджер сервантов закрепляется за определенным POA в два этапа. Сначала он создается операцией new и активируется, как любой другой сервант CORBA-объектов, операцией _this(), а уже после этого методом set_servant_manager() «встраивается» в POA:

      // Установить менеджер сервантов
      ServantActivator activator =
                    new DataChannelActivator()._this(orb);
      dcpPOA.set_servant_manager(activator);

Поскольку объект соты долгоживущий, на него должна быть помещена ссылка в таблицу активных объектов его POA, т. е., как говорится, он должен быть активирован. Для этого применяется операция activate_object_ with_id(). Ей следует передать идентификатор экземпляра объекта в виде массива байтов и ссылки на сервант:

      // Активировать объект под именем, переданным через
      // командную строку
      cellPOA.activate_object_with_id(
                         args[0].getBytes(), cellServant);
      cellPOA.the_POAManager().activate();
      dcpPOA.the_POAManager().activate();
      cellServant.startTestPolling();
...
      orb.run();
...

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

Следующий пункт рассмотрения — менеджер сервантов. Мы создаем его, реализуя методы incarnate() и etherealize() интерфейса ServantActivator (листинг 2).

Метод incarnate() делает только две вещи: выводит сообщение на экран и создает новый экземпляр серванта, возвращая ссылку на него:

...
public class DataChannelActivator extends ServantActivatorPOA
{
  public Servant incarnate(byte[] oid, POA adapter) throws
                   org.omg.PortableServer.ForwardRequest
{
  System.out.println(«Starting up object (id = « +
                                 ?oid.toString()? + ?)?);
  return new DataChannelImpl();
}

Если ваш компилятор Java выдаст сообщение о несовпадении возвращаемого типа, то просто приведите возвращаемую ссылку к типу Servant.

Метод etherealize() также несложен. В нем производится деактивация объекта через вызов метода POA под названием deactivate_object():

  public void etherealize(byte[] oid,
                          POA adapter,
                          Servant serv,
                          boolean cleanup_in_progress,
                          boolean remaining_activations)
  {
    System.out.println(?Shutting down object (id = ? +
                                 ?oid.toString()? + ?)?);
    try { adapter.deactivate_object(oid); }
    catch(Exception e){ e.printStackTrace(); };
    System.gc();
  }
}

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

Вот наконец-то мы и добрались до самих сот. Поскольку объект соты одновременно является и тестовым потоком, который посылает системе проверочные сигналы, он реализует интерфейс Runnable:

public class CellImpl extends CellPOA implements Runnable
{

Конструктор этого объекта мы уже упоминали. Теперь поглядим, как он выглядит изнутри:

  public CellImpl(long device_id, String textToDisplay,
          DataChannel[] dcPool, org.omg.CORBA.ORB orb)
  {
...
    // Получить ссылку на объект центральной системы
    System.out.println(?Looking for central system?);
    while(true)
    {
      try
      {
        Thread.sleep(1000);
        _system = CellularSystemHelper.bind(_orb,
          ?/central_system_poa?,
?CellularSystem?.getBytes());
        System.out.println(?Link is established?);
        return;
      }catch(Exception e)
      {
        System.out.print(?.?);
      }
    }
  }

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

Метод, с помощью которого сервер запускает тестовый поток, выглядит следующим образом:

  public void startTestPolling()
  {
    _poller = new Thread(this);
    _poller.start();
  }

Метод run() тестового потока в бесконечном цикле с интервалом в 5 с вызывает метод stationIsAlive() центральной системы, передавая свой идентификатор и время обращения:

  public void run()
  {
    try
    {
      while(true)
      {
        // Сообщить системе о своей работоспособности
        _system.stationIsAlive(_device_id, getTimeMark());
        // Подчистить ресурсы, пока есть время
        System.gc();
        Thread.sleep(5000); // Отдых
      }
    } catch(InterruptedException e) {}
  }
...

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

Наиважнейший метод объекта соты — willUseThisCell(), через который телефоны сообщают о своем желании подключиться к соте:

...
  public void willUseThisCell(
                 long device_id, String ref, Cell wentFrom)
  {
    // Добавить телефон в список подключенных
    _phoneList.add(new PhoneEntry(PhoneHelper.narrow(
                 _orb.string_to_object(ref)), device_id));
    // Уведомить систему о подключении телефона
    _system.phoneConnected(device_id, ref, getTimeMark());

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

Чтобы сделать это, сота находит в своем пуле каналов один свободный (предполагается, что переполнения соты звонками не произойдет) и упаковывает его в Holder-класс. Ссылка на него передается вызовом passCall() соте, из зоны которой телефон прибыл:

    // Найти в пуле каналов свободный
    DataChannelHolder channel = new DataChannelHolder();
    for(int i = 0; i < _dcPool.length; i++)
    {
      if(_dcPool[i] != null)
      {
        channel.value = _dcPool[i];
        // Узнать, соединен ли абонент в настоящий момент
        wentFrom.passCall(device_id, channel);
        if(channel.value != null) // Соединен!
        {
          // Перестроить канал на текущую соту
          if(channel.value.src() != null)
          channel.value.src(_this(_orb));
          else channel.value.dst(_this(_orb));
          _callList.add(new CallEntry(channel.value,
            PhoneHelper.narrow(_orb.string_to_object(ref))));
        }
      }
    }
  }

Если возвращаемое в Holder-классе значение равно null, значит, телефон не был подключен к другому абоненту. В противном случае мы имеем копию класса канала связи. Все, что остается сделать соте, это найти, какая из сотовых ссылок равна null, и записать на ее место ссылку на себя. После чего полученный канал добавляется в список активных звонков.

Метод, передающий новой соте ссылку на канал имеющегося звонка, также довольно сложен за счет итераторов, осуществляющих поиск ссылки на «перебрасываемый» телефон и поиск занятого телефоном канала:

  public void passCall(long device_id, DataChannelHolder
newChannel)
  {
    Phone phone = null;
    // Найти ссылку на телефон в _phoneList
    PhoneEntry entry = null;
    for(Iterator pit = _phoneList.iterator(); pit.hasNext(); )
    {
      entry = (PhoneEntry) pit.next();
      if(entry.device_id == device_id)
      {
        phone = entry.phone;  // Скопировать ссылку
        _phoneList.remove(entry); // Оригинал больше 
не нужен
        break;
      }
    }
    // Найти по ссылке на телефон канал его текущего звонка
    CallEntry call;
    for(Iterator cit = _callList.iterator(); cit.hasNext(); )
    {
      call = (CallEntry) cit.next();
      if(call.phone != phone) continue;
      else
      {
        // Заменить ссылку на себя константой null
        if(call.channel.src() == _this(_orb))
             call.channel.src(null);
        else call.channel.dst(null);
        // Скопировать канал в переданный параметр
        newChannel.value.copyChannel(call.channel);
        // Удалить скопированный звонок из списка активных
        _callList.remove(call);
        return;
      }
    }
    // Если телефон не участвует в каком-либо звонке,
    // вернуть null вместо ссылки на канал
    newChannel.value = null;
  }

Метод вызова абонента callNumber() проверяет, не остановлена ли сота, и выдает в таком случае вызывающему абоненту сигнал «занято» в виде исключительной ситуации NumberException:

  public void callNumber(long caller_device_id, String number)
             throws ru.pcworld.Utility.NumberException
  {
    // Если сота остановлена, выдать сигнал «Занято»
    if(!_isEnabled)
              throw new NumberException(BUSY.value);
    // Найти ID абонента по его номеру телефона...
    long calleeId = _system.findIdByNumber(number);
    // ... и получить ссылку на его соту
    Cell dstCell = (Cell) _orb.string_to_object(
                       _system.findClient(calleeId));
    // Попросить соту соединить с абонентом
    dstCell.receiveCall(caller_device_id, calleeId);
  }

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

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

  public void receiveCall(long sender_device_id, long callee)
              throws ru.pcworld.Utility.NumberException
  {
    // Найти ссылку на телефон в списке
    PhoneEntry pe = null;
    CallEntry ce = null;
    for(Iterator pit = _phoneList.iterator(); pit.hasNext(); )
    {
      pe = (PhoneEntry) pit.next();
      // Проверить, есть ли вызываемый телефон в списке
      if(pe.device_id != callee) continue;
      else
      {
        for(Iterator cit = _callList.iterator();
cit.hasNext();)
        {
          ce = (CallEntry) cit.next();
          // Если телефон занят, вернуть сигнал «Занято»
          if(ce.phone == pe.phone)
                     throw new NumberException(BUSY.value);
        }
        // Телефон готов к разговору. Уведомить систему
        _system.clientCallBegin(sender_device_id,
            _orb.object_to_string(pe.phone), getTimeMark());
      }
    }
    // Сообщить о звонке
    try { pe.phone.incomingCall(); } catch(Exception e)
    {
      // Телефон вышел из зоны обслуживания
      throw new NumberException(NOT_REACHABLE.value);
    }
  }

Методы для передачи и получения сотами текстовых сообщений SMS намного проще, хотя и принципиально не отличаются от «звонковых» методов:

  // Послать сообщение SMS
  public void sendMessage(
       long sender_device_id, String number, String message)
              throws ru.pcworld.Utility.NumberException
  {
    long calleeId = _system.findIdByNumber(number);
    Cell dstCell = (Cell) _orb.string_to_object(
                      _system.findClient(calleeId));
    dstCell.receiveMessage(sender_device_id, message,
calleeId);
  }
  public void receiveMessage(
         long sender_device_id, String message, long callee)
              throws ru.pcworld.Utility.NumberException
  {
    PhoneEntry pe = null;
    for(Iterator pit = _phoneList.iterator(); pit.hasNext(); )
    {
      pe = (PhoneEntry) pit.next();
      // Проверить, есть ли вызываемый телефон в списке
      if(pe.device_id != callee) continue;
      else   // Уведомить систему
        _system.SMSSent(sender_device_id, callee,
getTimeMark());
    }
    // Уведомить о сообщении
    try { pe.phone.incomingMessage(message); }
catch(Exception e)
    {
      // Телефон вышел из зоны обслуживания
      throw new NumberException(NOT_REACHABLE.value);
    }
  }

Собственно данные передаются вызовом метода receiveData(), который находит подходящий канал связи и в бесконечном цикле вызывает метод passData() до тех пор, пока канал не будет свободным, т. е. предыдущая порция данных не будет считана принимающей стороной:

  public void receiveData(byte[] data, org.omg.CORBA.Object
sender)
  {
    // Найти канал, по которому нужно передать данные
    CallEntry ce = null;
    for(Iterator cit = _callList.iterator(); cit.hasNext(); )
    {
      ce = (CallEntry) cit.next();
      if(ce.phone != (Phone)sender) continue;
      else
      while(true)
      {
        try
        {
          ce.channel.passData(data,_this(_orb));
          return;
        }
        catch(ChannelBusyException e) {}
      }
    }
  }
...

Интересный метод, который следовало бы вынести в отдельный класс, — getTimeMark(). Его задача — генерация текущей отметки времени и даты, необходимой для внутренних нужд системы:

...
  public TimeMark getTimeMark()
  {
    GregorianCalendar gc = new GregorianCalendar();
    return new TimeMark(
                   (short) gc.get(Calendar.DAY_OF_MONTH),
                   (short) gc.get(Calendar.MONTH),
                   (short) gc.get(Calendar.YEAR),
                   (short) gc.get(Calendar.HOUR_OF_DAY),
                   (short) gc.get(Calendar.MINUTE),
                (short) gc.get(Calendar.SECOND));
  }

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

На этом закончим наше занятие. Предоставляем вам возможность потренироваться в отладке описанных исходных текстов.