C, PHP, VB, .NET

Дневникът на Филип Петров


* Решение на вариант 1 от изпит 14.01.2013г.

Публикувано на 20 януари 2013 в раздел ПИК3 Java.

Тук ще дам едно възможно (не много прегледно написано) решение на вариант 1 от задачата за изпит. Условието гласеше следното:

Техническата поддръжка на софтуерна компания се обслужва чрез специализирана система за управление на входящи писма (Ticketing system). В системата работят потребители с две възможни роли – клиенти (customers) и техническа поддръжка (support). Техническата поддръжка имат потребителски имена, а клиентите имат клиентски номер. Нормалната работа включва следната последователност:

  1. Клиентите изпращат съобщение (ticket) в свободен текст;
  2. Всички съобщения се получават в общ екран (inbox) на системата, като всяко ново съобщение приема свой собствен уникален номер. Всеки човек от техническата поддръжка може да вижда и чете всички съобщения в тази входяща кутия;
  3. Някой от техническата поддръжка може да си избере да работи по входящо съобщение. В този случай то се маркира, че той е назначен да го разрешава. По този начин другите виждат, че той работи по съобщението и съответно се предотвратяват дублирани отговори;
  4. След като служителят от техническата поддръжка прочете съобщението, той разрешава проблема (това действие няма отношение към системата, за него той използва външен софтуер) и маркира съобщението като разрешено (resolved), като това включва преместване на съобщението от списъка с входящи към списъка с разрешени казуси.

Моделирайте сървърната част на въпросната система и създайте нужните класове. НЕ е нужно да реализирате клиентските приложения! Трябва да създадете само и единствено централния сървър!

Сега да подходим към едно възможно решение на задачата. Със системата ще работят два вида потребители - клиенти и техническа поддръжка. Те ще извършват коренно различни действия. Поради тази причина бихме могли да подходим по два различни начина:

  • Да има един общ ServerSocket, който ще приема всякакви връзки (и от клиенти, и от поддръжка), след което ще прави автентикация, т.е. на базата на подадени данни ще преценява дали свързалия се е клиент или е от поддръжката;
  • Да има два ServerSockets - на един порт ще се свързват клиентите, а на друг порт ще се свързват хората от поддръжката. Така сървърът недвусмислено ще знае кой е клиент и кой е от поддръжката.

В примерното решение ще използваме втория подход. За целта ще създадем две нишки - една, в която ще се очакват включвания от клиенти и една, в която ще се очакват включвания на хора от поддръжката. Тези нишки ще приемат връзка и ще стартират нови (да ги наречем поднишки), в които ще се обработва сесиите на свързалите се клиент/поддръжка. Това ще го направим с цел да стане възможно двама или повече клиенти да изпращат съобщения едновременно, както и двама или повече хора от поддръжката да работят едновременно със системата.

Също така знаем, че клиентите ще изпращат съобщения, а поддръжката ще работят с тези съобщения. С други думи ще имаме споделен ресурс - списък със съобщения. Него ще направим статичен в освовния клас на сървъра. Ще направим и статични методи за обработка на този списък, които ще се използват от поддръжката - четене на съобщение + маркиране на съобщение като "взето", премахване на маркера за взето съобщение, разрешаване на съобщение. Ето как ще изглежда основния клас:

package server;

import java.util.ArrayList;
import java.io.IOException;
import java.util.Iterator;

