Построение распределенных систем на Java

Использование Java RMI

Разбить на страницы
Показывать лекцию целиком

Рабочий каталог расположен в Practice.

Технология, о которой мы говорили в предыдущем разделе, предполагает существенный объем программирования для реализации взаимодействия компонентов. Следующая рассматриваемая технология - RMI - позволяет существенно упростить разработку распределенных систем.

RMI дает возможность выполнять объекты Java на различных компьютерах или в отдельных процессах путем взаимодействия их друг с другом посредством удаленных вызовов методов. Технология RMI основана на более ранней подобной технологии удаленного вызова процедур ( RPC ) для процедурного программирования, разработанной в 80-х годах. RPC позволяет процедуре вызывать функцию на другом компьютере столь же легко, как если бы эта функция была частью программы, выполняющейся на том же компьютере. RPC выполняет всю работу по организации сетевых взаимодействий и маршалинга данных (т.е. пакетирования параметров функций и возврата значений для передачи их через сеть). Но RPC не подходит для передачи и возврата объектов Java, потому что она поддерживает ограниченный набор простых типов данных. Есть и другой недостаток у RPC - программисту необходимо знать специальный язык определения интерфейса ( IDL ) для описания функций, которые допускают удаленный вызов. Для устранения этих недостатков и была разработана технология RMI.

RMI представляет собой реализацию RPC на Java для распределенных коммуникационных взаимодействий "Java-объект - Java -объект". Объект Java регистрируется для удаленного доступа, что дает возможность клиентам получать удаленную ссылку на этот объект - она позволяет использовать этот объект дистанционно. Синтаксис вызова метода идентичен синтаксису вызова методов других объектов в той же программе. RMI обслуживает маршалинг данных через сеть и дает возможность программам на Java передавать законченные объекты Java с помощью механизма сериализации объектов Java. В составе J2SE имеются инструментальные средства создания требуемого кода для сетевых взаимодействий из определенных интерфейсов программы, это означает, что RMI не требует от программиста знания языка IDL. Кроме того, никакого нейтрального к языку IDL интерфейса не требуется, так как RMI поддерживает только Java ; достаточно собственных интерфейсов Java.

Создание распределенной системы с помощью RMI

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

