пятница, 6 января 2012 г.

Сетевые приложения на Java. Консольный магазин.

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

        Start page.
       |          |           |
       |          |           |
       |          |           |
 catalogs    |           |
    |           basket    |
    |                          home page
    |__bikes
    |             |__Kinesis Racelite KR-210
    |
    |__skates
    |             |__ Schankel Rebec
    |
    |__bike wheels
    |                       |__Mavic 28 inches
    |
    |__skate wheels
    |                       |__Powerslide Impact 110mm
    |                       |__Matter Juice F1110
    |                       |__MPC Black Track EX-FIRM
    |                       |__Inlinebus 110mm
    |                       |__Rollerblade 100mm
    |__ skinsuits
                       |__Schankel
                       |__Rollerblade
                     

  Для реализации такого сервера на Java нам потребуется импортировать специально предназначенные для этого пакеты:


import java.net.*;
import java.io.*;
import java.util.*;

После чего, запускаем поток, который и будет обрабатывать наши запросы:

public class Server implements Runnable{

После чего, объявим поля, которые будут параметрами нашего сокета:


static ServerSocket ss;
       Socket s;
       Thread t;
       byte [] data;
       static String buf="";
       String sbuf="";
       static String out;
       static List two;
       static String temp=" ";

    PrintStream os;
    InputStreamReader dis;
    DataOutputStream dos;
    boolean stop = false;
    BufferedReader brr=new BufferedReader(new InputStreamReader(System.in)) ;

После инициализации полей, произойдёт запуск конструктора по умолчанию нашего класса Server:


    Server()throws Exception
    {   s=ss.accept();

        dis = new InputStreamReader(s.getInputStream());
        dos = new DataOutputStream(s.getOutputStream());
        t= new Thread(this);
        t.start();
    }

Строчкой  "t.start();" мы запускаем выполнение потока, который будет описан.
Именно для его запуска с определёнными параметрами и служит код приведённого конструктора.
   Далее, опишем собственно тело потока, который будет запущен:


    public void run ()
    {
        try
        {
                   do
                   {
                       if(dis.ready())
                       {
                      while(dis.ready())


  Здесь переопределяется метод run() интерфейса Runnable, от которого унаследован наш класс Server. До тех пор, пока входной и выходной потоки будут готовы принимать и отправлять пакеты, будет выполняться дальнейшее тело цикла. А что будет в теле цикла? То, что и полагается делать серверу. Чтение потока на предмет запросов клиента и соответствующие ответы сервера на определённые запросы клиента.
  В контексте онлайн-магазина запросами могут быть : выбор каталога, выбор товара, добавление товара в корзину.
  Первое, что нам нужно сделать - прочитать и сохранить в промежуточной структуре данных запрос пользователя:


                      {
                          Character c =(char)dis.read();
                          buf=buf+c.toString();

                      }

                       List two=new ArrayList();
    String a=buf;
    StringTokenizer st = new StringTokenizer(a);

     while (st.hasMoreTokens()) {
         two.add(st.nextToken());
    }
    String mask=two.get(two.size()-1).toString();

   Итак, возможные запросы, в виде отдельных слов, отделённых пробелами (с помощью класса StringTokenizer), добавлены в список "two", крайний элемент которого каждый раз присваивается определённой переменной строкового типа mask. Зачем городить огород с динамически расширяемым массивом (коллекцией) типа List по имени two, будет ясно далее. Нам нужно хранить результаты предыдущих запросов для некоторых дальнейших операций.
   Далее, выведем по запросу пользователя "m" (без запроса пользователя сервер никому ничего не пошлёт. Он, как вампир, может войти, точнее, что-то отправить только по приглашению, точнее, когда его об этом попросили), отправим ему главную страничку нашего магазина:


if(mask.contains("m")&&mask.length()<2){
       String out="Hi, it's my Stas Network Shop! \n - enter 'c' to view the catalogs \n - enter 'b' to view your basket \n - enter 'h' to view my home page";                    
         os = new PrintStream(s.getOutputStream());
                     os.println(out);
                           }

Операторами \n обозначается перенос строки.
В итоге, по запросу "m", пользователь получит такой ответ:


Hi, it's my Stas Network Shop!
 - enter 'c' to view the catalogs
 - enter 'b' to view your basket
 - enter 'h' to view my home page

  Здесь мы предлагаем пользователю либо просмотреть каталоги, либо содержимое корзины, либо посмотреть домашнюю страницу с контактами продавца, путём ввода соответствующих запросов.
  Разберём запрос на просмотр каталогов. Его нужно обработать, то есть, в ответ на ввод команды "с", сервер должен вернуть пользователю содержимое каталогов и инструкции для дальнейших действий. Для обработки запроса, нам нужно совершить следующие действия:
1. Считать содержимое буфера.
2. Убедиться, что содержимое буфера соответствует определённому запросу.
3. В случае, если оно соответствует, обратиться к слою хранения данных и извлечь оттуда соответствующую запросу информацию.
4. Вывести эту информацию пользователю.

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

public class Read {
    static String catalogsList=" ";
    static String bikeList=" ";
    static String Specs001=" ";
    static String skateList=" ";
    static String Specs011=" ";
    static String bikeWheels=" ";
    static String skateWheels=" ";
    static String skinsuits=" ";
    static String PS=" ";
    static String Juice=" ";
    static String BlackTrack=" ";
    static String Inlineb=" ";
    static String Rollerb=" ";
    static String Schankel=" ";
    static String Northshore=" ";
    static String bask=" ";
    static String specs=" ";

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

    public static String ShowCatalogs(){
        catalogsList="";
        Map catalog=new HashMap();
        catalog.put(1, "bikes");
        catalog.put(2, "skates");
        catalog.put(3, "bike wheels");
        catalog.put(4, "skate wheels");
        catalog.put(5, "skinsuits");
        for (int i=1;i<catalog.size()+1;i++){
    String temp = i+" "+catalog.get(i).toString()+'\n';
    catalogsList=catalogsList.concat(temp);
        }
        catalogsList=catalogsList+'\n'+"Enter the number of selected category";
        return catalogsList;

После этого, на стороне сервера, нам остаётся только отреагировать на запрос вызова каталогов и вызвать соответствующий метод класса Read:

                      Read r=new Read();
                       if(mask.contains("c")&&mask.length()<2){                          
                       Server.out=r.ShowCatalogs();
                           }
                     os.println(Server.out);
  Аналогично создадим методы ShowBikes(), Bike001Specs(), ShowSkates(), Skate011Specs(), ShowBikeWheels(), ShowSkateWheels() и другие, для показа списков и характеристик товара в каждой категории.
Таким образом, при вводе "с", клиент получит на экран содержимое каталогов:

1 bikes
2 skates
3 bike wheels
4 skate wheels
5 skinsuits

Enter the number of selected category


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

                if(mask.contains("2")&&mask.length()<2){
                       Server.out=r.ShowSkates();
                       os.println(Server.out);
                           }
с вызовом соответствующего метода ShowSkates(), описанного в классе Read следующим образом:

public static String ShowSkates(){
Map skates = new HashMap();
skates.put(1, "Schankel Rebec & Luigino P-51 Pilot");
for (int i=1;i<skates.size()+1;i++){
    String temp = "01"+i+" "+skates.get(i).toString()+'\n';
    skateList=skateList.concat(temp)+'\n'+"Enter the number of selected Skate to view specification";
      }
return skateList;
  }

в результате чего выведется номера артикулов и наименования товаров, а также, дальнейшие инструкции. В данном случае - один единственный комплект беговых роликов на основе ботинка Schankel Rebec и рамы Luigino P-51 Pilot и предложение просмотреть его спецификации:

 011 Schankel Rebec & Luigino P-51 Pilot

Enter the number of selected Skate to view specification

  После чего, если пользователь вводит артикул интересующего его товара, сервер считывает и идентифицирует запрос:

                 if(mask.contains("011")&&mask.length()<4){
                       Server.out=r.Skate011Specs();
                       os.println(Server.out);
                           }

Вызывая соответствующий метод класса Read:

public static String Skate011Specs(){
        Map specs011=new HashMap();
        specs011.put(1, "Boot: Schankel Rebec, carbon");
        specs011.put(2,"Wheels:Powerslide Impact 82A 110mm, Slightly used");
        specs011.put(3, "Frame:Luigino P-51 Pilot, 4x110");
        specs011.put(4, "Bearings:Chinese Noname abec7 standard");
        specs011.put(5,"Price:900 USD");
for (int i=1;i<specs011.size()+1;i++){
    String temp = i+" "+specs011.get(i).toString()+'\n';
    Specs011=Specs011.concat(temp);
        }
    Specs011=Specs011+'\n'+"Enter 's' to add item to your basket";
    return Specs011;
    }

В результате чего, клиент получит то, что он запрашивал: спецификацию товара "роликовые коньки" с артикулом "011":

1 Boot: Schankel Rebec, carbon
2 Wheels:Powerslide Impact 82A 110mm, Slightly used
3 Frame:Luigino P-51 Pilot, 4x110
4 Bearings:Chinese Noname abec7 standard
5 Price:900 USD

Enter 's' to add item to your basket

  Предположим, пользователю на стороне клиента понравились мои беговые ролики с карбоновым ботинком (ещё бы, не понравились бы...), в результате чего он решает добавить их в корзину.
  Для этого, он введёт "s". Тут-то нам и понадобиться знать, что пользователь вводил до этого и пригодится коллекция по имени "two", которую мы создавали в самом начале для хранения запросов пользователя. Итак, нам нужно извлечь значение предыдущего запроса, по нему в другой структуре данных получить описание товара в корзине и всё это вместе добавить в структуру данных basket, выполняющую роль нашей корзины, после чего, отправить пользователю уведомление о содержимом корзины. Непростая и немаленькая задачка.
Для выдачи описаний товаров для корзины по запросам, создадим метод GetAllSpecs(String id), возвращающий описание товара по номеру артикула:

public static String GetAllSpecs(String id){
    Map AllSpecs = new HashMap();
    AllSpecs.put("011", "Schankel Rebec & Luigino P-51 4x110 speedskates");
    AllSpecs.put("033", "MPC Black Track skate wheels, EX-FIRM, 110mm");
    ..........

    specs=AllSpecs.get(id).toString();
    return specs;
}

Для вызова этого метода из сервера, запишем:

                 if(mask.contains("s")&&mask.length()<2){
 basket.put(1, two.get(two.size()-2).toString());
           
              temp=temp+" "+basket.get(1).toString()+r.GetAllSpecs(two.get(two.size()-2).toString())+","+'\n';
                
           Server.out=temp;
           os.println(Server.out);
           }
В результате чего, данные о товарах в корзине будут выводиться через запятую и каждый товар в новой строке.

  Допустим, к комплекту роликов покупатель захотел трековые колёса MPC Black Track, которые также выставлены на продажу. Для этого он запрашивает список колёс, вводя "4". Его запрос на сервере обрабатывается аналогично предыдущим:

                            if(mask.contains("4")&&mask.length()<2){
                       Server.out=r.ShowSkateWheels();
                          os.println(Server.out);
                            }

А в слое хранения данных, класс, отвечающий за хранение и извлечение списка колёс, выглядит следующим образом:

public static String ShowSkateWheels(){
Map skatewheels = new HashMap();
skatewheels.put(1, "Powerslide Impact 110mm, 82A, slightly used");
skatewheels.put(2, "Matter Juice F1110, almost new");
skatewheels.put(3, "MPC Black Track EX-FIRM, 110mm, almost new");
skatewheels.put(4, "Inlinebus 110mm, F1, strongly used, 102 mm remaining");
skatewheels.put(5, "Rollerblade 100mm, 84A, almost dead, 88mm remaining");
for (int i=1;i<skatewheels.size()+1;i++){
    String temp = "03"+i+" "+skatewheels.get(i).toString()+'\n';

    skateWheels=skateWheels.concat(temp);
    }
skateWheels=skateWheels+'\n'+"Enter the number of selected skate wheel to view specification";
return skateWheels;
  }

  И выведет клиенту такие данные по его запросу:

 031 Powerslide Impact 110mm, 82A, slightly used
032 Matter Juice F1110, almost new
033 MPC Black Track EX-FIRM, 110mm, almost new
034 Inlinebus 110mm, F1, strongly used, 102 mm remaining
035 Rollerblade 100mm, 84A, almost dead, 88mm remaining

Enter the number of selected skate wheel to view specification

После чего, он введёт артикул интересующих его MPC Black Track, "033", который обработается аналогично случаю с роликами, специальным классом и специальным запросом, выведя ему результат:

1 Nominal Diameter: 110mm
2 Milage: about 100 km
3 Hardness: F1 (86A)
4 Remaining diamter: 110mm
5 Price:140 USD

Enter 's' to add item to your basket

  После добавления в корзину, путём ввода "s", пользователю вновь выводится обновлённое её содержимое:

 011 Schankel Rebec & Luigino P-51 4x110 speedskates,
 033 MPC Black Track skate wheels, EX-FIRM, 110mm,

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


Теперь, обсудим самое начало. Как сервер будет запущен в операционной системе и как клиент сможет к нему подключиться?
  Для этого служит метод Main нашего сокета. Собственно, с него и начнётся выполнение программы при запуске сервера, хотя код этого метода будет записан в самом конце класса Server. Вначале, закончим тело нашего цикла, после того, как мы прописали обработчиков всех возможных запросов от пользователя:

 }              
                   }while(!stop);
        }catch (Exception ex){System.out.println("server run ex "+ex);}
    }

Теперь пропишем главный метод, с которого начнётся выполнение класса Server:

 public static void main(String args[])throws Exception
    {      //The main class, where program starts its execution

        System.out.println("enter port");
        Read bl=new Read();
int port=Integer.parseInt(bl.brr.readLine());
//entering the port number information for socket creation
        ss=new ServerSocket(port);
        do
        {
            new Server();
        }while(true);

    }


}

Здесь сервер, запускаясь, запросит у администратора имя порта, к которому он подключится. Тут уж всё зависит от конкретной операционной системы и машины, какие там порты у Вас свободны. Я, в случае с Linux Ubuntu, использую порт 1024.
Поэтому, на сообщение:
enter port
вводим:
1024
 и нажимаем Enter.

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

$ telnet localhost 1024

 после чего появляется сообщение:
Trying ::1...
Connected to localhost.localdomain.
Escape character is '^]'

Также, можно подключиться к серверу с удалённой машины через локальную сеть, указав вместо localhost  соответствующий IP-адрес машины, на которой запущен данный сервер.