public class Server{
  static ArrayList inbox = new ArrayList();
  static final int CUSTOMERSPORT = 4444;
  static final int SUPPORTPORT = 4445;
  public static void main(String[] args){
    try{
      Thread t1 = new Thread(new CustomersThread(CUSTOMERSPORT));
      t1.start();
      Thread t2 = new Thread(new SupportThread(SUPPORTPORT));
      t2.start();
    }
    catch(IOException e){
      System.err.println("Cannot open server sockets");
    }
  }
  // Метод, който връща списък със съобщенията към поддръжката
  static String list(){
    if(Server.inbox.isEmpty()) return "No messages";
    StringBuffer strb = new StringBuffer();
    for(Message m: Server.inbox){
      if(m.supportUsernameWorkingOnIt == null){
        strb.append("MID: "+m.messageID+" from CID: "+m.clientID+"\n");
      }
      else{
        strb.append("MID: "+m.messageID+" from CID: "
                     +m.clientID+" taken by: "
                     +m.supportUsernameWorkingOnIt+"\n");
      }
    }
    return strb.toString();
  }
  // метод, който връща съобщение със съответно messageID
  // и същевременно с това маркира съобщението като "взето"
  static String get(int messageID, String username){
    String result = "No such message";
    for(Message m: Server.inbox){
      if(m.messageID == messageID){
        m.supportUsernameWorkingOnIt = username;
        result = "Message id: "+m.messageID+"\n"
          + "Client id: "+m.clientID+"\n"
          + "Message text: "+m.text+"\n";
        break;
      }
    }
    return result;
  }
  // метод, който "разрешава" подаден казус:
  // премахва се от списъка inbox и се връща
  // като резултат, за да може поддръжката да
  // си го запише в списъка с разрешени казуси
  static Message resolve(int messageID){
    Message result;
    Iterator it = Server.inbox.iterator();
    while(it.hasNext()){
      if(((result = (Message)it.next()).messageID) == messageID){
        it.remove();
        return result;
      }
    }
    return null;
  }
  // ако някой от поддръжката е маркирал съобщение
  // но впоследствие е решил, че не може да се справи с него
  // с тази функция ще го освободи
  static Boolean release(int messageID){
    Iterator it = Server.inbox.iterator();
    Message m;
    while(it.hasNext()){
      if(((m = (Message)it.next()).messageID) == messageID){
        m.supportUsernameWorkingOnIt = null;
        return true;
      }
    }
    return false;
  }
}

Сега да пристъпим към клас "Message". Той е сравнително елементарен:

package server;

public class Message{
  // идентификатор на съобщението
  final int messageID;
  // чрез тази променлива ще правим уникално id 
  // на новите съобщения - ще бъдат с поредни номера
  private static int lastMessageID = 1;
  // идентификационен номер на клиента
  final int clientID;
  // текст на съобщението
  String text;
  // ако тази променлива е null, значи никой не
  // работи по съобщението, а ако НЕ е null
  // то ще записваме името на човека от поддръжката
  // който работи по него
  String supportUsernameWorkingOnIt;
  // конструктор
  public Message(int clientID, String text){
      this.clientID = clientID;
      this.text = text;
      this.messageID = Message.lastMessageID;
      Message.lastMessageID++;
      supportUsernameWorkingOnIt = null;
  }
}

По-сложни са класовете, които ще управляват нишките. Първо да разгледаме сравнително по-простия за клиентите:

package server;

import java.net.ServerSocket;
import java.net.Socket;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.EOFException;

public class CustomersThread implements Runnable{
  private static ServerSocket ss;
  public CustomersThread(int port) throws IOException{
    CustomersThread.ss = new ServerSocket(port);
  }
  public void run(){
    while(true){
      Socket sock = null;
      try{
        sock = ss.accept();
      }
      catch(IOException e){
        System.err.println("Can open socket with support rep");
        continue;
      }

      new Thread(){
        Socket sock = null;
        DataOutputStream out = null;
        DataInputStream in = null;
        int clientID = 0;
        String text = null;

        Thread initialize(Socket sock){
          this.sock = sock;
          return this;
        }

        public void run(){
          try{
            out = new DataOutputStream(sock.getOutputStream());
            in = new DataInputStream(sock.getInputStream());
            // Получаваме id на клиента
            out.writeUTF("Please send your client id");
            try{
              clientID = in.readInt();
            }
            catch(EOFException e){
              out.writeUTF("Invalid id");
              return;
            }
            // Получаваме текста на новото съобщение
            out.writeUTF("Enter the message text");
            text = in.readUTF();
          }
          catch(IOException e){
            try{
              out.writeUTF("Error receiving data");
              in.close();
              out.close();
              sock.close();
              return;
            }
            catch(IOException e2){ return; }
          }
          finally{
            try{
              if(out!=null)  out.close();
              if(in!=null)   in.close();
              if(sock!=null) sock.close();
            }
            catch(IOException e){
              System.err.println("Error closing client connection");
            }
          }
          // Добавяме получените данни като ново съобщение в списъка
          if(clientID != 0 && !text.isEmpty()){
            Message m = new Message(clientID, text);
            Server.inbox.add(m);
          }
          else{
            System.err.println("Wrong message received?");
          }
        }
      }.initialize(sock).start();
    }
  }
}

