* Решение на вариант 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?