В примере выполняются четыре основных действия:

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

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

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

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

    Определение удаленного интерфейса

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

    Удаленные методы, посредством которых клиент взаимодействует с удаленным объектом, используя RMI, должны быть определены в удаленном интерфейсе. Соответственно, первый этап при создании распределенного приложения с помощью RMI состоит в определении удаленного интерфейса, который описывает эти удаленные методы. Чтобы создать удаленный интерфейс, необходимо определить интерфейс, который будет расширять интерфейс java.rmi.Remote.Интерфейс Remote представляет собой тегирующий интерфейс - он не объявляет каких-либо методов, поэтому не обременен реализацией класса. Распределенное RMI -приложение должно экспортировать объект класса, который реализует интерфейс Remote,чтобы сделать этот удаленный объект доступным для приема удаленных вызовов метода из любой виртуальной машины Java, которая имеет соединение с компьютером, где выполняется удаленный объект.

    Интерфейс BillingService (пример 5.1), который расширяет интерфейс Remote (строка 9), представляет собой интерфейс для нашего удаленного объекта (сервера). В строках 10-17 объявляются методы для работы с пластиковыми картами. Удаленный объект должен реализовать все объявленные в удаленном интерфейсе методы.

    1  // BillingService.java
    2  // Интерфейс BillingService объявляет методы для работы
    3  // с пластиковыми картами
    4  package com.asw.rmi.ex1;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8
    9  public interface BillingService extends Remote {
    10  // определение новой карты
    11  public void addNewCard(String personName, String card) throws RemoteException;
    12  // добавить денежные средства на карту
    13  public void addMoney(String card, double money) throws RemoteException;
    14  // снять денежные средства с карты
    15  public void subMoney(String card, double money) throws RemoteException;
    16  // получение баланса карты
    17  public double getCardBalance(String card) throws RemoteException;
    18  }

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

    RMI использует механизм сериализации по умолчанию Java для передачи параметров методу и возврата значений через сеть. В связи с этим все параметры метода и возвращаемые значения должны иметь описатель Serializable или один из примитивных типов.

    Реализация удаленного интерфейса

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

    Класс UnicastRemoteObject (пакет java.rmi.server ) представляет базовые функциональные возможности, которые необходимы удаленным объектам для обслуживания удаленных запросов.

    Конструкторы и методы класса UnicastRemoteObject возбуждают контролируемое исключение RemoteException,поэтому подклассы класса UnicastRemoteObject должны определять конструкторы, которые также возбуждают исключение RemoteException.

    Конструктор класса UnicastRemoteObject экспортирует объект, чтобы сделать его доступным для приема удаленных вызовов. Экспорт объекта дает возможность удаленному объекту ожидать соединений с клиентами на анонимном порте (т.е. порте, выбираемом компьютером, на котором выполняется удаленный объект). Это дает возможность объекту осуществлять однонаправленное взаимодействие (взаимодействие "точка-точка" между двумя объектами посредством вызовов методов) с использованием стандартных соединений через сокеты. Классам удаленных объектов не нужно расширять этот класс, если эти классы применяют статический метод exportObject класса UnicastRemoteObject для экспорта удаленных объектов. Предполагается, что клиенты RMI должны осуществлять соединение на порте 1099 при попытке найти удаленный объект в реестре RMI. Перегруженный конструктор для класса UnicastRemoteObject дает возможность задавать дополнительную информацию, такую как номер порта для экспорта удаленного объекта. Для этого необходимо определить URL, который клиент может использовать для получения удаленной ссылки на объект. Эта ссылка применяется для вызова методов удаленного объекта. URL обычно имеет форму

    rmi://хост:порт/ИмяУдаленногоОбъекта,

    где хост представляет собой имя компьютера, который выполняет сервер реестра ( rmiregistry ) для удаленных объектов (он также является компьютером, на котором выполняется удаленный объект), порт представляет собой номер порта, на котором выполняется сервер реестра на хост-компьютере, а ИмяУдаленногоОбъекта - имя, которое клиент будет предоставлять при попытках обнаружить удаленный объект в реестре. Утилита rmi-registry обслуживает реестр удаленных объектов и является составной частью J2SE. Номер порта реестра RMI по умолчанию - 1099.

    Для связывания удаленного объекта с реестром используются методы bind или rebind.Метод rebind гарантирует, что если объект уже был зарегистрирован под заданным именем, новый удаленный объект заменит ранее зарегистрированный объект. Это может быть важно, если регистрируется новая версия существующего удаленного объекта.

    Класс BillingServiceImpl (пример 5.2) представляет собой удаленный объект, который реализует удаленный интерфейс BillingService.Клиент взаимодействует с объектом класса BillingServiceImpl,вызывая методы addNewCard, addMoney, subMoney, getCardBalance интерфейса BillingService для обработки информации по пластиковым картам. Класс BillingServiceImpl хранит сведения о картах в хэш-таблице (Hashtable),содержащей баланс пластиковой карты с именем посетителя (personName) и номером пластиковой карты ( card ), где номер карты является ключом таблицы.

    1  // BillingServicelmpl.java
    2  // BillingServiceImpl реализует удаленный интерфейс BillingService
      для
    3  // предоставления удаленного объекта BillingService
    4  package com.asw.rmi.ex1;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8  import java.util.*;
    9  import java.rmi.server.*; 
    10
    11  public class BillingServiceImpl extends UnicastRemoteObject
    12  implements BillingService {
    13
    14  private   Hashtable hash;   // хэш-таблица для хранения карт
    15  // инициализация сервера
    16  public BillingServiceImpl() throws RemoteException{
    17  super();
    18  hash = new Hashtable();
    19  }
    20
    21  // реализация метода addNewCard интерфейса BillingService
    22  public void addNewCard(String personName, String card)
    23  throws RemoteException {
    24
    25  hash.put(card, new Double(0.0));
    26  }
    27
    28  // реализация метода addMoney интерфейса BillingService
    29  public void addMoney(String card, double money) throws RemoteException {
    30  Double d = (Double)hash.get(card);
    31
    32  if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
    33  else throw new NotExistsCardOperation();
    34  }
    35
    36  // реализация метода subMoney интерфейса BillingService
    37  public void subMoney(String card, double money) throws RemoteException {
    38  Double d = (Double)hash.get(card);
    39
    40  if (d!=null) hash.put(card,new Double(d.doubleValue()-money));
    41  else throw new NotExistsCardOperation();
    42  }
    43
    44  // реализация метода getCardBalance интерфейса BillingService
    45  public double getCardBalance(String card) throws RemoteException {
    46  Double d = (Double)hash.get(card);
    47  if (d!=null) return d.doubleValue();
    48  else throw new NotExistsCardOperation();
    49  };
    50
    51  // запуск удаленного объекта BillingService
    52  public static void main (String[] args) throws Exception {
    53  System.out.println("Initializing BillingService...");
    54
    55  // создание удаленного объекта
    56  BillingService service = new BillingServiceImpl();
    57
    58  //задание имени удаленного объекта
    59  String serviceName = "rmi://localhost/BillingService";
    60  // регистрация удаленного объекта BillingService в реестре rmiregistry
    61  Naming.rebind(serviceName, service);
    62  }
    63
    64  }

    Класс BillingServiceImpl реализует методы addNewCard (строки 22-26), addMoney (строки 29-34), subMoney (строки 37-42), getCardBalance (строки 45-49) интерфейса BillingService,чтобы отвечать на удаленные запросы.

    Метод main (строки 52-62) создает удаленный объект BillingServiceImpl.Когда конструктор выполняется, он экспортирует удаленный объект, чтобы прослушивать удаленные запросы. В строке 59 определяется URL, который клиент может применить для получения удаленной ссылки на объект для вызова методов удаленного объекта.

    В этой программе URL удаленного объекта имеет вид rmi://localhost/BillingService. Из этого следует, что реестр RMI выполняется на машине localhost (т.е. на локальном компьютере), а для обнаружения клиентом сервиса нужно использовать имя BillingService.Имя localhost является синонимом IP -адреса 127.0.0.1 ( loopback ).

    В строке 61 вызывается статический метод rebind класса Naming (пакет java.rmi) для связывания удаленного объекта service класса BillingServiceImpl в реестре RMI с URL rmi://localhost/BillingService.

    Класс NotExistsCardOperation (пример 5.3) расширяет класс RemoteException.

    1  // NotExistsCardOperation.java
    2  package com.asw.rmi.ex1;
    3
    4  // Набор базовых пакетов Java
    5  import java.rmi.RemoteException;
    6
    7  public class NotExistsCardOperation extends RemoteException {
    8
    9  }

    Далее мы определяем клиентское приложение, которое будет обрабатывать запросы к пластиковым картам и пересылать их серверу. Для работы клиентского приложения необходимо знать URL вызываемого им удаленного объекта. Статический метод lookup класса Naming применяется для получения объектной ссылки на удаленный объект с заданным URL. Метод lookup осуществляет соединение с реестром RMI и возвращает удаленную ссылку на удаленный объект. Клиент может использовать эту удаленную ссылку, если она обращается к локальному объекту, выполняющемуся на той же виртуальной машине. Эта удаленная ссылка обращается к объекту-заглушке на клиенте. Заглушки дают возможность клиентам вызывать методы удаленного объекта. Объекты-заглушки принимают удаленные вызовы метода и передают эти вызовы RMI, который выполняет сетевые соединения, позволяющие клиентам взаимодействовать с удаленным объектом. Уровень RMI отвечает за сетевые соединения с удаленными объектами, поэтому обращения к удаленным объектам являются прозрачными для пользователя. RMI обслуживает соединение с удаленным объектом, передачу параметров и возврат значений.

    (рис 5.1) Архитектура RMI

    Класс BillingClient (пример 5.4) является клиентским приложением, которое вызывает удаленные методы addNewCard, addMoney и getCardBa-lance интерфейса BillingService для работы с пластиковыми картами посетителей через RMI. В нашем примере мы не реализуем реальную работу касс столовых с пластиковыми картами. Для иллюстрации определения и работы клиентского приложения нам достаточно произвести несколько операций с несколькими картами, используя удаленные методы удаленного объекта. В данном случае клиент в цикле заносит денежные средства на три пластиковые карты и в конце печатает результирующий баланс по этим пластиковым картам. При первом проходе цикла в случае отсутствия карт с заданными номерами клиент их создает.

    1  // BillingClient.java
    2  // BillingClient использует удаленный объект BillingService для
    3  // работы с информацией на пластиковых картах
    4  package com.asw.rmi.ex1;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8
    9  public class BillingClient {
    10  // выполнение BillingClient
    11  public static void main(String[] args) throws Exception]
    12  // создание строки, содержащей URL удаленного объекта
    13  String objectName = "rmi://"+args[0]+"/BillingService";
    14  System.out.println("Starting...\n");
    15  // соединение с реестром RMI и получение удаленной ссылки
    16  // на удаленный объект
    17  BillingService bs = (BillingService)Naming.lookup(objectName);
    18  System.out.println("done");
    19
    20  // начисление денежных средств на пластиковые карты
    21  for (int i = 0; i < 10000;  i++) {
    22  try {
    23  bs.addMoney("1", 1);
    24  } catch (RemoteException   e) {
    25  bs.addNewCard("Piter", "1");
    26  } 
    27
    28  try {
    29  bs.addMoney("2", 1);
    30  } catch (RemoteException   e) {
    31  bs.addNewCard("Stefan", "2");
    32  } 
    33
    34  try {
    35  bs.addMoney("3", 1);
    36  } catch (RemoteException   e) {
    37  bs.addNewCard("Nataly", "3");
    38  }
    39  }
    40  // печать текущего баланса обработанных карт
    41  System.out.println("1:"+bs.getCardBalance("1"));
    42  System.out.println("2:"+bs.getCardBalance("2"));
    43  System.out.println("3:"+bs.getCardBalance("3"));
    44  }
    45  }

    Метод main (строки 11-44) принимает в качестве параметра имя компьютера, на котором выполняется удаленный объект BillingService.В строке 13 создается строка objectName,которая содержит URL для нашего удаленного объекта. В строке 17 вызывается метод lookup класса Naming для получения удаленной ссылки на удаленный объект BillingService с заданным URL. В строках 21-38 производится добавление денежных средств на карты, а в строках 41-43 - печать текущего баланса этих карт.

    Компиляция и выполнение сервера и клиента

    Подготовив отдельные фрагменты, мы можем сформировать и выполнить наше распределенное приложение, но для этого потребуется несколько действий. Для начала необходимо компилировать исходные классы. Далее, нужно компилировать класс удаленного объекта ( ...Impl ), используя компилятор rmic (утилита J2SE ) для формирования класса-заглушки (о котором говорилось в предыдущем разделе). Этот класс должен быть доступен для клиента (либо локально, либо путем загрузки по сети), чтобы дать возможность устанавливать удаленное соединение с серверным объектом. В зависимости от параметров командной строки, передаваемых rmic,может быть сгенерировано несколько файлов. В Java 1.1 rmic формирует два класса - класс-заглушку и класс-каркас ( skeleton ). В Java 2 класс-каркас больше не требуется. Параметр командной строки -v1.2 указывает, что rmic следует создать только класс-заглушку.

    Для нашего примера командная строка для компиляции класса удаленного объекта будет выглядеть следующим образом:

    rmic -v1.2 com.asw.rmi.ex1.BillingServiceImpl

    которая сгенерирует файл BillingServiceImpl_Stub.class.

    Следующий этап - запуск реестра RMI, который зарегистрирует удаленный объект. Командная строка

    rmiregistry

    запускает реестр RMI на локальной машине. В окне командной строки в ответ на эту команду никакого текста отображаться не будет. Типичная ошибка заключается в том, что если не запустить реестр RMI прежде чем попытаться привязать удаленный объект к реестру, будет сгенерировано исключение java.rmi.ConnectException,которое указывает, что программа не может соединиться с реестром.

    Чтобы удаленный объект мог принимать удаленные вызовы методов, необходимо связать объект с именем в реестре RMI. Для этого нужно запустить серверное приложение из командной строки. В нашем случае командная строка выглядит так:

    java com.asw.rmi.ex1.BillingServiceImpl

    В результате отображается сообщение об инициализации BillingService.

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

    java com.asw.rmi.ex1.BillingClient

    соединит BillingClient с объектом BillingServiceImpl.

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

    java com.asw.rmi.ex1.BillingClient 192.168.1.1

    Несколько слов о синхронизации

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

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

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

    Схема выполнения метода addMoney двумя потоками
    t Клиент 1 Клиент 2
    t1 Вызов метода сервера addMoney с номером карты 1 и суммой 10 Вызов метода сервера addMoney с номером карты 1 и суммой 12
    t2 Double d = (Double)hash.get(card); Double d = (Double)hash.get(card);
    t3 if (d!=null) hash.put(card,new Double(d.doubleValue()+money))
    t4 if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
    t5

    Итак, в момент t1 оба клиента вызывают метод addMoney для карты с номером 1 и с разными суммами начисления (мы предполагаем, что карта с таким номером существует). Далее по каким-то причинам выполнение потоков, в которых происходит работа методов addMoney для первого и второго клиентов, происходит с разными скоростями. В момент времени t2 первый клиент получает из хэша значение баланса карты - второй делает то же самое. В момент времени t3 первый клиент изменяет значение баланса, прибавляя к нему свое начисление, и возвращает баланс в хэш. Ту же самую операцию, но только в момент времени t4 - позднее, чем первый - делает второй клиент. В результате приведенного сценария в хэше окажется значение баланса, измененное вторым клиентов, а изменения, которые сделал первый клиент, просто потеряются - т.е. созданное нами приложение отработает неправильно.

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

    public synchronized void addMoney(String card, double money) throws RemoteException {
      
      Double d = (Double)hash.get(card);
      
      if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
      else throw new NotExistsCardOperation();
    }

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

    Может показаться, что, введя синхронизацию на уровне методов, мы решили проблему, однако это не так. Чтобы убедиться в этом, достаточно рассмотреть одновременное выполнение двух разных методов - addMoney и subMoney - с теми же самыми предположениями о скоростях выполнения. Несложно догадаться, что в приведенном примере блокировка должна выполняться не на уровне методов, а на уровне ресурсов - в нашем случае хэш-таблицы. Для окончательного устранения нежелательного эффекта код всех методов изменения баланса должен быть переписан как-нибудь такПоступив таким образом, мы фактически превратили наше приложение в строго последовательное. В действительности нам вовсе не обязательно было синхронизировать доступ ко всей хэш-таблице, достаточно было бы синхронизации доступа к конкретной карте - при этом операции с разными картами могли бы выполняться параллельно (пример 5.6).

    public void addMoney(String card, double money) throws RemoteException{
        synchronized (hash) {
          Double d = (Double) hash.get(card);
    
          if (d != null)
            hash.put(card, new Double(d.doubleValue() + money));
          else throw new NotExistsCardOperation();
        }
      }

    Второй пример

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

    Определение удаленного интерфейса

    В отличие от предыдущего варианта интерфейс BillingService (пример 5.7) имеет не четыре, а три метода. Метод addNewCard (строка 11) заводит новую карту с указанными параметрами. Метод processOperations (строка 13) производит изменение баланса карт с указанными параметрами. В предыдущей реализации изменение баланса карты производилось двумя методами, один из которых увеличивал текущий баланс карты, другой - уменьшал, а величина изменения в любом случае была положительной. В данном случае величина изменения баланса может быть любой - как положительной, так и отрицательной; в первом случае это означает поступление денежных средств на баланс карты, во втором - списание денежных средств. Метод getCard (строка 15) по номеру карты возвращает экземпляр класса Card.

    1  // BillingService.java
    2  // Интерфейс BillingService объявляет методы для работы
    3  // с пластиковыми картами
    4  package com.asw.rmi.ex2; 
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8
    9  public interface BillingService extends Remote {
    10  // определение новой карты
    11  public void addNewCard(Card card) throws RemoteException;
    12  // изменение баланса карты
    13  public void processOperations(CardOperation[] operations) throws RemoteException;
    14  // получение баланса карты
    15      public Card getCard(String card) throws RemoteException;
    16  }

    Как уже говорилось ранее, RMI использует механизм сериализации по умолчанию Java для передачи параметров методу и возврата значений через сеть. Поэтому все классы, используемые в качестве параметров метода и возвращаемых значений, должны иметь описатель Serializable.

    Тип Card (пример 5.8) несет информацию об имени посетителя ( person ), дате получения карты (createDate),номере карты (cardNumber) и балансе (balance).

    1  // Card.java
    2  // описание типа переменных Card
    3  package com.asw.rmi.ex2;
    4
    5  // Набор базовых пакетов Java
    6  import java.io.Serializable;
    7  import java.util.*;
    8
    9  public class Card implements Serializable{
    10  public Card(String person, Date createDate, String cardNumber, double balance)]
    11  this.person = person;
    12  this.createDate = createDate;
    13  this.cardNumber = cardNumber;
    14  this.balance = balance;
    15  }
    16  public String person;
    17  public Date createDate;
    18  public String cardNumber;
    19  double balance;
    20  public String toString(){
    21  return "Card: cardNumber="+cardNumber+"\tBalance="+balance+
    22  +"\tPerson="+person+"\tCreateDate="+createDate+"";
    23  }
    24  }

    Тип CardOperation (пример 5.9) представляет собой одну операцию изменения баланса карты и несет информацию о номере карты (card),величине изменения баланса карты ( amount ) и дате операции (operationDate).

    1  // CardOperation.java
    2  // описание типа переменных CardOperation
    3  package com.asw.rmi.ex2;
    4
    5  // Набор базовых пакетов Java
    6  import java.util.*;
    7  import java.io.*;
    8
    9  public class CardOperation implements Serializable {
    10  public CardOperation(String card,double amount,Date operationDate){
    11  this.card = card;
    12  this.amount = amount;
    13  this.operationDate = operationDate;
    14  }
    15  public String card;
    16  public double amount;
    17  public Date operationDate;
    18  }

    Реализация удаленного интерфейса

    Класс BillingServiceImpl (пример 5.10) является удаленным объектом, который реализует удаленный интерфейс BillingService.Класс BillingServiceImpl реализует методы addNewCard (строки 22-25), processOperations (строки 28-36), getCard (строки 39-42) интерфейса BillingService,чтобы отвечать на удаленные запросы. Класс BillingServiceImpl хранит сведения о картах в хэш-таблице (Hashtable),содержащей карты (Card),где номер карты (cardNumber) является ключом таблицы.

    1  // BillingServicelmpl.java
    2  // BillingServiceImpl реализует удаленный интерфейс BillingService
    3  // для предоставления удаленного объекта BillingService
    4  package com.asw.rmi.ex2;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8  import java.util.*;
    9  import java.rmi.server.*;
    10
    11  public class BillingServiceImpl extends UnicastRemoteObject
    12  implements BillingService {
    13
    14  private   Hashtable hash;   // хэш-таблица для хранения карт
    15  // инициализация сервера
    16  public BillingServiceImpl() throws RemoteException]
    17  super();
    18  hash = new Hashtable();
    19  }
    20
    21  // реализация метода addNewCard интерфейса BillingService
    22  public void addNewCard(Card card) throws RemoteException {
    23
    24  hash.put(card.cardNumber, card);
    25  }
    26
    27  // реализация метода processOperations интерфейса BillingService
    28  public void processOperations(CardOperation[] operations)
    29  throws RemoteException {
    30  for (int i=0;i<operations.length;i++){
    31  Card c = (Card)hash.get(operations[i].card);
    32  if (c==null) throw new NotExistsCardOperation();
    33  c.balance+=operations[i].amount;
    34  hash.put(operations[i].card,c);
    35  }
    36  }
    37
    38  // реализация метода getCard интерфейса BillingService
    39  public Card getCard(String card) throws RemoteException{
    40  Card c = (Card)hash.get(card);
    41  return c;
    42  };
    43
    44  // запуск удаленного объекта BillingService
    45  public static void main (String[] args) throws Exception {
    46  System.out.println("Initializing BillingService...");
    47
    48  // создание удаленного объекта
    49  BillingService service = new BillingServiceImpl();
    50
    51  //задание имени удаленного объекта
    52  String serviceName = "rmi://localhost/BillingService";
    53  // регистрация удаленного объекта BillingService в реестре rmiregistry
    54  Naming.rebind(serviceName, service);
    55  }
    56
    57  }

    Метод main (строки 45-55) создает удаленный объект BillingServiceImpl.В строке 52 определяется URL, который клиент может применить для получения удаленной ссылки на объект для вызова методов удаленного объекта.

    В этой программе URL удаленного объекта имеет вид rmi://localhost/BillingService, т.е. реестр RMI выполняется на машине localhost (т.е. на локальном компьютере), а для обнаружения клиентом сервиса должно использоваться имя BillingService.Имя localhost является синонимом IP -адреса 127.0.0.1.

    В строке 54 вызывается статический метод rebind класса Naming (пакет java.rmi) для связывания удаленного объекта service класса BillingServiceImpl в реестре RMI с URL rmi://localhost/BillingService.

    Класс NotExistsCardOperation (пример 5.11) расширяет класс RemoteException.

    1  // NotExistsCardOperation.java
    2  package com.asw.rmi.ex2;
    3
    4  // Набор базовых пакетов Java
    5  import java.rmi.RemoteException;
    6
    7   public class NotExistsCardOperation extends RemoteException {
    8
    9  }

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

    Класс BillingClient (пример 5.12) является клиентским приложением, которое вызывает удаленные методы addNewCard, addMoney и getCardBalance интерфейса BillingService для работы с пластиковыми картами посетителей через RMI. Для иллюстрации работы клиентского приложения нам достаточно произвести несколько операций с несколькими картами, используя удаленные методы удаленного объекта. В данном случае, так же как и в первом примере, мы в цикле заносим денежные средства на три пластиковые карты, предварительно их создав, и в конце печатаем результирующий баланс по этим картам.

    1  // BillingClient.java
    2  // BillingClient использует удаленный объект BillingService для
    3  // работы с информацией на пластиковых картах
    4  package com.asw.rmi.ex2;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8  import java.util.Date;
    9
    10  public class BillingClient {
    11  // выполнение BillingClient
    12  public static void main(String[] args) throws Exception]
    13  // создание строки, содержащей URL удаленного объекта
    14  String objectName = "rmi://"+args[0]+"/BillingService";
    15  System.out.println("Starting...\n");
    16  // соединение с реестром RMI и получение удаленной ссылки
    17  // на удаленный объект
    18  BillingService bs = (BillingService)Naming.lookup(objectName);
    19  System.out.println("done");
    20
    21  // проверка на наличие карт с указанными номерами
    22  // в случае отсутствия карты с указанными параметрами
    23  // добавляем новую карту
    24  Card c;
    25  c = bs.getCard("1");
    26  if (c==null) {
    27  c = new Card("Piter",new Date(),"1",0.0);
    28  bs.addNewCard(c);
    29  } 
    30
    31  c = bs.getCard("2");
    32  if (c==null) {
    33  c = new Card("Stefan",new Date(),"2",0.0);
    34  bs.addNewCard(c);
    35  } 
    36
    37  c = bs.getCard("3");
    38  if (c==null) {
    39  c = new Card("Nataly",new Date(),"3",0.0);
    40  bs.addNewCard(c);
    41  } 
    42
    43  // определение массива операций по картам
    44  System.err.println("begin...\n");
    45  int cnt = 30000;
    46  CardOperation[] co = new CardOperation[cnt];
    47  for (int i = 0; i < i++;)  {
    48  switch (i%3){
    49  case 0:   co[i] = new CardOperation("1",1,new Date());break;
    50  case 1:   co[i] = new CardOperation("2",1,new Date());break;
    51  case 2:   co[i] = new CardOperation("3",1,new Date());break;
    52  }
    53  }
    54  // проведение указанных в массиве операций
    55  bs.processOperations(co);
    56
    57  // печать текущего баланса обработанных карт
    58  System.out.println(bs.getCard("1"));
    59  System.out.println(bs.getCard("2"));
    60  System.out.println(bs.getCard("3"));
    61  }
    62  }

    Метод main (строки 12-61) принимает в качестве параметра имя компьютера, на котором выполняется удаленный объект BillingService.В строке 14 создается строка objectName,которая содержит URL для нашего удаленного объекта. В строке 18 вызывается метод lookup класса Naming для получения удаленной ссылки на удаленный объект BillingService с заданным URL. В строках 24-41 производится проверка наличия карт с номерами №1, №2, №3, в случае их отсутствия производится добавление новых карт с соответствующими параметрами. В строках 44-53 определяется массив операций по указанным картам. В цикле в массив заносятся операции по добавлению денежных средств на указанные карты и в указанном количестве. В строке 55 метод processOperations производит изменение балансов согласно полученному в качестве параметра массиву операций по картам. В строках 58-60 печатаем текущий баланс карт с номерами №1, №2, №3.

    Компиляция и выполнение сервера и клиента

    Компиляция и запуск аналогичны для первого примера. Для начала необходимо компилировать классы. Далее нужно компилировать класс удаленного объекта (...Impl).

    Для нашего примера командная строка для компиляции класса удаленного объекта будет выглядеть следующим образом:

    rmic -v1.2 com.asw.rmi.ex2.BillingServiceImpl

    и она сгенерирует файл BillingServiceImplStub.class.

    Следующий этап - запуск реестра RMI, который зарегистрирует удаленный объект. Командная строка

    rmiregistry

    запускает реестр RMI на локальной машине.

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

    java com.asw.rmi.ex2.BillingServiceImpl

    В результате отображается сообщение об инициализации BillingService.

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

    java com.asw.rmi.ex2.BillingClient localhost

    соединит BillingClient с объектом BillingServiceImpl.

    Сравнение двух реализаций

    Несмотря на то, что два рассмотренных примера делают практически одно и то же, они все же достаточно сильно различаются. О первом отличии уже говорилось, однако нелишним будет еще раз акцентировать внимание на том, что во втором примере осуществлена передача пользовательских типов от клиента к серверу. И для того, чтобы это реализовать, нам не потребовалось предпринимать никаких действий, кроме объявления наших классов как сериализуемые. Абсолютно всю техническую работу сделала за нас RMI. Второе отличие заключается в скорости работы этих двух приложений - второе приложение работает гораздо быстрее. Связано это с тем, что вызов удаленного метода связан с довольно большими накладными расходами, заметную часть которых составляет время работы системной "обвязки" - чем меньше вызовов, тем этот вклад меньше. Свой вклад, безусловно, вносит и передача данных по сети - передача большого количества мелких пакетов опять же связана с большими накладными расходами, чем передача одного объемного.

    Страницы:

    Рабочий каталог расположен в Practice.

    Технология, о которой мы говорили в предыдущем разделе, предполагает существенный объем программирования для реализации взаимодействия компонентов. Следующая рассматриваемая технология - RMI - позволяет существенно упростить разработку распределенных систем.

    RMI дает возможность выполнять объекты Java на различных компьютерах или в отдельных процессах путем взаимодействия их друг с другом посредством удаленных вызовов методов. Технология RMI основана на более ранней подобной технологии удаленного вызова процедур ( RPC ) для процедурного программирования, разработанной в 80-х годах. RPC позволяет процедуре вызывать функцию на другом компьютере столь же легко, как если бы эта функция была частью программы, выполняющейся на том же компьютере. RPC выполняет всю работу по организации сетевых взаимодействий и маршалинга данных (т.е. пакетирования параметров функций и возврата значений для передачи их через сеть). Но RPC не подходит для передачи и возврата объектов Java, потому что она поддерживает ограниченный набор простых типов данных. Есть и другой недостаток у RPC - программисту необходимо знать специальный язык определения интерфейса ( IDL ) для описания функций, которые допускают удаленный вызов. Для устранения этих недостатков и была разработана технология RMI.

    RMI представляет собой реализацию RPC на Java для распределенных коммуникационных взаимодействий "Java-объект - Java -объект". Объект Java регистрируется для удаленного доступа, что дает возможность клиентам получать удаленную ссылку на этот объект - она позволяет использовать этот объект дистанционно. Синтаксис вызова метода идентичен синтаксису вызова методов других объектов в той же программе. RMI обслуживает маршалинг данных через сеть и дает возможность программам на Java передавать законченные объекты Java с помощью механизма сериализации объектов Java. В составе J2SE имеются инструментальные средства создания требуемого кода для сетевых взаимодействий из определенных интерфейсов программы, это означает, что RMI не требует от программиста знания языка IDL. Кроме того, никакого нейтрального к языку IDL интерфейса не требуется, так как RMI поддерживает только Java ; достаточно собственных интерфейсов Java.

    Создание распределенной системы с помощью RMI

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

    В примере выполняются четыре основных действия:

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

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

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

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

    Определение удаленного интерфейса

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

    Удаленные методы, посредством которых клиент взаимодействует с удаленным объектом, используя RMI, должны быть определены в удаленном интерфейсе. Соответственно, первый этап при создании распределенного приложения с помощью RMI состоит в определении удаленного интерфейса, который описывает эти удаленные методы. Чтобы создать удаленный интерфейс, необходимо определить интерфейс, который будет расширять интерфейс java.rmi.Remote.Интерфейс Remote представляет собой тегирующий интерфейс - он не объявляет каких-либо методов, поэтому не обременен реализацией класса. Распределенное RMI -приложение должно экспортировать объект класса, который реализует интерфейс Remote,чтобы сделать этот удаленный объект доступным для приема удаленных вызовов метода из любой виртуальной машины Java, которая имеет соединение с компьютером, где выполняется удаленный объект.

    Интерфейс BillingService (пример 5.1), который расширяет интерфейс Remote (строка 9), представляет собой интерфейс для нашего удаленного объекта (сервера). В строках 10-17 объявляются методы для работы с пластиковыми картами. Удаленный объект должен реализовать все объявленные в удаленном интерфейсе методы.

    1  // BillingService.java
    2  // Интерфейс BillingService объявляет методы для работы
    3  // с пластиковыми картами
    4  package com.asw.rmi.ex1;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8
    9  public interface BillingService extends Remote {
    10  // определение новой карты
    11  public void addNewCard(String personName, String card) throws RemoteException;
    12  // добавить денежные средства на карту
    13  public void addMoney(String card, double money) throws RemoteException;
    14  // снять денежные средства с карты
    15  public void subMoney(String card, double money) throws RemoteException;
    16  // получение баланса карты
    17  public double getCardBalance(String card) throws RemoteException;
    18  }

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

    RMI использует механизм сериализации по умолчанию Java для передачи параметров методу и возврата значений через сеть. В связи с этим все параметры метода и возвращаемые значения должны иметь описатель Serializable или один из примитивных типов.

    Реализация удаленного интерфейса

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

    Класс UnicastRemoteObject (пакет java.rmi.server ) представляет базовые функциональные возможности, которые необходимы удаленным объектам для обслуживания удаленных запросов.

    Конструкторы и методы класса UnicastRemoteObject возбуждают контролируемое исключение RemoteException,поэтому подклассы класса UnicastRemoteObject должны определять конструкторы, которые также возбуждают исключение RemoteException.

    Конструктор класса UnicastRemoteObject экспортирует объект, чтобы сделать его доступным для приема удаленных вызовов. Экспорт объекта дает возможность удаленному объекту ожидать соединений с клиентами на анонимном порте (т.е. порте, выбираемом компьютером, на котором выполняется удаленный объект). Это дает возможность объекту осуществлять однонаправленное взаимодействие (взаимодействие "точка-точка" между двумя объектами посредством вызовов методов) с использованием стандартных соединений через сокеты. Классам удаленных объектов не нужно расширять этот класс, если эти классы применяют статический метод exportObject класса UnicastRemoteObject для экспорта удаленных объектов. Предполагается, что клиенты RMI должны осуществлять соединение на порте 1099 при попытке найти удаленный объект в реестре RMI. Перегруженный конструктор для класса UnicastRemoteObject дает возможность задавать дополнительную информацию, такую как номер порта для экспорта удаленного объекта. Для этого необходимо определить URL, который клиент может использовать для получения удаленной ссылки на объект. Эта ссылка применяется для вызова методов удаленного объекта. URL обычно имеет форму

    rmi://хост:порт/ИмяУдаленногоОбъекта,

    где хост представляет собой имя компьютера, который выполняет сервер реестра ( rmiregistry ) для удаленных объектов (он также является компьютером, на котором выполняется удаленный объект), порт представляет собой номер порта, на котором выполняется сервер реестра на хост-компьютере, а ИмяУдаленногоОбъекта - имя, которое клиент будет предоставлять при попытках обнаружить удаленный объект в реестре. Утилита rmi-registry обслуживает реестр удаленных объектов и является составной частью J2SE. Номер порта реестра RMI по умолчанию - 1099.

    Для связывания удаленного объекта с реестром используются методы bind или rebind.Метод rebind гарантирует, что если объект уже был зарегистрирован под заданным именем, новый удаленный объект заменит ранее зарегистрированный объект. Это может быть важно, если регистрируется новая версия существующего удаленного объекта.

    Класс BillingServiceImpl (пример 5.2) представляет собой удаленный объект, который реализует удаленный интерфейс BillingService.Клиент взаимодействует с объектом класса BillingServiceImpl,вызывая методы addNewCard, addMoney, subMoney, getCardBalance интерфейса BillingService для обработки информации по пластиковым картам. Класс BillingServiceImpl хранит сведения о картах в хэш-таблице (Hashtable),содержащей баланс пластиковой карты с именем посетителя (personName) и номером пластиковой карты ( card ), где номер карты является ключом таблицы.

    1  // BillingServicelmpl.java
    2  // BillingServiceImpl реализует удаленный интерфейс BillingService
      для
    3  // предоставления удаленного объекта BillingService
    4  package com.asw.rmi.ex1;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8  import java.util.*;
    9  import java.rmi.server.*; 
    10
    11  public class BillingServiceImpl extends UnicastRemoteObject
    12  implements BillingService {
    13
    14  private   Hashtable hash;   // хэш-таблица для хранения карт
    15  // инициализация сервера
    16  public BillingServiceImpl() throws RemoteException{
    17  super();
    18  hash = new Hashtable();
    19  }
    20
    21  // реализация метода addNewCard интерфейса BillingService
    22  public void addNewCard(String personName, String card)
    23  throws RemoteException {
    24
    25  hash.put(card, new Double(0.0));
    26  }
    27
    28  // реализация метода addMoney интерфейса BillingService
    29  public void addMoney(String card, double money) throws RemoteException {
    30  Double d = (Double)hash.get(card);
    31
    32  if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
    33  else throw new NotExistsCardOperation();
    34  }
    35
    36  // реализация метода subMoney интерфейса BillingService
    37  public void subMoney(String card, double money) throws RemoteException {
    38  Double d = (Double)hash.get(card);
    39
    40  if (d!=null) hash.put(card,new Double(d.doubleValue()-money));
    41  else throw new NotExistsCardOperation();
    42  }
    43
    44  // реализация метода getCardBalance интерфейса BillingService
    45  public double getCardBalance(String card) throws RemoteException {
    46  Double d = (Double)hash.get(card);
    47  if (d!=null) return d.doubleValue();
    48  else throw new NotExistsCardOperation();
    49  };
    50
    51  // запуск удаленного объекта BillingService
    52  public static void main (String[] args) throws Exception {
    53  System.out.println("Initializing BillingService...");
    54
    55  // создание удаленного объекта
    56  BillingService service = new BillingServiceImpl();
    57
    58  //задание имени удаленного объекта
    59  String serviceName = "rmi://localhost/BillingService";
    60  // регистрация удаленного объекта BillingService в реестре rmiregistry
    61  Naming.rebind(serviceName, service);
    62  }
    63
    64  }

    Класс BillingServiceImpl реализует методы addNewCard (строки 22-26), addMoney (строки 29-34), subMoney (строки 37-42), getCardBalance (строки 45-49) интерфейса BillingService,чтобы отвечать на удаленные запросы.

    Метод main (строки 52-62) создает удаленный объект BillingServiceImpl.Когда конструктор выполняется, он экспортирует удаленный объект, чтобы прослушивать удаленные запросы. В строке 59 определяется URL, который клиент может применить для получения удаленной ссылки на объект для вызова методов удаленного объекта.

    В этой программе URL удаленного объекта имеет вид rmi://localhost/BillingService. Из этого следует, что реестр RMI выполняется на машине localhost (т.е. на локальном компьютере), а для обнаружения клиентом сервиса нужно использовать имя BillingService.Имя localhost является синонимом IP -адреса 127.0.0.1 ( loopback ).

    В строке 61 вызывается статический метод rebind класса Naming (пакет java.rmi) для связывания удаленного объекта service класса BillingServiceImpl в реестре RMI с URL rmi://localhost/BillingService.

    Класс NotExistsCardOperation (пример 5.3) расширяет класс RemoteException.

    1  // NotExistsCardOperation.java
    2  package com.asw.rmi.ex1;
    3
    4  // Набор базовых пакетов Java
    5  import java.rmi.RemoteException;
    6
    7  public class NotExistsCardOperation extends RemoteException {
    8
    9  }

    Далее мы определяем клиентское приложение, которое будет обрабатывать запросы к пластиковым картам и пересылать их серверу. Для работы клиентского приложения необходимо знать URL вызываемого им удаленного объекта. Статический метод lookup класса Naming применяется для получения объектной ссылки на удаленный объект с заданным URL. Метод lookup осуществляет соединение с реестром RMI и возвращает удаленную ссылку на удаленный объект. Клиент может использовать эту удаленную ссылку, если она обращается к локальному объекту, выполняющемуся на той же виртуальной машине. Эта удаленная ссылка обращается к объекту-заглушке на клиенте. Заглушки дают возможность клиентам вызывать методы удаленного объекта. Объекты-заглушки принимают удаленные вызовы метода и передают эти вызовы RMI, который выполняет сетевые соединения, позволяющие клиентам взаимодействовать с удаленным объектом. Уровень RMI отвечает за сетевые соединения с удаленными объектами, поэтому обращения к удаленным объектам являются прозрачными для пользователя. RMI обслуживает соединение с удаленным объектом, передачу параметров и возврат значений.

    (рис 5.1) Архитектура RMI

    Класс BillingClient (пример 5.4) является клиентским приложением, которое вызывает удаленные методы addNewCard, addMoney и getCardBa-lance интерфейса BillingService для работы с пластиковыми картами посетителей через RMI. В нашем примере мы не реализуем реальную работу касс столовых с пластиковыми картами. Для иллюстрации определения и работы клиентского приложения нам достаточно произвести несколько операций с несколькими картами, используя удаленные методы удаленного объекта. В данном случае клиент в цикле заносит денежные средства на три пластиковые карты и в конце печатает результирующий баланс по этим пластиковым картам. При первом проходе цикла в случае отсутствия карт с заданными номерами клиент их создает.

    1  // BillingClient.java
    2  // BillingClient использует удаленный объект BillingService для
    3  // работы с информацией на пластиковых картах
    4  package com.asw.rmi.ex1;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8
    9  public class BillingClient {
    10  // выполнение BillingClient
    11  public static void main(String[] args) throws Exception]
    12  // создание строки, содержащей URL удаленного объекта
    13  String objectName = "rmi://"+args[0]+"/BillingService";
    14  System.out.println("Starting...\n");
    15  // соединение с реестром RMI и получение удаленной ссылки
    16  // на удаленный объект
    17  BillingService bs = (BillingService)Naming.lookup(objectName);
    18  System.out.println("done");
    19
    20  // начисление денежных средств на пластиковые карты
    21  for (int i = 0; i < 10000;  i++) {
    22  try {
    23  bs.addMoney("1", 1);
    24  } catch (RemoteException   e) {
    25  bs.addNewCard("Piter", "1");
    26  } 
    27
    28  try {
    29  bs.addMoney("2", 1);
    30  } catch (RemoteException   e) {
    31  bs.addNewCard("Stefan", "2");
    32  } 
    33
    34  try {
    35  bs.addMoney("3", 1);
    36  } catch (RemoteException   e) {
    37  bs.addNewCard("Nataly", "3");
    38  }
    39  }
    40  // печать текущего баланса обработанных карт
    41  System.out.println("1:"+bs.getCardBalance("1"));
    42  System.out.println("2:"+bs.getCardBalance("2"));
    43  System.out.println("3:"+bs.getCardBalance("3"));
    44  }
    45  }

    Метод main (строки 11-44) принимает в качестве параметра имя компьютера, на котором выполняется удаленный объект BillingService.В строке 13 создается строка objectName,которая содержит URL для нашего удаленного объекта. В строке 17 вызывается метод lookup класса Naming для получения удаленной ссылки на удаленный объект BillingService с заданным URL. В строках 21-38 производится добавление денежных средств на карты, а в строках 41-43 - печать текущего баланса этих карт.

    Компиляция и выполнение сервера и клиента

    Подготовив отдельные фрагменты, мы можем сформировать и выполнить наше распределенное приложение, но для этого потребуется несколько действий. Для начала необходимо компилировать исходные классы. Далее, нужно компилировать класс удаленного объекта ( ...Impl ), используя компилятор rmic (утилита J2SE ) для формирования класса-заглушки (о котором говорилось в предыдущем разделе). Этот класс должен быть доступен для клиента (либо локально, либо путем загрузки по сети), чтобы дать возможность устанавливать удаленное соединение с серверным объектом. В зависимости от параметров командной строки, передаваемых rmic,может быть сгенерировано несколько файлов. В Java 1.1 rmic формирует два класса - класс-заглушку и класс-каркас ( skeleton ). В Java 2 класс-каркас больше не требуется. Параметр командной строки -v1.2 указывает, что rmic следует создать только класс-заглушку.

    Для нашего примера командная строка для компиляции класса удаленного объекта будет выглядеть следующим образом:

    rmic -v1.2 com.asw.rmi.ex1.BillingServiceImpl

    которая сгенерирует файл BillingServiceImpl_Stub.class.

    Следующий этап - запуск реестра RMI, который зарегистрирует удаленный объект. Командная строка

    rmiregistry

    запускает реестр RMI на локальной машине. В окне командной строки в ответ на эту команду никакого текста отображаться не будет. Типичная ошибка заключается в том, что если не запустить реестр RMI прежде чем попытаться привязать удаленный объект к реестру, будет сгенерировано исключение java.rmi.ConnectException,которое указывает, что программа не может соединиться с реестром.

    Чтобы удаленный объект мог принимать удаленные вызовы методов, необходимо связать объект с именем в реестре RMI. Для этого нужно запустить серверное приложение из командной строки. В нашем случае командная строка выглядит так:

    java com.asw.rmi.ex1.BillingServiceImpl

    В результате отображается сообщение об инициализации BillingService.

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

    java com.asw.rmi.ex1.BillingClient

    соединит BillingClient с объектом BillingServiceImpl.

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

    java com.asw.rmi.ex1.BillingClient 192.168.1.1

    Несколько слов о синхронизации

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

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

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

    Схема выполнения метода addMoney двумя потоками
    t Клиент 1 Клиент 2
    t1 Вызов метода сервера addMoney с номером карты 1 и суммой 10 Вызов метода сервера addMoney с номером карты 1 и суммой 12
    t2 Double d = (Double)hash.get(card); Double d = (Double)hash.get(card);
    t3 if (d!=null) hash.put(card,new Double(d.doubleValue()+money))
    t4 if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
    t5

    Итак, в момент t1 оба клиента вызывают метод addMoney для карты с номером 1 и с разными суммами начисления (мы предполагаем, что карта с таким номером существует). Далее по каким-то причинам выполнение потоков, в которых происходит работа методов addMoney для первого и второго клиентов, происходит с разными скоростями. В момент времени t2 первый клиент получает из хэша значение баланса карты - второй делает то же самое. В момент времени t3 первый клиент изменяет значение баланса, прибавляя к нему свое начисление, и возвращает баланс в хэш. Ту же самую операцию, но только в момент времени t4 - позднее, чем первый - делает второй клиент. В результате приведенного сценария в хэше окажется значение баланса, измененное вторым клиентов, а изменения, которые сделал первый клиент, просто потеряются - т.е. созданное нами приложение отработает неправильно.

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

    public synchronized void addMoney(String card, double money) throws RemoteException {
      
      Double d = (Double)hash.get(card);
      
      if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
      else throw new NotExistsCardOperation();
    }

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

    Может показаться, что, введя синхронизацию на уровне методов, мы решили проблему, однако это не так. Чтобы убедиться в этом, достаточно рассмотреть одновременное выполнение двух разных методов - addMoney и subMoney - с теми же самыми предположениями о скоростях выполнения. Несложно догадаться, что в приведенном примере блокировка должна выполняться не на уровне методов, а на уровне ресурсов - в нашем случае хэш-таблицы. Для окончательного устранения нежелательного эффекта код всех методов изменения баланса должен быть переписан как-нибудь такПоступив таким образом, мы фактически превратили наше приложение в строго последовательное. В действительности нам вовсе не обязательно было синхронизировать доступ ко всей хэш-таблице, достаточно было бы синхронизации доступа к конкретной карте - при этом операции с разными картами могли бы выполняться параллельно (пример 5.6).

    public void addMoney(String card, double money) throws RemoteException{
        synchronized (hash) {
          Double d = (Double) hash.get(card);
    
          if (d != null)
            hash.put(card, new Double(d.doubleValue() + money));
          else throw new NotExistsCardOperation();
        }
      }

    Второй пример

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

    Определение удаленного интерфейса

    В отличие от предыдущего варианта интерфейс BillingService (пример 5.7) имеет не четыре, а три метода. Метод addNewCard (строка 11) заводит новую карту с указанными параметрами. Метод processOperations (строка 13) производит изменение баланса карт с указанными параметрами. В предыдущей реализации изменение баланса карты производилось двумя методами, один из которых увеличивал текущий баланс карты, другой - уменьшал, а величина изменения в любом случае была положительной. В данном случае величина изменения баланса может быть любой - как положительной, так и отрицательной; в первом случае это означает поступление денежных средств на баланс карты, во втором - списание денежных средств. Метод getCard (строка 15) по номеру карты возвращает экземпляр класса Card.

    1  // BillingService.java
    2  // Интерфейс BillingService объявляет методы для работы
    3  // с пластиковыми картами
    4  package com.asw.rmi.ex2; 
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8
    9  public interface BillingService extends Remote {
    10  // определение новой карты
    11  public void addNewCard(Card card) throws RemoteException;
    12  // изменение баланса карты
    13  public void processOperations(CardOperation[] operations) throws RemoteException;
    14  // получение баланса карты
    15      public Card getCard(String card) throws RemoteException;
    16  }

    Как уже говорилось ранее, RMI использует механизм сериализации по умолчанию Java для передачи параметров методу и возврата значений через сеть. Поэтому все классы, используемые в качестве параметров метода и возвращаемых значений, должны иметь описатель Serializable.

    Тип Card (пример 5.8) несет информацию об имени посетителя ( person ), дате получения карты (createDate),номере карты (cardNumber) и балансе (balance).

    1  // Card.java
    2  // описание типа переменных Card
    3  package com.asw.rmi.ex2;
    4
    5  // Набор базовых пакетов Java
    6  import java.io.Serializable;
    7  import java.util.*;
    8
    9  public class Card implements Serializable{
    10  public Card(String person, Date createDate, String cardNumber, double balance)]
    11  this.person = person;
    12  this.createDate = createDate;
    13  this.cardNumber = cardNumber;
    14  this.balance = balance;
    15  }
    16  public String person;
    17  public Date createDate;
    18  public String cardNumber;
    19  double balance;
    20  public String toString(){
    21  return "Card: cardNumber="+cardNumber+"\tBalance="+balance+
    22  +"\tPerson="+person+"\tCreateDate="+createDate+"";
    23  }
    24  }

    Тип CardOperation (пример 5.9) представляет собой одну операцию изменения баланса карты и несет информацию о номере карты (card),величине изменения баланса карты ( amount ) и дате операции (operationDate).

    1  // CardOperation.java
    2  // описание типа переменных CardOperation
    3  package com.asw.rmi.ex2;
    4
    5  // Набор базовых пакетов Java
    6  import java.util.*;
    7  import java.io.*;
    8
    9  public class CardOperation implements Serializable {
    10  public CardOperation(String card,double amount,Date operationDate){
    11  this.card = card;
    12  this.amount = amount;
    13  this.operationDate = operationDate;
    14  }
    15  public String card;
    16  public double amount;
    17  public Date operationDate;
    18  }

    Реализация удаленного интерфейса

    Класс BillingServiceImpl (пример 5.10) является удаленным объектом, который реализует удаленный интерфейс BillingService.Класс BillingServiceImpl реализует методы addNewCard (строки 22-25), processOperations (строки 28-36), getCard (строки 39-42) интерфейса BillingService,чтобы отвечать на удаленные запросы. Класс BillingServiceImpl хранит сведения о картах в хэш-таблице (Hashtable),содержащей карты (Card),где номер карты (cardNumber) является ключом таблицы.

    1  // BillingServicelmpl.java
    2  // BillingServiceImpl реализует удаленный интерфейс BillingService
    3  // для предоставления удаленного объекта BillingService
    4  package com.asw.rmi.ex2;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8  import java.util.*;
    9  import java.rmi.server.*;
    10
    11  public class BillingServiceImpl extends UnicastRemoteObject
    12  implements BillingService {
    13
    14  private   Hashtable hash;   // хэш-таблица для хранения карт
    15  // инициализация сервера
    16  public BillingServiceImpl() throws RemoteException]
    17  super();
    18  hash = new Hashtable();
    19  }
    20
    21  // реализация метода addNewCard интерфейса BillingService
    22  public void addNewCard(Card card) throws RemoteException {
    23
    24  hash.put(card.cardNumber, card);
    25  }
    26
    27  // реализация метода processOperations интерфейса BillingService
    28  public void processOperations(CardOperation[] operations)
    29  throws RemoteException {
    30  for (int i=0;i<operations.length;i++){
    31  Card c = (Card)hash.get(operations[i].card);
    32  if (c==null) throw new NotExistsCardOperation();
    33  c.balance+=operations[i].amount;
    34  hash.put(operations[i].card,c);
    35  }
    36  }
    37
    38  // реализация метода getCard интерфейса BillingService
    39  public Card getCard(String card) throws RemoteException{
    40  Card c = (Card)hash.get(card);
    41  return c;
    42  };
    43
    44  // запуск удаленного объекта BillingService
    45  public static void main (String[] args) throws Exception {
    46  System.out.println("Initializing BillingService...");
    47
    48  // создание удаленного объекта
    49  BillingService service = new BillingServiceImpl();
    50
    51  //задание имени удаленного объекта
    52  String serviceName = "rmi://localhost/BillingService";
    53  // регистрация удаленного объекта BillingService в реестре rmiregistry
    54  Naming.rebind(serviceName, service);
    55  }
    56
    57  }

    Метод main (строки 45-55) создает удаленный объект BillingServiceImpl.В строке 52 определяется URL, который клиент может применить для получения удаленной ссылки на объект для вызова методов удаленного объекта.

    В этой программе URL удаленного объекта имеет вид rmi://localhost/BillingService, т.е. реестр RMI выполняется на машине localhost (т.е. на локальном компьютере), а для обнаружения клиентом сервиса должно использоваться имя BillingService.Имя localhost является синонимом IP -адреса 127.0.0.1.

    В строке 54 вызывается статический метод rebind класса Naming (пакет java.rmi) для связывания удаленного объекта service класса BillingServiceImpl в реестре RMI с URL rmi://localhost/BillingService.

    Класс NotExistsCardOperation (пример 5.11) расширяет класс RemoteException.

    1  // NotExistsCardOperation.java
    2  package com.asw.rmi.ex2;
    3
    4  // Набор базовых пакетов Java
    5  import java.rmi.RemoteException;
    6
    7   public class NotExistsCardOperation extends RemoteException {
    8
    9  }

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

    Класс BillingClient (пример 5.12) является клиентским приложением, которое вызывает удаленные методы addNewCard, addMoney и getCardBalance интерфейса BillingService для работы с пластиковыми картами посетителей через RMI. Для иллюстрации работы клиентского приложения нам достаточно произвести несколько операций с несколькими картами, используя удаленные методы удаленного объекта. В данном случае, так же как и в первом примере, мы в цикле заносим денежные средства на три пластиковые карты, предварительно их создав, и в конце печатаем результирующий баланс по этим картам.

    1  // BillingClient.java
    2  // BillingClient использует удаленный объект BillingService для
    3  // работы с информацией на пластиковых картах
    4  package com.asw.rmi.ex2;
    5
    6  // Набор базовых пакетов Java
    7  import java.rmi.*;
    8  import java.util.Date;
    9
    10  public class BillingClient {
    11  // выполнение BillingClient
    12  public static void main(String[] args) throws Exception]
    13  // создание строки, содержащей URL удаленного объекта
    14  String objectName = "rmi://"+args[0]+"/BillingService";
    15  System.out.println("Starting...\n");
    16  // соединение с реестром RMI и получение удаленной ссылки
    17  // на удаленный объект
    18  BillingService bs = (BillingService)Naming.lookup(objectName);
    19  System.out.println("done");
    20
    21  // проверка на наличие карт с указанными номерами
    22  // в случае отсутствия карты с указанными параметрами
    23  // добавляем новую карту
    24  Card c;
    25  c = bs.getCard("1");
    26  if (c==null) {
    27  c = new Card("Piter",new Date(),"1",0.0);
    28  bs.addNewCard(c);
    29  } 
    30
    31  c = bs.getCard("2");
    32  if (c==null) {
    33  c = new Card("Stefan",new Date(),"2",0.0);
    34  bs.addNewCard(c);
    35  } 
    36
    37  c = bs.getCard("3");
    38  if (c==null) {
    39  c = new Card("Nataly",new Date(),"3",0.0);
    40  bs.addNewCard(c);
    41  } 
    42
    43  // определение массива операций по картам
    44  System.err.println("begin...\n");
    45  int cnt = 30000;
    46  CardOperation[] co = new CardOperation[cnt];
    47  for (int i = 0; i < i++;)  {
    48  switch (i%3){
    49  case 0:   co[i] = new CardOperation("1",1,new Date());break;
    50  case 1:   co[i] = new CardOperation("2",1,new Date());break;
    51  case 2:   co[i] = new CardOperation("3",1,new Date());break;
    52  }
    53  }
    54  // проведение указанных в массиве операций
    55  bs.processOperations(co);
    56
    57  // печать текущего баланса обработанных карт
    58  System.out.println(bs.getCard("1"));
    59  System.out.println(bs.getCard("2"));
    60  System.out.println(bs.getCard("3"));
    61  }
    62  }

    Метод main (строки 12-61) принимает в качестве параметра имя компьютера, на котором выполняется удаленный объект BillingService.В строке 14 создается строка objectName,которая содержит URL для нашего удаленного объекта. В строке 18 вызывается метод lookup класса Naming для получения удаленной ссылки на удаленный объект BillingService с заданным URL. В строках 24-41 производится проверка наличия карт с номерами №1, №2, №3, в случае их отсутствия производится добавление новых карт с соответствующими параметрами. В строках 44-53 определяется массив операций по указанным картам. В цикле в массив заносятся операции по добавлению денежных средств на указанные карты и в указанном количестве. В строке 55 метод processOperations производит изменение балансов согласно полученному в качестве параметра массиву операций по картам. В строках 58-60 печатаем текущий баланс карт с номерами №1, №2, №3.

    Компиляция и выполнение сервера и клиента

    Компиляция и запуск аналогичны для первого примера. Для начала необходимо компилировать классы. Далее нужно компилировать класс удаленного объекта (...Impl).

    Для нашего примера командная строка для компиляции класса удаленного объекта будет выглядеть следующим образом:

    rmic -v1.2 com.asw.rmi.ex2.BillingServiceImpl

    и она сгенерирует файл BillingServiceImplStub.class.

    Следующий этап - запуск реестра RMI, который зарегистрирует удаленный объект. Командная строка

    rmiregistry

    запускает реестр RMI на локальной машине.

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

    java com.asw.rmi.ex2.BillingServiceImpl

    В результате отображается сообщение об инициализации BillingService.

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

    java com.asw.rmi.ex2.BillingClient localhost

    соединит BillingClient с объектом BillingServiceImpl.

    Сравнение двух реализаций

    Несмотря на то, что два рассмотренных примера делают практически одно и то же, они все же достаточно сильно различаются. О первом отличии уже говорилось, однако нелишним будет еще раз акцентировать внимание на том, что во втором примере осуществлена передача пользовательских типов от клиента к серверу. И для того, чтобы это реализовать, нам не потребовалось предпринимать никаких действий, кроме объявления наших классов как сериализуемые. Абсолютно всю техническую работу сделала за нас RMI. Второе отличие заключается в скорости работы этих двух приложений - второе приложение работает гораздо быстрее. Связано это с тем, что вызов удаленного метода связан с довольно большими накладными расходами, заметную часть которых составляет время работы системной "обвязки" - чем меньше вызовов, тем этот вклад меньше. Свой вклад, безусловно, вносит и передача данных по сети - передача большого количества мелких пакетов опять же связана с большими накладными расходами, чем передача одного объемного.

    Вернуться к учебному плану