При класа с техническата поддръжка ще имаме повече възможни действия. Човекът от поддръжката ще може да подава различни команди - LIST, GET, RESOLVE, RELEASE и EXIT:

package server;

import java.net.ServerSocket;
import java.net.Socket;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;

public class SupportThread implements Runnable{
  private static ServerSocket ss;
  // Списък за вече разрешени казуси
  private static ArrayList resolvedMessages = new ArrayList();

  public SupportThread(int port) throws IOException{
    SupportThread.ss = new ServerSocket(port);
  }
  public void run(){
    while(true){
      Socket sock = null;
      try{
        sock = ss.accept();
      }
      catch(IOException e){
        System.err.println("Can open socket with support rep");
        continue;
      }

      new Thread(){
        Socket sock = null;
        DataOutputStream out = null;
        DataInputStream in = null;
        String username = null;
        Thread initialize(Socket sock){
          this.sock = sock;
          return this;
        }
        public void run(){
          try{
            out = new DataOutputStream(sock.getOutputStream());
            in = new DataInputStream(sock.getInputStream());
            // Получаваме потребителското име
            out.writeUTF("Please send your username");
            username = in.readUTF();
            // Изпращаме списък с възможните команди
            String options = "Commands available are: "
                             +"LIST, GET, RESOLVE, RELEASE, EXIT";
            out.writeUTF(options);

            String answer = null;
            do{
              answer = in.readUTF();
              switch(answer){
                case "LIST":
                      out.writeUTF(Server.list());
                      break;
                case "GET":  
                      out.writeUTF("Now send message id");
                      int messageID;
                      try{
                         messageID = Integer.parseInt(in.readUTF());
                      }
                      catch(NumberFormatException nfe){
                         out.writeUTF("Invalid id");
                         break;
                      }
                      String message = Server.get(messageID, username);
                      if(message.isEmpty()) out.writeUTF("Invalid message");
                      else out.writeUTF(message);
                      break;
                case "RESOLVE": 
                      out.writeUTF("Now send message id");
                      int msgID;
                      try{
                         msgID = Integer.parseInt(in.readUTF());
                      }
                      catch(NumberFormatException nfe){
                         out.writeUTF("Invalid id");
                         break;
                      }
                      Message resolved = Server.resolve(msgID);
                      if(resolved == null) out.writeUTF("No such message");
                      else{
                         SupportThread.resolvedMessages.add(resolved);
                         out.writeUTF("Resolved");
                      }
                      break;
                case "RELEASE": 
                      out.writeUTF("Now send message id");
                      int mID;
                      try{
                         mID = Integer.parseInt(in.readUTF());
                      }
                      catch(NumberFormatException nfe){
                         out.writeUTF("Invalid id");
                         break;
                      }
                      Boolean result = Server.release(mID);
                      if(result) out.writeUTF("Released "+mID);
                      else out.writeUTF("No such message");
                      break;
                case "EXIT":
                      break;
                default: 
                      out.writeUTF("Unrecognized command");
              }
            }
            while(!answer.equals("EXIT"));
          }
          catch(IOException e){
            try{
              out.writeUTF("Error receiving data");
              in.close();
              out.close();
              sock.close();
              return;
            }
            catch(IOException e2){ return; }
          }
          finally{
            try{
              if(out!=null)  out.close();
              if(in!=null)   in.close();
              if(sock!=null) sock.close();
            }
            catch(IOException e){
              System.err.println("Error closing client connection");
            }
          }
        }
      }.initialize(sock).start();
    }
  }
}

