Занятие 4
С тех пор, как мы и предполагали, исходные тексты претерпели изменения, хотя и не радикальные.
Как выглядит сервер с сервантом по умолчанию, для чего такого рода сервант может быть нужен, — тема четвертого занятия.
Проекты с использованием объектов CORBA следует начинать с реализации атомарных объектов, не зависящих от объектов более низкого уровня. Поэтому рассмотрим CORBA-интерфейс DataStorage, служащий «черным ящиком» для административных и системных объектов, когда требуется записать данные в хранилище или считать их оттуда. Напрашивается вопрос: а зачем, собственно, нужен еще один объект, когда можно обратиться к базам данных напрямую? В том-то и дело, что именно максимальный уровень изоляции объектов от хранилищ и прочих вспомогательных элементов нам и нужен! Объект DataStorageImpl, который реализует интерфейс DataStorage, служит драйвером доступа к базе данных. Входной интерфейс DataStorage может оставаться неизменным, тогда как внутренняя схема работы может быть изменена до неузнаваемости. Если завтра, к примеру, ваша система перейдет с СУБД Sybase на СУБД Oracle, вам придется переделать только один-единственный интерфейсный объект. Да здравствует многозвенная архитектура!
Скорректированное описание интерфейса DataStorage на языке IDL представлено на листинге 1.
Сначала директивой typedef описывается новый тип CellNames для списка имен сотовых станций. Он используется операцией DataStorage::getCellNameList, которую вызывает главный объект системы, чтобы получить список имен всех сот, под которыми они регистрируют свои CORBA-объекты. Только имея эти имена, можно получить ссылки на объекты станций.
Далее идут операции проверки допустимости обслуживания абонента (DataStorage::isInService), поиска идентификатора абонента по его телефонному номеру (DataStorage::findIdByNumber), протоколирования и запроса местоположения абонента (DataStorage:: storeClientPlace и DataStorage::recallClientPlace соответственно), протоколирования и запроса времени последней проверки работоспособности соты (DataStorage:: setCellTestTime и DataStorage::getCellTestTime). Для того чтобы сохранять параметры действий абонентов, существуют операции регистрации посылки сообщения (DataStorage::SMSSent), а также протоколирования начала и окончания звонка (DataStorage::clienCallBegin и DataStorage::clienCallEnd). Поскольку в наши планы не входила реализация всей подноготной системы, я не стал уточнять внутренние детали объекта и, следовательно, не привожу здесь описание класса DataStorageImpl, реализующего интерфейс DataStorage, — он пуст. Если хотите, то можете сами написать его начинку для выбранного вами сервера баз данных.
Файл Radio_Subsystem.idl претерпел значительные изменения, и рассмотреть их следует еще до того момента, как мы примемся за другие объекты. Поэтому обратите внимание на листинг 2.
Интерфейс телефонного объекта Phone мало изменился, хотя некоторые уточнения имеются. Более важным является введение нового объекта канала данных. Для чего он служит? Ответ на этот вопрос кроется в самой схеме работы создаваемой системы. Пока телефон пользователя статичен, работа всех объектов не вызывает проблем. Но что произойдет, если во время разговора пользователь с телефоном будет перемещаться из зоны одного сотового передатчика в другую? Очевидно, произойдет разрыв связи. Чтобы избежать этого, мы должны предусмотреть некий механизм, позволяющий передавать установившийся канал связи между станциями. Сделать это несложно. Как вы помните, в техническом задании мы упоминали методику выбора телефоном подходящей соты. Для этого телефон раз в несколько секунд излучает в эфир тестовый сигнал вместе с объектной ссылкой, принимаемый ближайшими сотовыми ретрансляторами (в реальных сотовых системах все работает совершенно не так!). Они расшифровывают ссылку на объект телефона и замеряют силу радиосигнала, после чего вызывают операцию Phone::useThisCell с параметром, соответствующим размаху сигнала. Телефонный объект находит станцию с более сильным сигналом и подключается к ней. И так происходит каждые несколько секунд. Как только телефонный аппарат попадет в зону, где старая сота работает слабее, чем новая, произойдет переключение, а чтобы соединение не прервалось, в системе появляется новый объект DataChannel, инкапсулирующий тонкости связи между сотовыми ретрансляторами, к которым подключены разговаривающие абоненты. В процессе переключения достаточно изменить параметры этого канала, не разрывая его.
Сам интерфейс DataChannel содержит описание исключительной ситуации занятости канала ChannelBusyException на тот случай, если одна станция попытается передать блок данных еще до того, как был считан старый, пару атрибутов src и dst, являющихся ссылками на ретранслятор, инициировавший соединение (src), и ретранслятор-ответчик (dst). Чтобы сообщить о готовности переслать данные по каналу, сота может воспользоваться операцией DataChannel::passData. Специально для передачи пакета двоичных телефонных данных в модуль IDL введен массив:
typedef octet Packet[1024];
Настоящую реализацию объекта сотового телефона PhoneImpl.java можно видеть на листинге 3. После описания пакета, куда будет компилироваться объект, и директивы импорта:
package ru.pcworld; import ru.pcworld.Radio_Subsystem.*;
описывается собственно класс, наследующий скелет интерфейса, сгенерированный компилятором IDL и реализующий интерфейс Runnable, который позволяет создавать многопоточные приложения:
public class PhoneImpl extends PhonePOA implements Runnable {
Конструктор класса объекта получает уникальный идентификатор телефона и ссылку на уже инициализированный сервером брокер объектных запросов, после чего эти данные сохраняются во внутренних переменных:
public PhoneImpl(long device_id, org.omg.CORBA.ORB orb) { _device_id = device_id; _orb = orb; }
Чтобы начать генерировать тестовый сигнал, нужно вызвать метод startTestPolling() и передать ему ссылку на объект телефона, переведенную в строчный вид, после чего она будет передаваться в эфир. Собственно, генерация сигнала производится в методе run() потока, созданного и запущенного внутри метода startTestPolling():
{ _selfRef = selfRef; _poller = new Thread(this); _poller.start(); } public void run() { try { while(true) { _replied = false; _timeout = false; System.out.println(_selfRef); Thread.sleep(100); _timeout = true; if(_replied) public void startTestPolling(String selfRef) { _temp.willUseThisCell(_device_id, _selfRef, _cell); _cell = _temp; _signal = 0; System.out.println(_cell.getDataToDisplay()); } Thread.sleep(2000); } } catch(InterruptedException e) {} }
Обратите внимание на внутренние переменные _replied и _timeout. Первая «взводится», когда пришел ответ хотя бы от одного сотового передатчика. В противном случае телефон находится в необслуживаемой зоне либо абонент отключен от системы. Вторая переменная принимает значение true, когда время, отпущенное на получение ответов от сотовых станций, истекло. Логика же работы запущенного потока довольно проста. Сначала он излучает тестовый сигнал. Мы еще не знаем, как это будет происходить, это детали конкретной системной реализации, поэтому вместо конкретного исходного текста подставлен вывод на экран (см. выделенный участок). После этого поток тормозится на 0,1 с, чтобы принять в это время ответные сигналы. Когда истекает отпущенное время, прием сигналов блокируется и вызывается операция Cell::willUseThisCell, с помощью которой сота узнает, что она была выбрана телефоном, а заодно ей передается ссылка на предыдущую соту, к которой был подключен телефон. Это нужно, чтобы произвести переключение канала связи, если телефон в текущий момент находится в режиме разговора. Текст, который должен быть выведен на дисплей телефона, получается объектом посредством вызова операции Cell::getDataToDisplay. И наконец, поток опроса переходит в режим ожидания, после которого все происходит сначала. Для примера интервал выбран размером в 2 с.
Выбор соты по размаху принятого ею сигнала происходит в следующем методе:
public synchronized void useThisCell(Cell cell, short signal) { if(_timeout) return; if(signal > _signal) _temp = cell; _replied = true; // В этой зоне есть соты }
Пришедший сигнал сравнивается с предыдущим сохраненным, и каждая сота, чей сигнал мощнее, «вытесняет» более слабую. Для защиты от одновременного обращения к объекту со стороны множества ретрансляторов метод описан как synchronized.
Следующие несколько методов очень просты и не требуют комментариев. В основном они просто делегируют вызовы объекту сотовой станции. Метод receiveData() специально оставлен без реализации, поскольку в нем производятся действия по преобразованию цифрового сигнала в звуковой или его передача на модем компьютера, а эти процессы, как известно, весьма аппаратно-зависимые.
Осталось рассмотреть, как устроен сервер телефона. В самом начале статьи было сказано, что мы будем использовать сервант по умолчанию. А причина тому очень проста — зачем отдельно сохранять ссылку на сервант, если он всего один и заведомо известно, что любой запрос нужно будет адресовать именно этому единственному серванту?
Создание серванта по умолчанию начинается с определения политик адаптера объектов POA, причем предполагается, что брокер запросов уже инициализирован (см. листинг 4).
Корневой адаптер POA существует всегда, поэтому нужно лишь получить на него ссылку вызовом операции ORB::resolve_initial_references с единственным параметром «RootPOA»:
. . . POA rootPOA = POAHelper.narrow( orb.resolve_initial_references(?RootPOA?));
Политики MULTIPLE_ID и USE_DEFAULT_SERVANT необходимы для работы серванта по умолчанию, а NON_RETAIN не дает сохранить ссылку на сервант во внутренней таблице POA, чтобы объект не стал долгоживущим — это не входит в наши планы:
org.omg.CORBA.Policy[] policies = { rootPOA.create_id_uniqueness_policy( IdUniquenessPolicyValue.MULTIPLE_ID), rootPOA.create_servant_retention_policy( ServantRetentionPolicyValue.NON_RETAIN), rootPOA.create_request_processing_policy( RequestProcessingPolicyValue.USE_DEFAULT_SERVANT) };
Далее генерируется случайное число, уникально идентифицирующее телефон:
long uniqueId = Math.round(Long.MAX_VALUE * Math.random()); System.out.println(? уникальный номер телефона -> ? + uniqueId);
В реальной жизни каждый абонент сотовой сети получает свой номер во время регистрации.
Создание серванта сводится к вызову его конструктора оператором new:ru.pcworld.PhoneImpl phoneServant = new ru.pcworld.PhoneImpl(uniqueId, orb);
А вот теперь уже создается новый POA с политиками, которые мы определили, и произвольным именем phone_poa:
POA phonePOA = rootPOA.create_POA( ?phone_poa?, rootPOA.the_POAManager(), policies);
И у него задается сервант по умолчанию:
phonePOA.set_servant(phoneServant);
Вслед за этим адаптер объектов активизируется:
phonePOA.the_POAManager().activate();
и запускается поток-генератор тестовых сигналов:
phoneServant.startTestPolling( orb.object_to_string( phonePOA.create_reference( ru.pcworld.Radio_Subsystem.PhoneHelper.id())));
Остается ждать запросов:
orb.run(); . . .
Как вы, возможно, заметили, представляемый сервантом объект телефона — временный. А раз так, то нельзя получить ссылку на него. Сделано это специально, чтобы избежать незапланированных подключений к объекту, ведь на временные объекты нельзя явно получить ссылку. Поэтому единственный способ использовать объект — взять ссылку на него из тестового сигнала, который излучается телефоном. Короче, полная изоляция.