* Решение на вариант 1 от изпит 14.01.2013г.
Публикувано на 20 януари 2013 в раздел ПИК3 Java.
Тук ще дам едно възможно (не много прегледно написано) решение на вариант 1 от задачата за изпит. Условието гласеше следното:
Техническата поддръжка на софтуерна компания се обслужва чрез специализирана система за управление на входящи писма (Ticketing system). В системата работят потребители с две възможни роли – клиенти (customers) и техническа поддръжка (support). Техническата поддръжка имат потребителски имена, а клиентите имат клиентски номер. Нормалната работа включва следната последователност:
- Клиентите изпращат съобщение (ticket) в свободен текст;
- Всички съобщения се получават в общ екран (inbox) на системата, като всяко ново съобщение приема свой собствен уникален номер. Всеки човек от техническата поддръжка може да вижда и чете всички съобщения в тази входяща кутия;
- Някой от техническата поддръжка може да си избере да работи по входящо съобщение. В този случай то се маркира, че той е назначен да го разрешава. По този начин другите виждат, че той работи по съобщението и съответно се предотвратяват дублирани отговори;
- След като служителят от техническата поддръжка прочете съобщението, той разрешава проблема (това действие няма отношение към системата, за него той използва външен софтуер) и маркира съобщението като разрешено (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. Потърсете и поправете тези моменти.
...Тези нишки ще приемат връзка и ще стартират нови (да ги наречем поднишки), в които ще се обработва сесиите на свързалите се клиент/поддръжка. Това ще го направим с цел да стане възможно двама или повече клиенти да изпращат съобщения едновременно, както и двама или повече хора от поддръжката да работят едновременно със системата.
Поднишките в случая са анонимните класове:
var;
new Thread(){
Thread initialize( var){
this.var = var;
return this; // връща текущия обект
}
public void run(){
...
}
}.initialize(var).start();
в класовете CustomersThread и SupportThread.
Правилно ли съм разбрал?
Грешно ли ще е ако методите list(), get(), resolve(), release() са в класа SupportThread?