С това задачата е решена. Тук искам специално да отбележа една конструкция за анонимни класове, която използвам (не е задължително да бъде реализирано точно така, но в случая я показвам като възможност). Един основен проблем на анонимните класове е, че при тях няма конструктори. В случая обаче ние се нуждаем от конструктор, с който да инициализираме sock променливата, която ще се използва в тялото на run() метода на анонимния клас. Ето как можем да го направим (прототип) за "анонимен клас с конструктор":

<type> var; // тази променлива ще се инициализира
new Thread(){
   <type> var; // член променлива
   // метод, който ще върши работата на конструктор
   Thread initialize(<type> var){
      this.var = var;
      return this; // връща текущия обект
   }
   public void run(){
      ...
   }
}.initialize(var).start(); // стартираме върнатия thread

Нека покажем и примерни клиентски приложения, с които можете да експериментирате. Клиент:

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

public class Client{
  private static final int ID = 2;
  public static void main(String[] args){
    try{
      Socket sock = new Socket("localhost", 4444);
      DataOutputStream out = new DataOutputStream(sock.getOutputStream());
      DataInputStream in = new DataInputStream(sock.getInputStream());
      Scanner keyboard = new Scanner(System.in);
      System.out.println("Server said: "+in.readUTF());
      out.writeInt(ID);
      System.out.println("I've send your ID automatically");
      System.out.println("Server said: "+in.readUTF());
      System.out.print("Enter answer: ");
      out.writeUTF(keyboard.nextLine());
      out.close();
      in.close();
      sock.close();
    }
    catch(IOException e){
      e.printStackTrace();
    }
  }
}

Поддръжка:

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

public class Support{
  private static String username = "Petar";
  public static void main(String[] args){
    try{
      Socket sock = new Socket("localhost", 4445);
      DataOutputStream out = new DataOutputStream(sock.getOutputStream());
      DataInputStream in = new DataInputStream(sock.getInputStream());
      Scanner keyboard = new Scanner(System.in);
      System.out.println("Server said: "+in.readUTF());
      out.writeUTF(username);
      System.out.println("I've send your username automatically");
      String yourAnswer;
      do{
        System.out.println("Server said: "+in.readUTF());
        System.out.print("Enter answer: ");
        yourAnswer = keyboard.nextLine();
        out.writeUTF(yourAnswer);
      }
      while(!yourAnswer.equals("EXIT"));
      out.close();
      in.close();
      sock.close();
    }
    catch(IOException e){
      e.printStackTrace();
    }
  }
}

Остава да се поправят няколко пропуска свързани със синхронизацията на нишките. Например възможния "race condition" двама клиента да създадат ново съобщение едновременно, с което да получат едно и също id. Потърсете и поправете тези моменти.

 



2 коментара


  1. ...Тези нишки ще приемат връзка и ще стартират нови (да ги наречем поднишки), в които ще се обработва сесиите на свързалите се клиент/поддръжка. Това ще го направим с цел да стане възможно двама или повече клиенти да изпращат съобщения едновременно, както и двама или повече хора от поддръжката да работят едновременно със системата.

    Поднишките в случая са анонимните класове:
    var;
    new Thread(){
    Thread initialize( var){
    this.var = var;
    return this; // връща текущия обект
    }
    public void run(){
    ...
    }
    }.initialize(var).start();
    в класовете CustomersThread и SupportThread.
    Правилно ли съм разбрал?

  2. Грешно ли ще е ако методите list(), get(), resolve(), release() са в класа SupportThread?

Добави коментар

Адресът на електронната поща няма да се публикува


*