Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D La Corte in Rete
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Stefano Russo » 10.Programmazione di applicazioni client-server: il Pattern Proxy-Skeleton


Sommario

  • Il pattern Proxy
  • Il pattern Skeleton
    • Realizzazione per composizione
    • Realizzazione per ereditarietà

Applicazione client/server con socket

In un’applicazione client/server (C/S), basata su socket TCP/IP come mezzo di comunicazione, tipicamente client e server devono implementare meccanismi per:

  1. la connessione;
  2. l’esternalizzazione dei dati da trasmettere;
  3. l’invio/ricezione delle richieste;
  4. la ricostruzione dei valori dei dati ricevuti.

Problemi:

  • molti dettagli di implementazione della comunicazione C/S distraggono dalla realizzazione delle funzionalità dell’applicazione;
  • difficoltà di manutenzione e porting: client e server “mischiano” il codice applicativo a quello per la comunicazione C/S.

Applicazione client/server con socket

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.

Esempio: Contatore Remoto

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.


Esempio: Contatore Remoto

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.

Esempio: Contatore Remoto

Mostra codice

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);

Esempio: Contatore Remoto

Mostra codice

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;

Esempio: Contatore Remoto

Mostra codice

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(); } }

Esempio: Contatore Remoto

Mostra codice

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();

Esempio: Contatore Remoto

Mostra codice

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();...

Pattern “Proxy Remoto”

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:

  • interfaccia del proxy = interfaccia server reale.

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

  • Distanza tra “Concetto” ed Implementazione minimizzata
  • Maggiore portabilità del codice del client: le modifiche sono incapsulate nell’oggetto Proxy

Inconvenienti?

Pattern “Proxy Remoto”


Pattern “Proxy Remoto”

Servizio specificato da un interfaccia (InterfacciaServer).

Duplice diversa implementazione, ServerReale e Proxy

  • il ServerReale implementa effettivamente i servizi pubblicati nell’interfaccia ereditata;
  • il Proxy si fa carico dell’inoltro delle richieste (effettuate dal client) verso il server reale.

L’associazione direzionale tra Proxy e ServerRale, indicata in figura, sintetizza tutti i meccanismi necessari al Proxy per riferirsi al server reale.

Pattern “Proxy Remoto”

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.

Pattern “Proxy Remoto”


Pattern “Skeleton”

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.

Pattern “Skeleton”

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.


Pattern “Skeleton”

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(); }


Esempio Proxy-Skeleton

Realizzare un programma Client-Server che realizza un contatore remoto e impiega il pattern Proxy-Skeleton.


Esempio Proxy-Skeleton

Client Mostra codice

Server Mostra codice

Esempio 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();}}

Esempio Proxy-Skeleton

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;}

Esempio Proxy-Skeleton

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();

Esempio Proxy-Skeleton

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();

Esempio Proxy-Skeleton

ServizioReale Mostra codice

Esempio Proxy-Skeleton

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);

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion

Fatal error: Call to undefined function federicaDebug() in /usr/local/apache/htdocs/html/footer.php on line 93