In un’applicazione client/server (C/S), basata su socket TCP/IP come mezzo di comunicazione, tipicamente client e server devono implementare meccanismi per:
Problemi:
Idealmente, il programmatore lato client dovrebbe concentrarsi sulla logica applicativa, richiedendo i servizi al server (sulla base della loro interfaccia), ed elaborandone i risultati in base, appunto, alla logica applicativa, separando quest’ultima dai dettagli dei meccanismi di interazione con server.
Analogamente, il programmatore lato server dovrebbe concentrarsi sulla codifica dei servizi da offrire, separandola dai dettagli dei meccanismi per la comunicazione col client.
Il Server gestisce un oggetto Contatore che permette l’inizializ-zazione e l’incremento di una variabile di conteggio.
Il Client effettua il calcolo del tempo medio di servizio su un certo numero di incrementi.
Il servizio offerto dal Server può essere descritto dalla seguente interfaccia Java:
public interface Counter {
public int sum(int sum);
public int set_sum();
public int increment();
Dove
public int sum(int sum);
è il metodo per sommare un intero al valore corrente del contatore;public int set_sum();
è il metodo per azzerare il valore corrente del contatore;public int increment();
è il metodo per incrementare il valore del contatore.Buffer per operazioni di IO per mezzo delle socket e variabile per numero di byte letti:
byte[] readBuffer = new byte[40];
byte[] writeBuffer = new byte[40];
int bytesRead;
Creazione ServerSocket
ServerSocket = new ServerSocket(250);
Aspetto la richiesta di connessione da parte di un client: socket = serverSocket.accept();
Ottengo le strutture per l'IO attraverso le socket
BufferedOutputStream ostream = new BufferedOutputStream (socket.getOutputStream());
BufferedInputStream istream = new BufferedInputStream (socket.getInputStream());
Effettuo un'operazione di read() e converto in String i byte letti:
bytesRead = istream.read(readBuffer, 0, 40);
String myOper = new String(readBuffer, 0, bytesRead);
Codifico il messaggio per poter comprendere che operazioni eseguire:
if(myOper.equals("increment"))
++sum;
else if(myOper.equals("set_sum"))
sum = 0;
Per realizzare sum() suddivido la stringa ricevuta nelle due componenti per estrarre l'addendo alla variabile contatore:
StringTokenizer st = new StringTokenizer(myOper);
String op = st.nextToken();
String add = st.nextToken();
if(op.equals("sum"))
sum = sum + Integer. parseInt(add); }
Il valore di ritorno dell'operazione effettuata viene codificato in un messaggio da inoltrare al client:
sumString = String.valueOf(sum);
writeBuffer = sumString.getBytes();
ostream.write(writeBuffer, 0, sumString.length());
ostream.flush(); } }
Variabili a supporto delle operazioni di IO per lettura e scrittura:
byte[] buffer = new byte[10];
int bytesRead;
String sumString = ""; String myOperation;
Istanzio una socket e i necessari oggetti per le operazioni di IO:
String host = args[0]; Socket soc = new Socket(host, 250);
BufferedOutputStream ostream=new BufferedOutputStream (soc.getOutputStream);
BufferedInputStream istream = new BufferedInputStream (soc.getInputStream());
Invio al server la richiesta di esecuzione operazione set_sum():
myOperation = "set_sum";
buffer = myOperation.getBytes();
ostream.write(buffer, 0, myOperation.length());
ostream.flush();
Ottengo la risposta del server (che posso scegliere di stampare a video o meno):
bytesRead = istream.read(buffer, 0, 10);
sumString = new String(buffer, 0, bytesRead);
Registro l'istante di inzio: long startTime = System.currentTimeMillis();
Effettuo 1000 richieste di incremento:
for(int i = 0; i < count; i++){
myOperation = "increment";
buffer = myOperation.getBytes();
ostream.write(buffer, 0, myOperation.length());
ostream.flush();
bytesRead = istream.read(buffer, 0, 10);
sumString = new String(buffer, 0, bytesRead);
}
Registro l'istante di conclusione: long endTime = System.currentTimeMillis();...
Questo pattern introduce nel modello di interazione client-server un ulteriore componente, il Proxy (detto anche “Surrogato” o “Ambassador”).
Il Proxy si presenta come una implementazione del servizio remoto, locale al client:
I meccanismi di “basso livello” necessari per l’instaurazione della comunicazione, per il marshalling e l’unmarshalling dei dati, sono implementati ed incapsulati all’interno del Proxy.
Benefici
Inconvenienti?
Servizio specificato da un interfaccia (InterfacciaServer).
Duplice diversa implementazione, ServerReale e Proxy
L’associazione direzionale tra Proxy e ServerRale, indicata in figura, sintetizza tutti i meccanismi necessari al Proxy per riferirsi al server reale.
Il client possiede un riferimento ad un oggetto di tipo “InterfacciaServer” in realtà possederà un riferimento ad un oggetto di tipo Proxy: il Proxy è dello stesso tipo del Server (relazione is-a).
Il client usa il proxy come una versione collocata del server remoto, il proxy si fa carico dell’interazione col server reale.
Con l’aggiunta del Proxy (lato client) il client è “sollevato” dalle problematiche di comunicazione. Il server tuttavia ha ancora l’onere di implementare i necessari meccanismi di comunicazione con il partner remoto (in questo caso, il Proxy lato client).
Una variazione dello schema precedente è quella che vede l’aggiunta di un Proxy lato server, detto Skeleton, che si faccia carico della conduzione della comunicazione con il Proxy lato client.
Lo skeleton avrà la responsabilità di ricevere le richieste di servizio, di strutturare l’informazione fornita in ingresso, fare l’up call al server reale, ricevere da questi eventuali risultati e rispedirli al proxy lato client.
Lo skeleton può essere implementato per ereditarietà: la classe Skeleton implementa solo gli opportuni schemi di comunicazione, ma lascia senza implementazione i metodi dell’interfaccia.
Il ServerReale è una sottoclasse dello skeleton e fornisce implementazione ai metodi astratti.
Lo skeleton può anche essere implementato per composizione: la classe Skeleton presenta al suo interno un riferimento al ServerReale (rif), e i metodi da implementare dell’interfaccia sono così realizzati:
void Servizio1() { rif.Servizio1(); }
Realizzare un programma Client-Server che realizza un contatore remoto e impiega il pattern Proxy-Skeleton.
Stub Mostra codice
Istanzio una socket UDP nel costruttore:public Stub(){
try{
socket = new DatagramSocket();
}catch(IOException e){e.printStackTrace();}}
Nelle funzioni specificate dall'interfaccia, los tub si limita a costruire un pacchetto UDP e ad inviarlo al server:
public void sum(int i) {
byte[] data = ("SUM_"+i).getBytes();
try{
packet = new DatagramPacket(data, data.length,
InetAddress.getLocalHost(), 3000);
socket.send(packet);
}catch(IOException e){e.printStackTrace();}}
Stub (cons.) Mostra codice
Se la funzione ha un valore di ritorno, dopo l'invio del datagramma lo stub si pone inattesa di un messaggio di ritorno: socket.receive(packet);
Il messaggio ritornato viene convertito e memorizzato in una variabile che verrà restituita a conclusione della funzione:
risultato=Integer.valueOf(new String (
packet.getData(),0,
packet.getLength())).intValue();
}catch(IOException e){e.printStackTrace();}
return risultato;}
Skeleton Mostra codice
Decidiamo di realizzare lo skeleton per ereditarietà e quindi questa classe è astratta: public abstract
Il costruttore dello Skeleton istanzia una socket UDP per ricevere le richieste da parte dei client:
try{
socket = new DatagramSocket(3000);
}catch (SocketException socketException){}}
Quando attivo lo Skeleton attende nuovi datagrammi, al cui arrivo attiva un nuovo thread servente:
while(true) {
byte[] data = new byte[65508];
packet = new DatagramPacket(data, data.length);
try{
socket.receive(packet);
}catch(IOException e){e.printStackTrace();}
SkeletonThread corrente = new SkeletonThread(
packet, socket, this);
corrente.start();
Skeleton Mostra codice
Lo skeleton non fornisce alcuna implementazione delle funzioni derivanti dall'interfaccia:
public abstract void sum(int i);
public abstract int get();
public abstract int inc();
ServizioReale Mostra codice
SkeletonThread Mostra codice
SkeletonThread estende Thread così da realizzare una gestione multithread delle richieste in arrivo: public SkeletonThread extends Thread {
Memorizzo all'interno dello SkeletonThread, il datagramma arrivato, la socket e il riferimento al servizio: this.packet = p; this.socket = s; this.ser = server;}
Acquisisco il contenuto del datagramma: String com = new String(packet.getData(),0 ,lunghezza);
Parsing della richiesta per capire quale funzionalità é stata richiesta: if (com.matches("SUM_\\d+")){
Invoco la funzionalità richiesta e offerta del servizio, eventualmente ottenendo i parametri d'ingresso dal contenuto del datagramma:
com = comando.replace("SUM_","");
ser.sum(Integer.parseInt(com));
Se la funzione restituisce un valore di ritorno, costruisco un nuovo datagramma e lo invio al client:
DatagramPacket ris = new DatagramPacket(res, res.length, da, por);
try{ socket.send(ris);
2. La modellazione a oggetti e il linguaggio UML (richiami)
3. Generalità su Java e la programmazione ad oggetti
6. Regole di traduzione da UML a Java/C++
7. Programmazione multi-thread
8. Sincronizzazione tra thread
9. Programmazione client-server con socket TCP/IP (Java networkin...
10. Programmazione di applicazioni client-server: il Pattern Proxy...
12. Design Patterns
13. Pattern architetturali - Esempi
14. Design pattern creazionali. Esempi
15. Design pattern strutturali. Esempi
16. Introduzione alle tecnologie middleware
17. Modelli di middleware: RPC, MOM, TP, TS