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

Пример использования API java.net

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

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

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

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

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

Первый пример

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

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

    Класс BillingService

    Первый серверный класс BillingService (пример 4.1) очень похож на класс TCPServer из предыдущего примера - так же как и TCPServer,он создает серверный сокет, после чего большую часть времени проводит в ожидании соединения клиентов. При подключении клиента создается экземпляр класса BillingClientService,в конструктор которого передаются, соответственно, InputStream и OutputStream,извлеченные из сокета. BillingClientService является потомком класса Thread,поэтому после запуска (вызов метода start) начинает обрабатывать клиентский запрос параллельно, в то время как BillingService возвращается к ожиданию соединения со следующим клиентом. Таким образом, предлагаемый класс BillingService способен одновременно обрабатывать соединения с несколькими клиентами.

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

  • void addNewCard(String personName, String card) - добавляет в систему новую карту, с идентификатором card и personName - ФИО пользователя;
  • void addMoney(String card, double money) - увеличивает остаток на карте card на величину money (пополнение счета);
  • void subMoney(String card, double money) - уменьшает остаток на карте card на величину money (оплата картой);
  • double getCardBalance(String card) - для карты с идентификатором card возвращает значение баланса.
  • Для хранения "базы" карт применяется хэш-таблица, в которой ключом служит идентификатор карты, а в качестве значения выступает ее баланс. Соответственно, все операции над картой в качестве обязательного параметра содержат ее идентификатор, используя который в качестве ключа, получают доступ к балансу.

    1  package com.asw.net.ex1;
    2  import java.net.*;
    3  import java.util.Hashtable;
    4  import java.io.*; 
    5
    6  public class BillingService extends Thread{ 
    7  public static final int ADD_NEW_CARD = 1; 
    8  public static final int ADD_MONEY = 2; 
    9  public static final int SUB_MONEY = 3; 
    10  public static final int GET_CARD_BALANCE = 4; 
    11  public static final int EXIT_CLIENT = 5;
    12
    13  private  int serverPort = 7896;
    14  private  ServerSocket ss;
    15  private  Hashtable hash;
    16
    17  public static void main(String[] args) {
    18      BillingService bs = new BillingService();
    19     bs.start();
    20  }
    21
    22  public BillingService(){
    23     hash = new Hashtable();
    24  }
    25
    26  public void run(){
    27    try {
    28      ss = new ServerSocket(serverPort);
    29       System.out.println("Server started");
    30       while(true){
    31          Socket s = ss.accept();
    32          System.out.println("Client accepted");
    33        BillingClientService bcs = new BillingClientService(this, 
           new DataInputStream(s.getInputStream()), new DataOutputStream(s.getOutputStream()));
    34      bcs.start();
    35  }
    36  } catch (IOException e) {
    37  e.printStackTrace();
    38  }
    39  }
    40
    41  public void addNewCard(String personName, String card) {
    42       hash.put(card, new Double(0.0));
    43  }
    44  public void addMoney(String card, double money) {
    45      Double d = (Double)hash.get(card);
    46      if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
    47  }
    48  public void subMoney(String card, double money) {
    49     Double d = (Double)hash.get(card);
    50     if (d!=null) hash.put(card,new Double(d.doubleValue()-money));
    51  }
    52  public double getCardBalance(String card) {
    53     Double d = (Double)hash.get(card);
    54     if (d!=null) return d.doubleValue();
    55     return 0;
    56  }
    57  }

    Класс BillingClientService

    Следующий серверный класс - BillingClientService - отвечает непосредственно за взаимодействие с клиентом (пример 4.2). Получив в конструкторе два потока - поток ввода и поток вывода, он способен с их помощью как принимать данные, отправляемые клиентом, так и посылать ему данные.

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

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

    Код операции Данные

    При этом количество и типы данных для каждой операции различны, а вот коды операции имеют целый тип, и их значения заранее известны.

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

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

    Клиент посылает серверу данные следующего формата: код операции (в данном случае операция ADD_NEW_CARD имеет код 1), затем строку с именем владельца, затем строку, содержащую идентификатор карты. BillingClientService в методе run считывает код операции, после чего вызывает свой метод addNewCard.Этот метод "знает", что для операции заведения новой карты необходимо два строковых параметра. Он считывает их из потока, после чего вызывает метод addNewCard,который заносит новую запись в хэш-таблицу. Остальные операции выполняются аналогично. Единственный метод, о котором стоит сказать особо, - это метод getCardBalance,который отличается от других тем, что не только читает данные от клиента (идентификатор карты), но и отправляет ему данные, используя поток вывода OutputStream.

    1  package com.asw.net.ex1;
    2  import java.io.*;
    3  
    4  public class BillingClientService extends Thread {
    5  DataInputStream dis;
    6  DataOutputStream dos;
    7  BillingService bs;
    8  
    9  public BillingClientService (BillingService bs,DataInputStream dis,DataOutputStream dos){
    10    this.bs = bs;
    11    this.dis = dis;
    12    this.dos = dos;
    13  }
    14  
    15  public void run(){
    16    System.out.println("ClientService thread started");
    17    boolean work = true;
    18    while (work) {
    19      int command;
    20      try {
    21        command = dis.readInt();
    22        switch (command) {
    23        case BillingService.ADD_NEW_CARD:
    24          addNewCard();
    25          break;
    26        case BillingService.ADD_MONEY:
    27          addMoney();
    28          break;
    29        case BillingService.SUB_MONEY:
    30          subMoney();
    31          break;
    32        case BillingService.GET_CARD_BALANCE:
    33          getCardBalance();
    34          break;
    35        case BillingService.EXIT_CLIENT:
    36          work = false;
    37          break;
    38        default:
    39          System.out.println("Bad operation:" + command);
    40        }
    41      } catch (IOException e) {
    42        e.printStackTrace();
    43      }
    44    }
    45  }
    46  
    47  void addNewCard() throws IOException{
    48    String personName = dis.readUTF();
    49    String card = dis.readUTF();
    50    bs.addNewCard(personName,card);
    51  }
    52  void addMoney() throws IOException{
    53    String card = dis.readUTF();
    54    double money = dis.readDouble();
    55    bs.addMoney(card,money);
    56  }
    57  void subMoney() throws IOException{
    58    String card = dis.readUTF();
    59    double money = dis.readDouble();
    60    bs.subMoney(card,money);
    61  }
    62  void getCardBalance() throws IOException{
    63    String card = dis.readUTF();
    64    double money = bs.getCardBalance(card);
    65    dos.writeDouble(money);
    66  }
    67  }

    Класс BillingClient

    Что касается клиентского приложения, то в данном случае оно устроено очень просто. Сетевое имя сервера оно принимает в качестве аргумента командной строки, порт известен заранее. Первое, что делается, - устанавливается соединение с сервером (метод connectToServer).Затем создаются три новые карты с разными идентификаторами. Создаются они с помощью метода sendNewCardOperation,который действует в соответствии с установленными нами протоколом: он записывает в поток сначала код операции, затем имя владельца карты, затем его идентификатор.

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

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

    Все реализованные в клиентском приложении методы: send NewCardOperation, sendAddMoneyOperation, sendSubMoneyOperation, sendGetCardBalanceOperation, closeConnection - "симметричны" соответствующим методам BillingClientService и неукоснительно соблюдают разработанный протокол.

    1  package com.asw.net.ex1;
    2  import java.net.*;
    3  import java.io.*;
    4  
    5  public class BillingClient {
    6  int serverPort = 7896;
    7  String serverName;
    8  Socket s;
    9  DataInputStream dis;
    10  DataOutputStream dos;
    11  
    12  public BillingClient(String serverName){
    13    this.serverName = serverName;
    14  }
    15  
    16  public static void main(String[] args) {
    17    BillingClient bc = new BillingClient(args[0]);
    18    try {
    19      bc.startTest();
    20    } catch (IOException e) {
    21      e.printStackTrace();
    22    }
    23  }
    24  
    25  public void startTest() throws IOException{
    26    connectToServer();
    27    sendNewCardOperation("Piter","1");
    28    sendNewCardOperation("Stefan","2");
    29    sendNewCardOperation("Nataly","3");
    30    for (int i=0; i<1000;i++){
    31      sendAddMoneyOperation("1", i%10);
    32      sendAddMoneyOperation("2", i%20);
    33      sendAddMoneyOperation("3", i%30);
    34    }
    35    System.out.println("1:"+sendGetCardBalanceOperation("1"));
    36    System.out.println("2:"+sendGetCardBalanceOperation("2"));
    37    System.out.println("3:"+sendGetCardBalanceOperation("3"));
    38    closeConnection();
    39  }
    40  
    41  void connectToServer() throws UnknownHostException, IOException{
    42    s = new Socket(serverName, serverPort);
    43    dis = new DataInputStream(s.getInputStream());
    44    dos = new DataOutputStream(s.getOutputStream());
    45  }
    46  
    47  void sendNewCardOperation(String personName, String card) throws IOException{
    48    dos.writeInt(BillingService.ADD_NEW_CARD);
    49    dos.writeUTF(personName);
    50    dos.writeUTF(card);
    51  }
    52  
    53  void sendAddMoneyOperation(String card, double money) throws IOException{
    54    dos.writeInt(BillingService.ADD_MONEY);
    55    dos.writeUTF(card);
    56    dos.writeDouble(money);
    57  }
    58  void sendSubMoneyOperation(String card, double money) throws IOException{
    59    dos.writeInt(BillingService.SUB_MONEY);
    60    dos.writeUTF(card);
    61    dos.writeDouble(money);
    62  }
    63  double sendGetCardBalanceOperation(String card) throws IOException{
    64    dos.writeInt(BillingService.GET_CARD_BALANCE);
    65    dos.writeUTF(card);
    66    return dis.readDouble();
    67  }
    68  void closeConnection() throws IOException{
    69    dos.writeInt(BillingService.EXIT_CLIENT);
    70  }
    71  }

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

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

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

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

    В самом деле, рассмотрим еще раз нашу задачу. Все, что необходимо, - реализовать механизм обмена структурами данных (или объектами) между клиентом и сервером. И встроенный механизм сериализации в этом случае нам идеально подходит. Мы будем передавать по сети не отдельные "поля", а целые объекты.

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

    Класс CardOperation

    Первый класс, который мы реализуем, - класс "операция над картой" (пример 4.4).

    1  package com.asw.net.ex2;
    2  import java.util.*;
    3  import java.io.*;
    4  
    5  
    6  public class CardOperation implements Serializable {
    7  public CardOperation(String card,double amount,Date operationDate){
    8    this.card = card;
    9    this.amount = amount;
    10    this.operationDate = operationDate;
    11  }
    12  public String card;
    13  public double amount;
    14  public Date operationDate;
    15  }

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

    Класс содержит три поля, которые и составляют его полезное наполнение - номер карты, сумма начисляемых/списываемых средств и дата операции.

    Класс Card

    Второй транспортный класс представляет собой представление карты и выглядит следующим образом (пример 4.5).

    1  package com.asw.net.ex2;
    2  import java.io.Serializable;
    3  import java.util.*;
    4  
    5  public class Card implements Serializable{
    6  public Card(String person, Date createDate, String cardNumber,double balance){
    7  this.person = person;
    8  this.createDate = createDate;
    9  this.cardNumber = cardNumber;
    10  this.balance = balance;
    11  }
    12  public String person;
    13  public transient Date createDate;
    14  public String cardNumber;
    15  public double balance;
    16  public String toString(){
    17  return "Card: cardNumber="+cardNumber+"\tBalance="+balance+"\tPerson="+person+"\tCreateDate="+createDate+"";
    18  }
    19  }

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

    Перейдем теперь к серверным классам.

    Класс BillingService

    Первый из серверных классов - BillingService (пример 4.6).

    1  package com.asw.net.ex2;
    2  import java.net.*;
    3  import java.util.Hashtable;
    4  import java.io.*;
    5  
    6  public class BillingService extends Thread{
    7  private int serverPort = 7896;
    8  private ServerSocket ss;
    9  private  Hashtable hash;
    10  
    11  public static void main(String[] args) {
    12    BillingService bs = new BillingService();
    13    bs.start();
    14  }
    15  
    16  public BillingService(){
    17    hash = new Hashtable();
    18  }
    19  
    20  public void run(){
    21    try {
    22      ss = new ServerSocket(serverPort);
    23      System.out.println("Server started");
    24      while(true){
    25        System.out.println("new client waiting...");
    26        Socket s = ss.accept();
    27        System.out.println("Client accepted");
    28        BillingClientService bcs = new BillingClientService(this,s);
    29        System.out.println("bcs created");
    30        bcs.start();
    31      }
    32    } catch (IOException e) {
    33      e.printStackTrace();
    34    }
    35    
    36  }
    37  
    38  public void addNewCard(Card card) {
    39    hash.put(card.cardNumber, card);
    40  }
    41  public void addMoney(String card, double money) {
    42    Card c = (Card)hash.get(card);
    43    if (c==null) {
    44      System.out.println("Bad Card number\n");
    45      return;
    46    };
    47    c.balance+=money;
    48    hash.put(card,c);
    49  }
    50  public Card getCard(String card){
    51    return (Card)hash.get(card);
    52  }
    53  }

    Несложно заметить, что он очень похож на BillingService из предыдущего примера. И действительно, он точно так же хранит хэш-таблицу карт, имеет методы для обработки соответствующих событий - добавления новой карты, запроса баланса карты и обработки операций изменения баланса. Основной цикл run (строки 21-32) тоже точно такой же - сначала создается серверный сокет, затем ожидаются соединения клиентов. При появлении нового клиента создается экземпляр класса BillingClientService (строка 28), который и занимается всем дальнейшим обслуживанием клиента.

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

    Наибольшие отличия от примера из предыдущего раздела имеет класс BillingClientService (пример 4.7).

    Класс BillingClientService

    1  package com.asw.net.ex2;
    2  import java.io.*;
    3  import java.net.*;
    4  
    5  public class BillingClientService extends Thread {
    6  ObjectInputStream ois;
    7  ObjectOutputStream oos;
    8  BillingService bs;
    9  Socket s;
    10  
    11  public BillingClientService(BillingService bs,Socket s){
    12    System.out.println("Constructor BillingClientService\n");
    13    this.bs = bs;
    14    this.s = s;
    15    try {
    16      this.oos = new ObjectOutputStream(s.getOutputStream());
    17      this.ois = new ObjectInputStream(s.getInputStream());
    18    } catch (IOException e) {
    19      // TODO Auto-generated catch block
    20      e.printStackTrace();
    21    }
    22    
    23    System.out.println("Stream`s done \n socket="+s);
    24  }
    25  public void run(){
    26    System.out.println("ClientService thread started\n");
    27    boolean work = true;
    28    while (work) {
    29      int command;
    30      Object o;
    31      try {
    32        o = ois.readObject();              
    33        if (o instanceof Card[]) {
    34          Card[] cards = (Card[])o;
    35          for (int i=0;i<cards.length;i++){
    36            bs.addNewCard(cards[i]);
    37          }
    38        }else if (o instanceof CardOperation[]){
    39          CardOperation[] co = (CardOperation[])o;
    40          for (int i=0;i<co.length;i++){
    41            bs.addMoney(co[i].card,co[i].amount);
    42          }
    43        }else if (o instanceof String){
    44          oos.writeObject(bs.getCard((String)o)); 
    45        }else System.out.println("Bad operation");
    46        
    47      } catch (IOException e) {
    48        e.printStackTrace();
    49      } catch (ClassNotFoundException e) {
    50        e.printStackTrace();
    51      }
    52    }
    53  }
    54  }

    Во-первых, в качестве потоков ввода-вывода он использует, соответственно, ObjectInputStream и ObjectOutputStream.Эти высокоуровневые потомки от InputStream и OutputStream предоставляют возможность оперировать с целыми объектами, а не со значениями примитивных типов, как DataInputStream и DataOutputStream из предыдущего примера. Конструируются они, как и следовало ожидать, принимая в качестве параметров базовый класс (строки 16,17).

    Поскольку класс BillingClientService является потомком Thread,вся его функциональность сконцентрирована в методе run.Так же как и в предыдущем примере, этот метод представляет собой цикл ожидания сообщений от клиента. Однако метод чтения клиентских сообщений значительно отличается от метода, рассмотренного в предыдущем примере. В данном случае, при использовании метода readObject() класса ObjectInputStream,считывается экземпляр посланного клиентом класса (строка 32). Поскольку неизвестно заранее, какие именно классы прислал клиент (напомним, транспортных классов в нашей системе два - класс карт и класс операций над ними), осуществляется проверка типов (строки 33, 38, 43). Если присланный объект оказался массивом Card,то осуществляется его преобразование к этому типу (строка 34), после чего в цикле производится вызов соответствующего метода создания новой карты, в качестве параметра которому передается соответствующий элемент массива, т.е. значение типа Card.

    В случае если присланный объект оказывается массивом CardOperation,осуществляется его приведение к типу "массив CardOperation* (строка 39) и в цикле вызывается метод изменения баланса.

    Если же присланный объект оказался объектом класса String,то мы интерпретируем этот вызов как запрос карты по ее номеру и отправляем карту клиенту (строка 44).

    Стоит обратить внимание на простоту этой операции. Все, что нужно сделать, - вызвать метод writeObject класса ObjectOutputStream.Поскольку класс Card объявлен как сериализуемый, экземпляр этого класса, возвращенный методом getCard класса BillingService,будет автоматически "упакован", т.е. преобразован в некий служебный формат, который может быть сохранен во внешнем файле, отправлен по сети и т.д. В этом формате будут сохранены все текущие значения полей объекта, а значит, после восстановления мы получим точную копию сохраненного объекта.

    Итак, для передачи копии карты клиенту сервер вызывает метод writeObject,а клиент должен прочитать карту соответствующим вызовом readObject.

    При использовании сериализации следует отчетливо понимать, что принимающая сторона получает копию объекта, а не сам объект. Так, если сервер передаст клиенту карту с номером "1", а клиент, получив ее, изменит в ней поле баланса, значение баланса у карты "1" на сервере не изменится, поскольку изменения эти произойдут лишь в клиентской копии.

    Класс BillingClient

    Реализация клиента (класс BillingClient - пример 4.8) похожа на реализацию из предыдущего примера, за исключением, конечно, способа передачи данных.

    После запуска теста - вызов startTest из метода main (строка 77) - происходит соединение с сервером - connectToServer (строка 18), после чего формируется два массива. Первый массив содержит объекты класса Card (это те карты, которые затем будут переданы на сервер для их заведения), второй - массив из 30000 операций над этими картами. Вызов метода processCard (строка 23) передает на сервер массив карт, вызов processOperation (строка 34) передает на сервер массив операций.

    Для передачи методы processCard и processOperation используют уже знакомый нам метод writeObject класса ObjectOutputStream.

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

    1  package com.asw.net.ex2;
    2  import java.net.*;
    3  import java.util.Date;
    4  import java.io.*;
    5  
    6  
    7  public class BillingClient {
    8  
    9  int serverPort = 7896;
    10  String serverName;
    11  Socket s;
    12  ObjectInputStream ois;
    13  ObjectOutputStream oos;
    14  public BillingClient(String serverName){
    15    this.serverName = serverName;
    16  }
    17  public void startTest() throws IOException{
    18    connectToServer();
    19          
    20    Card[] cards = {new Card("Piter",new Date(),"1",0.0),
    21            new Card("Stefan",new Date(),"2",0.0),
    22            new Card("Nataly",new Date(),"3",0.0)};
    23    processCard(cards);
    24    
    25    int cnt = 30000;
    26    CardOperation[] co = new CardOperation[cnt];
    27    for (int i = 0; i < cnt; i++) {
    28      switch (i%3){
    29      case 0:  co[i] = new CardOperation("1",1,new Date());break;
    30      case 1:  co[i] = new CardOperation("2",2,new Date());break;
    31      case 2: co[i] = new CardOperation("3",3,new Date());break;
    32      }
    33    }
    34    processOperation(co);
    35    try {
    36      System.out.println("getCard: "+getCard("1"));
    37      System.out.println("getCard: "+getCard("2"));
    38      System.out.println("getCard: "+getCard("3"));
    39    } catch (IOException e1) {
    40      e1.printStackTrace();
    41    } catch (ClassNotFoundException e1) {
    42      e1.printStackTrace();
    43    }
    44    
    45    java.lang.Object sync = new java.lang.Object();
    46    synchronized (sync) {
    47      try {
    48        sync.wait();
    49      } catch (InterruptedException e) {
    50        e.printStackTrace();
    51      }
    52    }
    53    
    54  }
    55  void connectToServer() throws UnknownHostException, IOException{
    56    s = new Socket(serverName, serverPort);
    57    System.out.println("connection established\n");
    58    ois = new ObjectInputStream(s.getInputStream());
    59    oos = new ObjectOutputStream(s.getOutputStream());
    60    System.out.println("Stream`s done \n ois="+ois+"\n oos="+oos);
    61  }
    62  void processOperation(CardOperation[] co) throws IOException{
    63    System.out.println(co);
    64    oos.writeObject(co);
    65  }
    66  void processCard(Card[] c) throws IOException{
    67    System.out.println("processCard: c="+c);
    68    oos.writeObject(c);
    69  }
    70  Card getCard(String card) throws IOException, ClassNotFoundException{
    71    oos.writeObject(card);
    72    return (Card)ois.readObject();
    73  }
    74  public static void main(String[] args) throws Exception{
    75    BillingClient bc = new BillingClient(args[0]);
    76    try {
    77      bc.startTest();
    78    } catch (IOException e) {
    79      e.printStackTrace();
    80    }
    81  }
    82  }
    Страницы:

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

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

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

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

    Первый пример

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

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

    Класс BillingService

    Первый серверный класс BillingService (пример 4.1) очень похож на класс TCPServer из предыдущего примера - так же как и TCPServer,он создает серверный сокет, после чего большую часть времени проводит в ожидании соединения клиентов. При подключении клиента создается экземпляр класса BillingClientService,в конструктор которого передаются, соответственно, InputStream и OutputStream,извлеченные из сокета. BillingClientService является потомком класса Thread,поэтому после запуска (вызов метода start) начинает обрабатывать клиентский запрос параллельно, в то время как BillingService возвращается к ожиданию соединения со следующим клиентом. Таким образом, предлагаемый класс BillingService способен одновременно обрабатывать соединения с несколькими клиентами.

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

  • void addNewCard(String personName, String card) - добавляет в систему новую карту, с идентификатором card и personName - ФИО пользователя;
  • void addMoney(String card, double money) - увеличивает остаток на карте card на величину money (пополнение счета);
  • void subMoney(String card, double money) - уменьшает остаток на карте card на величину money (оплата картой);
  • double getCardBalance(String card) - для карты с идентификатором card возвращает значение баланса.
  • Для хранения "базы" карт применяется хэш-таблица, в которой ключом служит идентификатор карты, а в качестве значения выступает ее баланс. Соответственно, все операции над картой в качестве обязательного параметра содержат ее идентификатор, используя который в качестве ключа, получают доступ к балансу.

    1  package com.asw.net.ex1;
    2  import java.net.*;
    3  import java.util.Hashtable;
    4  import java.io.*; 
    5
    6  public class BillingService extends Thread{ 
    7  public static final int ADD_NEW_CARD = 1; 
    8  public static final int ADD_MONEY = 2; 
    9  public static final int SUB_MONEY = 3; 
    10  public static final int GET_CARD_BALANCE = 4; 
    11  public static final int EXIT_CLIENT = 5;
    12
    13  private  int serverPort = 7896;
    14  private  ServerSocket ss;
    15  private  Hashtable hash;
    16
    17  public static void main(String[] args) {
    18      BillingService bs = new BillingService();
    19     bs.start();
    20  }
    21
    22  public BillingService(){
    23     hash = new Hashtable();
    24  }
    25
    26  public void run(){
    27    try {
    28      ss = new ServerSocket(serverPort);
    29       System.out.println("Server started");
    30       while(true){
    31          Socket s = ss.accept();
    32          System.out.println("Client accepted");
    33        BillingClientService bcs = new BillingClientService(this, 
           new DataInputStream(s.getInputStream()), new DataOutputStream(s.getOutputStream()));
    34      bcs.start();
    35  }
    36  } catch (IOException e) {
    37  e.printStackTrace();
    38  }
    39  }
    40
    41  public void addNewCard(String personName, String card) {
    42       hash.put(card, new Double(0.0));
    43  }
    44  public void addMoney(String card, double money) {
    45      Double d = (Double)hash.get(card);
    46      if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
    47  }
    48  public void subMoney(String card, double money) {
    49     Double d = (Double)hash.get(card);
    50     if (d!=null) hash.put(card,new Double(d.doubleValue()-money));
    51  }
    52  public double getCardBalance(String card) {
    53     Double d = (Double)hash.get(card);
    54     if (d!=null) return d.doubleValue();
    55     return 0;
    56  }
    57  }

    Класс BillingClientService

    Следующий серверный класс - BillingClientService - отвечает непосредственно за взаимодействие с клиентом (пример 4.2). Получив в конструкторе два потока - поток ввода и поток вывода, он способен с их помощью как принимать данные, отправляемые клиентом, так и посылать ему данные.

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

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

    Код операции Данные

    При этом количество и типы данных для каждой операции различны, а вот коды операции имеют целый тип, и их значения заранее известны.

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

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

    Клиент посылает серверу данные следующего формата: код операции (в данном случае операция ADD_NEW_CARD имеет код 1), затем строку с именем владельца, затем строку, содержащую идентификатор карты. BillingClientService в методе run считывает код операции, после чего вызывает свой метод addNewCard.Этот метод "знает", что для операции заведения новой карты необходимо два строковых параметра. Он считывает их из потока, после чего вызывает метод addNewCard,который заносит новую запись в хэш-таблицу. Остальные операции выполняются аналогично. Единственный метод, о котором стоит сказать особо, - это метод getCardBalance,который отличается от других тем, что не только читает данные от клиента (идентификатор карты), но и отправляет ему данные, используя поток вывода OutputStream.

    1  package com.asw.net.ex1;
    2  import java.io.*;
    3  
    4  public class BillingClientService extends Thread {
    5  DataInputStream dis;
    6  DataOutputStream dos;
    7  BillingService bs;
    8  
    9  public BillingClientService (BillingService bs,DataInputStream dis,DataOutputStream dos){
    10    this.bs = bs;
    11    this.dis = dis;
    12    this.dos = dos;
    13  }
    14  
    15  public void run(){
    16    System.out.println("ClientService thread started");
    17    boolean work = true;
    18    while (work) {
    19      int command;
    20      try {
    21        command = dis.readInt();
    22        switch (command) {
    23        case BillingService.ADD_NEW_CARD:
    24          addNewCard();
    25          break;
    26        case BillingService.ADD_MONEY:
    27          addMoney();
    28          break;
    29        case BillingService.SUB_MONEY:
    30          subMoney();
    31          break;
    32        case BillingService.GET_CARD_BALANCE:
    33          getCardBalance();
    34          break;
    35        case BillingService.EXIT_CLIENT:
    36          work = false;
    37          break;
    38        default:
    39          System.out.println("Bad operation:" + command);
    40        }
    41      } catch (IOException e) {
    42        e.printStackTrace();
    43      }
    44    }
    45  }
    46  
    47  void addNewCard() throws IOException{
    48    String personName = dis.readUTF();
    49    String card = dis.readUTF();
    50    bs.addNewCard(personName,card);
    51  }
    52  void addMoney() throws IOException{
    53    String card = dis.readUTF();
    54    double money = dis.readDouble();
    55    bs.addMoney(card,money);
    56  }
    57  void subMoney() throws IOException{
    58    String card = dis.readUTF();
    59    double money = dis.readDouble();
    60    bs.subMoney(card,money);
    61  }
    62  void getCardBalance() throws IOException{
    63    String card = dis.readUTF();
    64    double money = bs.getCardBalance(card);
    65    dos.writeDouble(money);
    66  }
    67  }

    Класс BillingClient

    Что касается клиентского приложения, то в данном случае оно устроено очень просто. Сетевое имя сервера оно принимает в качестве аргумента командной строки, порт известен заранее. Первое, что делается, - устанавливается соединение с сервером (метод connectToServer).Затем создаются три новые карты с разными идентификаторами. Создаются они с помощью метода sendNewCardOperation,который действует в соответствии с установленными нами протоколом: он записывает в поток сначала код операции, затем имя владельца карты, затем его идентификатор.

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

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

    Все реализованные в клиентском приложении методы: send NewCardOperation, sendAddMoneyOperation, sendSubMoneyOperation, sendGetCardBalanceOperation, closeConnection - "симметричны" соответствующим методам BillingClientService и неукоснительно соблюдают разработанный протокол.

    1  package com.asw.net.ex1;
    2  import java.net.*;
    3  import java.io.*;
    4  
    5  public class BillingClient {
    6  int serverPort = 7896;
    7  String serverName;
    8  Socket s;
    9  DataInputStream dis;
    10  DataOutputStream dos;
    11  
    12  public BillingClient(String serverName){
    13    this.serverName = serverName;
    14  }
    15  
    16  public static void main(String[] args) {
    17    BillingClient bc = new BillingClient(args[0]);
    18    try {
    19      bc.startTest();
    20    } catch (IOException e) {
    21      e.printStackTrace();
    22    }
    23  }
    24  
    25  public void startTest() throws IOException{
    26    connectToServer();
    27    sendNewCardOperation("Piter","1");
    28    sendNewCardOperation("Stefan","2");
    29    sendNewCardOperation("Nataly","3");
    30    for (int i=0; i<1000;i++){
    31      sendAddMoneyOperation("1", i%10);
    32      sendAddMoneyOperation("2", i%20);
    33      sendAddMoneyOperation("3", i%30);
    34    }
    35    System.out.println("1:"+sendGetCardBalanceOperation("1"));
    36    System.out.println("2:"+sendGetCardBalanceOperation("2"));
    37    System.out.println("3:"+sendGetCardBalanceOperation("3"));
    38    closeConnection();
    39  }
    40  
    41  void connectToServer() throws UnknownHostException, IOException{
    42    s = new Socket(serverName, serverPort);
    43    dis = new DataInputStream(s.getInputStream());
    44    dos = new DataOutputStream(s.getOutputStream());
    45  }
    46  
    47  void sendNewCardOperation(String personName, String card) throws IOException{
    48    dos.writeInt(BillingService.ADD_NEW_CARD);
    49    dos.writeUTF(personName);
    50    dos.writeUTF(card);
    51  }
    52  
    53  void sendAddMoneyOperation(String card, double money) throws IOException{
    54    dos.writeInt(BillingService.ADD_MONEY);
    55    dos.writeUTF(card);
    56    dos.writeDouble(money);
    57  }
    58  void sendSubMoneyOperation(String card, double money) throws IOException{
    59    dos.writeInt(BillingService.SUB_MONEY);
    60    dos.writeUTF(card);
    61    dos.writeDouble(money);
    62  }
    63  double sendGetCardBalanceOperation(String card) throws IOException{
    64    dos.writeInt(BillingService.GET_CARD_BALANCE);
    65    dos.writeUTF(card);
    66    return dis.readDouble();
    67  }
    68  void closeConnection() throws IOException{
    69    dos.writeInt(BillingService.EXIT_CLIENT);
    70  }
    71  }

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

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

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

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

    В самом деле, рассмотрим еще раз нашу задачу. Все, что необходимо, - реализовать механизм обмена структурами данных (или объектами) между клиентом и сервером. И встроенный механизм сериализации в этом случае нам идеально подходит. Мы будем передавать по сети не отдельные "поля", а целые объекты.

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

    Класс CardOperation

    Первый класс, который мы реализуем, - класс "операция над картой" (пример 4.4).

    1  package com.asw.net.ex2;
    2  import java.util.*;
    3  import java.io.*;
    4  
    5  
    6  public class CardOperation implements Serializable {
    7  public CardOperation(String card,double amount,Date operationDate){
    8    this.card = card;
    9    this.amount = amount;
    10    this.operationDate = operationDate;
    11  }
    12  public String card;
    13  public double amount;
    14  public Date operationDate;
    15  }

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

    Класс содержит три поля, которые и составляют его полезное наполнение - номер карты, сумма начисляемых/списываемых средств и дата операции.

    Класс Card

    Второй транспортный класс представляет собой представление карты и выглядит следующим образом (пример 4.5).

    1  package com.asw.net.ex2;
    2  import java.io.Serializable;
    3  import java.util.*;
    4  
    5  public class Card implements Serializable{
    6  public Card(String person, Date createDate, String cardNumber,double balance){
    7  this.person = person;
    8  this.createDate = createDate;
    9  this.cardNumber = cardNumber;
    10  this.balance = balance;
    11  }
    12  public String person;
    13  public transient Date createDate;
    14  public String cardNumber;
    15  public double balance;
    16  public String toString(){
    17  return "Card: cardNumber="+cardNumber+"\tBalance="+balance+"\tPerson="+person+"\tCreateDate="+createDate+"";
    18  }
    19  }

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

    Перейдем теперь к серверным классам.

    Класс BillingService

    Первый из серверных классов - BillingService (пример 4.6).

    1  package com.asw.net.ex2;
    2  import java.net.*;
    3  import java.util.Hashtable;
    4  import java.io.*;
    5  
    6  public class BillingService extends Thread{
    7  private int serverPort = 7896;
    8  private ServerSocket ss;
    9  private  Hashtable hash;
    10  
    11  public static void main(String[] args) {
    12    BillingService bs = new BillingService();
    13    bs.start();
    14  }
    15  
    16  public BillingService(){
    17    hash = new Hashtable();
    18  }
    19  
    20  public void run(){
    21    try {
    22      ss = new ServerSocket(serverPort);
    23      System.out.println("Server started");
    24      while(true){
    25        System.out.println("new client waiting...");
    26        Socket s = ss.accept();
    27        System.out.println("Client accepted");
    28        BillingClientService bcs = new BillingClientService(this,s);
    29        System.out.println("bcs created");
    30        bcs.start();
    31      }
    32    } catch (IOException e) {
    33      e.printStackTrace();
    34    }
    35    
    36  }
    37  
    38  public void addNewCard(Card card) {
    39    hash.put(card.cardNumber, card);
    40  }
    41  public void addMoney(String card, double money) {
    42    Card c = (Card)hash.get(card);
    43    if (c==null) {
    44      System.out.println("Bad Card number\n");
    45      return;
    46    };
    47    c.balance+=money;
    48    hash.put(card,c);
    49  }
    50  public Card getCard(String card){
    51    return (Card)hash.get(card);
    52  }
    53  }

    Несложно заметить, что он очень похож на BillingService из предыдущего примера. И действительно, он точно так же хранит хэш-таблицу карт, имеет методы для обработки соответствующих событий - добавления новой карты, запроса баланса карты и обработки операций изменения баланса. Основной цикл run (строки 21-32) тоже точно такой же - сначала создается серверный сокет, затем ожидаются соединения клиентов. При появлении нового клиента создается экземпляр класса BillingClientService (строка 28), который и занимается всем дальнейшим обслуживанием клиента.

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

    Наибольшие отличия от примера из предыдущего раздела имеет класс BillingClientService (пример 4.7).

    Класс BillingClientService

    1  package com.asw.net.ex2;
    2  import java.io.*;
    3  import java.net.*;
    4  
    5  public class BillingClientService extends Thread {
    6  ObjectInputStream ois;
    7  ObjectOutputStream oos;
    8  BillingService bs;
    9  Socket s;
    10  
    11  public BillingClientService(BillingService bs,Socket s){
    12    System.out.println("Constructor BillingClientService\n");
    13    this.bs = bs;
    14    this.s = s;
    15    try {
    16      this.oos = new ObjectOutputStream(s.getOutputStream());
    17      this.ois = new ObjectInputStream(s.getInputStream());
    18    } catch (IOException e) {
    19      // TODO Auto-generated catch block
    20      e.printStackTrace();
    21    }
    22    
    23    System.out.println("Stream`s done \n socket="+s);
    24  }
    25  public void run(){
    26    System.out.println("ClientService thread started\n");
    27    boolean work = true;
    28    while (work) {
    29      int command;
    30      Object o;
    31      try {
    32        o = ois.readObject();              
    33        if (o instanceof Card[]) {
    34          Card[] cards = (Card[])o;
    35          for (int i=0;i<cards.length;i++){
    36            bs.addNewCard(cards[i]);
    37          }
    38        }else if (o instanceof CardOperation[]){
    39          CardOperation[] co = (CardOperation[])o;
    40          for (int i=0;i<co.length;i++){
    41            bs.addMoney(co[i].card,co[i].amount);
    42          }
    43        }else if (o instanceof String){
    44          oos.writeObject(bs.getCard((String)o)); 
    45        }else System.out.println("Bad operation");
    46        
    47      } catch (IOException e) {
    48        e.printStackTrace();
    49      } catch (ClassNotFoundException e) {
    50        e.printStackTrace();
    51      }
    52    }
    53  }
    54  }

    Во-первых, в качестве потоков ввода-вывода он использует, соответственно, ObjectInputStream и ObjectOutputStream.Эти высокоуровневые потомки от InputStream и OutputStream предоставляют возможность оперировать с целыми объектами, а не со значениями примитивных типов, как DataInputStream и DataOutputStream из предыдущего примера. Конструируются они, как и следовало ожидать, принимая в качестве параметров базовый класс (строки 16,17).

    Поскольку класс BillingClientService является потомком Thread,вся его функциональность сконцентрирована в методе run.Так же как и в предыдущем примере, этот метод представляет собой цикл ожидания сообщений от клиента. Однако метод чтения клиентских сообщений значительно отличается от метода, рассмотренного в предыдущем примере. В данном случае, при использовании метода readObject() класса ObjectInputStream,считывается экземпляр посланного клиентом класса (строка 32). Поскольку неизвестно заранее, какие именно классы прислал клиент (напомним, транспортных классов в нашей системе два - класс карт и класс операций над ними), осуществляется проверка типов (строки 33, 38, 43). Если присланный объект оказался массивом Card,то осуществляется его преобразование к этому типу (строка 34), после чего в цикле производится вызов соответствующего метода создания новой карты, в качестве параметра которому передается соответствующий элемент массива, т.е. значение типа Card.

    В случае если присланный объект оказывается массивом CardOperation,осуществляется его приведение к типу "массив CardOperation* (строка 39) и в цикле вызывается метод изменения баланса.

    Если же присланный объект оказался объектом класса String,то мы интерпретируем этот вызов как запрос карты по ее номеру и отправляем карту клиенту (строка 44).

    Стоит обратить внимание на простоту этой операции. Все, что нужно сделать, - вызвать метод writeObject класса ObjectOutputStream.Поскольку класс Card объявлен как сериализуемый, экземпляр этого класса, возвращенный методом getCard класса BillingService,будет автоматически "упакован", т.е. преобразован в некий служебный формат, который может быть сохранен во внешнем файле, отправлен по сети и т.д. В этом формате будут сохранены все текущие значения полей объекта, а значит, после восстановления мы получим точную копию сохраненного объекта.

    Итак, для передачи копии карты клиенту сервер вызывает метод writeObject,а клиент должен прочитать карту соответствующим вызовом readObject.

    При использовании сериализации следует отчетливо понимать, что принимающая сторона получает копию объекта, а не сам объект. Так, если сервер передаст клиенту карту с номером "1", а клиент, получив ее, изменит в ней поле баланса, значение баланса у карты "1" на сервере не изменится, поскольку изменения эти произойдут лишь в клиентской копии.

    Класс BillingClient

    Реализация клиента (класс BillingClient - пример 4.8) похожа на реализацию из предыдущего примера, за исключением, конечно, способа передачи данных.

    После запуска теста - вызов startTest из метода main (строка 77) - происходит соединение с сервером - connectToServer (строка 18), после чего формируется два массива. Первый массив содержит объекты класса Card (это те карты, которые затем будут переданы на сервер для их заведения), второй - массив из 30000 операций над этими картами. Вызов метода processCard (строка 23) передает на сервер массив карт, вызов processOperation (строка 34) передает на сервер массив операций.

    Для передачи методы processCard и processOperation используют уже знакомый нам метод writeObject класса ObjectOutputStream.

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

    1  package com.asw.net.ex2;
    2  import java.net.*;
    3  import java.util.Date;
    4  import java.io.*;
    5  
    6  
    7  public class BillingClient {
    8  
    9  int serverPort = 7896;
    10  String serverName;
    11  Socket s;
    12  ObjectInputStream ois;
    13  ObjectOutputStream oos;
    14  public BillingClient(String serverName){
    15    this.serverName = serverName;
    16  }
    17  public void startTest() throws IOException{
    18    connectToServer();
    19          
    20    Card[] cards = {new Card("Piter",new Date(),"1",0.0),
    21            new Card("Stefan",new Date(),"2",0.0),
    22            new Card("Nataly",new Date(),"3",0.0)};
    23    processCard(cards);
    24    
    25    int cnt = 30000;
    26    CardOperation[] co = new CardOperation[cnt];
    27    for (int i = 0; i < cnt; i++) {
    28      switch (i%3){
    29      case 0:  co[i] = new CardOperation("1",1,new Date());break;
    30      case 1:  co[i] = new CardOperation("2",2,new Date());break;
    31      case 2: co[i] = new CardOperation("3",3,new Date());break;
    32      }
    33    }
    34    processOperation(co);
    35    try {
    36      System.out.println("getCard: "+getCard("1"));
    37      System.out.println("getCard: "+getCard("2"));
    38      System.out.println("getCard: "+getCard("3"));
    39    } catch (IOException e1) {
    40      e1.printStackTrace();
    41    } catch (ClassNotFoundException e1) {
    42      e1.printStackTrace();
    43    }
    44    
    45    java.lang.Object sync = new java.lang.Object();
    46    synchronized (sync) {
    47      try {
    48        sync.wait();
    49      } catch (InterruptedException e) {
    50        e.printStackTrace();
    51      }
    52    }
    53    
    54  }
    55  void connectToServer() throws UnknownHostException, IOException{
    56    s = new Socket(serverName, serverPort);
    57    System.out.println("connection established\n");
    58    ois = new ObjectInputStream(s.getInputStream());
    59    oos = new ObjectOutputStream(s.getOutputStream());
    60    System.out.println("Stream`s done \n ois="+ois+"\n oos="+oos);
    61  }
    62  void processOperation(CardOperation[] co) throws IOException{
    63    System.out.println(co);
    64    oos.writeObject(co);
    65  }
    66  void processCard(Card[] c) throws IOException{
    67    System.out.println("processCard: c="+c);
    68    oos.writeObject(c);
    69  }
    70  Card getCard(String card) throws IOException, ClassNotFoundException{
    71    oos.writeObject(card);
    72    return (Card)ois.readObject();
    73  }
    74  public static void main(String[] args) throws Exception{
    75    BillingClient bc = new BillingClient(args[0]);
    76    try {
    77      bc.startTest();
    78    } catch (IOException e) {
    79      e.printStackTrace();
    80    }
    81  }
    82  }
    Вернуться к учебному плану