Рабочий каталог расположен в
Следующие разделы курса посвящены различным программным технологиям, позволяющим реализовать распределенные системы. Наилучшей иллюстрацией использования этих технологий была бы программная система, созданная на их основе. Тут, однако, существует следующая опасность: если эта разрабатываемая система будет слишком сложна, то за ее сложностью может потеряться изучаемый предмет, если же она будет слишком проста, то может возникнуть вопрос, зачем для решения такой простой задачи применяются столь изощренные средства. Поэтому для иллюстрации основных рассматриваемых в курсе технологий выбрана достаточно простая система, позволяющая сконцентрироваться на важных для нас моментах, не углубляясь слишком сильно в предметную область. С другой стороны, рассматриваемая задача является достаточно сложной, чтобы проиллюстрировать на ее примере некоторые типичные проблемы, возникающие при разработке серьезных распределенных систем.
В качестве такой тестовой задачи рассмотрим задачу автоматизации некоторого отдельного контура работы сети столовых: необходимо разработать систему, которая автоматизирует участок работы, связанный с обслуживанием пластиковых карт. Дело в том, что по легенде в указанных столовых предусмотрен дополнительный сервис: постоянным клиентам выдают пластиковые карты, при предъявлении которых клиент получает существенную скидку. При этом на карту можно положить некоторую сумму денег и расплачиваться за обеды не наличными, а средствами, находящимися на карте. В целях экономии затрат на эмиссию карты закуплены не чиповые, а штриховые (такая карта несет только код - идентификатор клиента). Существенным является то обстоятельство, что карта, выданная в одной столовой, может использоваться в другой столовой. Между столовыми нет устойчивых защищенных каналов связи, в связи с этим встает проблема передачи баланса карт между столовыми. Кроме того, в будущем руководство предполагает изучать предпочтения клиентов, чтобы формировать более гибкую ценовую политику и более рационально управлять ассортиментом предлагаемых блюд.
В дальнейшем при реализации того или иного примера будут выдвигаться некоторые дополнительные условия и ограничения, позволяющие иллюстрировать те или иные аспекты рассматриваемых технологий.
Разобранный в предыдущем разделе пример уже достаточно сложен и содержит примеры всех необходимых нам технологий, для того чтобы послужить прототипом для первой версии нашей "ресторанной" системы. Чтобы программы-примеры не получались слишком большими, договоримся, что наша система будет поддерживать очень ограниченный набор операций:
Кроме того, практически не будут реализованы механизмы защиты от перерасхода средств, механизмы контроля действий пользователя и т.д., поскольку главной задачей рассматриваемых программ является демонстрация тех или иных механизмов взаимодействия, а не демонстрация реализации соответствующей бизнес-логики.
Класс BillingService
Первый серверный класс BillingService (пример 4.1) очень похож на класс TCPServer из предыдущего примера - так же как и TCPServer,он создает серверный сокет, после чего большую часть времени проводит в ожидании соединения клиентов. При подключении клиента создается экземпляр класса BillingClientService,в конструктор которого передаются, соответственно, InputStream и OutputStream,извлеченные из сокета. BillingClientService является потомком класса Thread,поэтому после запуска (вызов метода start) начинает обрабатывать клиентский запрос параллельно, в то время как BillingService возвращается к ожиданию соединения со следующим клиентом. Таким образом, предлагаемый класс BillingService способен одновременно обрабатывать соединения с несколькими клиентами.
Поскольку основной задачей системы является выполнение операций по обслуживанию клиентских карт, класс BillingService предлагает соответствующие методы - всего их четыре:
card и personName - ФИО пользователя;card на величину money (пополнение счета);card на величину money (оплата картой);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). Получив в конструкторе два потока - поток ввода и поток вывода, он способен с их помощью как принимать данные, отправляемые клиентом, так и посылать ему данные.
Клиентское приложение должно иметь возможность передать серверу информацию о том, какие действия должен выполнить сервер - создать новую карту, изменить баланс существующей и т.д. Причем для многих из этих операций нужно передать дополнительную информацию - об идентификаторе карты, о сумме, которую с нее предстоит списать, и т.д. Таким образом, поскольку все, что у нас есть - это потоки, нам предстоит разработать некое соглашение о том, как передавать информацию от клиента к серверу, или, говоря другими словами, создать собственный прикладной протокол для нашей системы.
Разработка таких протоколов, вообще говоря, задача не простая, поскольку от ее решения во многом зависит производительность и надежность будущей системы. В нашем же случае мы реализуем простейший протокол, сообщения в котором будут иметь следующий вид:
| Данные |
При этом количество и типы данных для каждой операции различны, а вот коды операции имеют целый тип, и их значения заранее известны.
Зафиксировав такое соглашение, мы можем построить обработку клиентского соединения следующим образом: в цикле читаем из потока целое число, которое интерпретируем как
Проиллюстрируем этот механизм на примере обработки операции по заведению новой карты.
Клиент посылает серверу данные следующего формата: ADD_NEW_CARD имеет код 1), затем строку с именем владельца, затем строку, содержащую идентификатор карты. BillingClientService в методе run считывает
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
Что касается клиентского приложения, то в данном случае оно устроено очень просто. Сетевое имя сервера оно принимает в качестве
После создания карт в цикле выполняется метод 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 из предыдущего примера. И действительно, он точно так же хранит
Однако есть и отличие - теперь в качестве элемента хранения в
Наибольшие отличия от примера из предыдущего раздела имеет класс 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 }
Во-первых, в качестве
Поскольку класс BillingClientService является потомком Thread,вся его функциональность сконцентрирована в методе run.Так же как и в предыдущем примере, этот метод представляет собой цикл ожидания сообщений от клиента. Однако метод чтения клиентских сообщений значительно отличается от метода, рассмотренного в предыдущем примере. В данном случае, при использовании метода readObject() класса ObjectInputStream,считывается экземпляр посланного клиентом класса (строка 32). Поскольку неизвестно заранее, какие именно классы прислал клиент (напомним, транспортных классов в нашей системе два - класс карт и класс операций над ними), осуществляется проверка типов (строки 33, 38, 43). Если присланный объект оказался массивом Card,то осуществляется его преобразование к этому типу (строка 34), после чего в цикле производится вызов соответствующего метода создания новой карты, в качестве параметра которому передается соответствующий элемент массива, т.е. значение типа Card.
В случае если присланный объект оказывается массивом CardOperation,осуществляется его
Если же присланный объект оказался объектом класса String,то мы интерпретируем этот вызов как запрос карты по ее номеру и отправляем карту клиенту (строка 44).
Стоит обратить внимание на простоту этой операции. Все, что нужно сделать, - вызвать метод writeObject класса ObjectOutputStream.Поскольку класс Card объявлен как
Итак, для передачи копии карты клиенту сервер вызывает метод writeObject,а клиент должен прочитать карту соответствующим вызовом readObject.
При использовании
Класс BillingClient
Реализация клиента (класс BillingClient - пример 4.8) похожа на реализацию из предыдущего примера, за исключением, конечно, способа передачи данных.
После запуска теста - вызов startTest из метода main (строка 77) - происходит соединение с сервером - connectToServer (строка 18), после чего формируется два массива. Первый массив содержит объекты класса Card (это те карты, которые затем будут переданы на сервер для их заведения), второй - массив из 30000 операций над этими картами. Вызов метода processCard (строка 23) передает на сервер массив карт, вызов processOperation (строка 34) передает на сервер массив операций.
Для передачи методы processCard и processOperation используют уже знакомый нам метод writeObject класса ObjectOutputStream.
После того, как соответствующие карты и операции по ним переданы для обработки на сервер, с помощью вызовов метода getCard с сервера запрашиваются копии соответствующих объектов. Поскольку у класса Card
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 }
Рабочий каталог расположен в
Следующие разделы курса посвящены различным программным технологиям, позволяющим реализовать распределенные системы. Наилучшей иллюстрацией использования этих технологий была бы программная система, созданная на их основе. Тут, однако, существует следующая опасность: если эта разрабатываемая система будет слишком сложна, то за ее сложностью может потеряться изучаемый предмет, если же она будет слишком проста, то может возникнуть вопрос, зачем для решения такой простой задачи применяются столь изощренные средства. Поэтому для иллюстрации основных рассматриваемых в курсе технологий выбрана достаточно простая система, позволяющая сконцентрироваться на важных для нас моментах, не углубляясь слишком сильно в предметную область. С другой стороны, рассматриваемая задача является достаточно сложной, чтобы проиллюстрировать на ее примере некоторые типичные проблемы, возникающие при разработке серьезных распределенных систем.
В качестве такой тестовой задачи рассмотрим задачу автоматизации некоторого отдельного контура работы сети столовых: необходимо разработать систему, которая автоматизирует участок работы, связанный с обслуживанием пластиковых карт. Дело в том, что по легенде в указанных столовых предусмотрен дополнительный сервис: постоянным клиентам выдают пластиковые карты, при предъявлении которых клиент получает существенную скидку. При этом на карту можно положить некоторую сумму денег и расплачиваться за обеды не наличными, а средствами, находящимися на карте. В целях экономии затрат на эмиссию карты закуплены не чиповые, а штриховые (такая карта несет только код - идентификатор клиента). Существенным является то обстоятельство, что карта, выданная в одной столовой, может использоваться в другой столовой. Между столовыми нет устойчивых защищенных каналов связи, в связи с этим встает проблема передачи баланса карт между столовыми. Кроме того, в будущем руководство предполагает изучать предпочтения клиентов, чтобы формировать более гибкую ценовую политику и более рационально управлять ассортиментом предлагаемых блюд.
В дальнейшем при реализации того или иного примера будут выдвигаться некоторые дополнительные условия и ограничения, позволяющие иллюстрировать те или иные аспекты рассматриваемых технологий.
Разобранный в предыдущем разделе пример уже достаточно сложен и содержит примеры всех необходимых нам технологий, для того чтобы послужить прототипом для первой версии нашей "ресторанной" системы. Чтобы программы-примеры не получались слишком большими, договоримся, что наша система будет поддерживать очень ограниченный набор операций:
Кроме того, практически не будут реализованы механизмы защиты от перерасхода средств, механизмы контроля действий пользователя и т.д., поскольку главной задачей рассматриваемых программ является демонстрация тех или иных механизмов взаимодействия, а не демонстрация реализации соответствующей бизнес-логики.
Класс BillingService
Первый серверный класс BillingService (пример 4.1) очень похож на класс TCPServer из предыдущего примера - так же как и TCPServer,он создает серверный сокет, после чего большую часть времени проводит в ожидании соединения клиентов. При подключении клиента создается экземпляр класса BillingClientService,в конструктор которого передаются, соответственно, InputStream и OutputStream,извлеченные из сокета. BillingClientService является потомком класса Thread,поэтому после запуска (вызов метода start) начинает обрабатывать клиентский запрос параллельно, в то время как BillingService возвращается к ожиданию соединения со следующим клиентом. Таким образом, предлагаемый класс BillingService способен одновременно обрабатывать соединения с несколькими клиентами.
Поскольку основной задачей системы является выполнение операций по обслуживанию клиентских карт, класс BillingService предлагает соответствующие методы - всего их четыре:
card и personName - ФИО пользователя;card на величину money (пополнение счета);card на величину money (оплата картой);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). Получив в конструкторе два потока - поток ввода и поток вывода, он способен с их помощью как принимать данные, отправляемые клиентом, так и посылать ему данные.
Клиентское приложение должно иметь возможность передать серверу информацию о том, какие действия должен выполнить сервер - создать новую карту, изменить баланс существующей и т.д. Причем для многих из этих операций нужно передать дополнительную информацию - об идентификаторе карты, о сумме, которую с нее предстоит списать, и т.д. Таким образом, поскольку все, что у нас есть - это потоки, нам предстоит разработать некое соглашение о том, как передавать информацию от клиента к серверу, или, говоря другими словами, создать собственный прикладной протокол для нашей системы.
Разработка таких протоколов, вообще говоря, задача не простая, поскольку от ее решения во многом зависит производительность и надежность будущей системы. В нашем же случае мы реализуем простейший протокол, сообщения в котором будут иметь следующий вид:
| Данные |
При этом количество и типы данных для каждой операции различны, а вот коды операции имеют целый тип, и их значения заранее известны.
Зафиксировав такое соглашение, мы можем построить обработку клиентского соединения следующим образом: в цикле читаем из потока целое число, которое интерпретируем как
Проиллюстрируем этот механизм на примере обработки операции по заведению новой карты.
Клиент посылает серверу данные следующего формата: ADD_NEW_CARD имеет код 1), затем строку с именем владельца, затем строку, содержащую идентификатор карты. BillingClientService в методе run считывает
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
Что касается клиентского приложения, то в данном случае оно устроено очень просто. Сетевое имя сервера оно принимает в качестве
После создания карт в цикле выполняется метод 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 из предыдущего примера. И действительно, он точно так же хранит
Однако есть и отличие - теперь в качестве элемента хранения в
Наибольшие отличия от примера из предыдущего раздела имеет класс 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 }
Во-первых, в качестве
Поскольку класс BillingClientService является потомком Thread,вся его функциональность сконцентрирована в методе run.Так же как и в предыдущем примере, этот метод представляет собой цикл ожидания сообщений от клиента. Однако метод чтения клиентских сообщений значительно отличается от метода, рассмотренного в предыдущем примере. В данном случае, при использовании метода readObject() класса ObjectInputStream,считывается экземпляр посланного клиентом класса (строка 32). Поскольку неизвестно заранее, какие именно классы прислал клиент (напомним, транспортных классов в нашей системе два - класс карт и класс операций над ними), осуществляется проверка типов (строки 33, 38, 43). Если присланный объект оказался массивом Card,то осуществляется его преобразование к этому типу (строка 34), после чего в цикле производится вызов соответствующего метода создания новой карты, в качестве параметра которому передается соответствующий элемент массива, т.е. значение типа Card.
В случае если присланный объект оказывается массивом CardOperation,осуществляется его
Если же присланный объект оказался объектом класса String,то мы интерпретируем этот вызов как запрос карты по ее номеру и отправляем карту клиенту (строка 44).
Стоит обратить внимание на простоту этой операции. Все, что нужно сделать, - вызвать метод writeObject класса ObjectOutputStream.Поскольку класс Card объявлен как
Итак, для передачи копии карты клиенту сервер вызывает метод writeObject,а клиент должен прочитать карту соответствующим вызовом readObject.
При использовании
Класс BillingClient
Реализация клиента (класс BillingClient - пример 4.8) похожа на реализацию из предыдущего примера, за исключением, конечно, способа передачи данных.
После запуска теста - вызов startTest из метода main (строка 77) - происходит соединение с сервером - connectToServer (строка 18), после чего формируется два массива. Первый массив содержит объекты класса Card (это те карты, которые затем будут переданы на сервер для их заведения), второй - массив из 30000 операций над этими картами. Вызов метода processCard (строка 23) передает на сервер массив карт, вызов processOperation (строка 34) передает на сервер массив операций.
Для передачи методы processCard и processOperation используют уже знакомый нам метод writeObject класса ObjectOutputStream.
После того, как соответствующие карты и операции по ним переданы для обработки на сервер, с помощью вызовов метода getCard с сервера запрашиваются копии соответствующих объектов. Поскольку у класса Card
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 }
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.