Indice
LPRb 2007--2008: Esercizi assegnati a lezione
Test di ingresso
- Si realizzi un programma Java che si comporta come il comando Unix cat (il programma deve copiare quanto letto dallo standard input sullo standard output) da completare in 10 minuti
- Si realizzi un programma che stampa i metodi statici di una classe. Il nome della classe è passato come parametro della riga di comando da completare in 15 minuti
- Si realizzi un programma nel quale due thread si alternano in esecuzione: il primo thread attende un numero di secondi random, compreso fra 0 e 10, poi si sospende, mandando in esecuzione il secondo thread. Il secondo thread attende un numero di secondi random, sempre compreso fra 0 e 10, quindi si sospende riattivando il primo thread, e così via. Durante il periodo di attesa, i thread stampano un messaggio con il proprio identificatore ogni secondo. da completare in 20 minuti
Soluzione
- comando Cat
- metodi statici di una classe
Pool di Thread
Si realizzi un programma in grado di eseguire calcoli generici utilizzando un pool di thread. I calcoli da eseguire sono quelli modellati dal metodo compute definito dall'interfaccia
public interface Compute<T,S> { public S compute(T x); }
i valori di tipo *T* da calcolare si devono prelevare da un Repository<T> nel quale vengono inseriti da un Generatore e i risultati ottenuti devono essere riposti in una secondo Repository<S> dal quale saranno prelevati da uno Stampatore<S>. Assumiamo che i dati da calcolare siano Integer e che i risultati del calcolo siano Integer. Il numero di thread Calcolatore da utilizzare per eseguire i calcoli deve essere un parametro della riga di comando.
La classe Repository<T> deve mettere a disposizione due metodi
... modificatori opportuni ... void insert(T x) // inserisce un oggetto di tipo T nel repository ... modificatori opportuni ... T extract() // preleva il più vecchio elemento del repository
Per l'implementazione del repository di deve usare una struttura dati fra quelle messe a disposizione dall'ambiente di programmazione Java. Si considerino per esempio quelle definite in java.util. La richiesta di extract da un Repository vuoto deve provocare il blocco del thread che l'ha invocata fino a quando non sia effettivamente disponibile un dato da estrarre. Per il generatore, si può usare la classe:
public class Generatore extends Thread { Repository<Integer> rep = null; int n = 10; final int DELAY = 1000; final float RANGE = (float) 1024.00; /** * costruttore * @param n numero di oggetti da generare * @param rep repository nel quale depositare gli oggetti generati */ public Generatore(int n, Repository<Integer> rep) { this.rep = rep; this.n = n; } /** * corpo del thread generatore: ciclo finito che genera interi compresi fra 0 e RANGE */ public void run() { for(int i=0; i<n; i++) { try { rep.insert(new Integer((int) (Math.random() * RANGE))); sleep(DELAY); } catch(InterruptedException e) { System.out.println("Interrupted"); } } } }
Per lo stampatore invece, si consideri il codice
public class Stampatore<T> extends Thread { Repository<T> rep; public Stampatore(Repository<T> rep) { this.rep = rep; } public void run() { while(true) { T i = rep.extract(); System.out.println("Estratto "+i.toString()); } } }
Durante l'esecuzione del programma, devono essere attivi ad ogni istante: 1 thread generatore, 1 thread stampatore e N thread calcolatori. Il thread generatore deve terminare quando ha terminato la generazione degli n valori richiesti. Si può realizzare il programma senza necessariamente trattare la terminazione. Una volta realizzato il programma, si rimuova il ritardo nel ciclo di produzione dei valori iniziali da parte del Generatore e si inserisca, nel thread Calcolatore un ritardo pseudo casuale fra 0 e 5 secondi, utilizzando il codice:
try { int random = ((int)(Math.random() * 5000.00)) System.out.println("Thread "+this.getName()+" inizio calcolo"); sleep(random); ... calcolo vero e proprio ... System.out.println("Thread "+this.getName()+" fine calcolo"); } catch(...) {...}
Si controlli quindi che i meccanismi di sincronizzazione fra thread funzionano ancora e che effettivamente si siano più thread che calcolano contemporaneamente.
Soluzione
Senza terminazione
- Codice per l'interfaccia Compute e per la classe FunzioneSemplice, che implementa l'interfaccia
- Codice per la classe ThreadComputer che implementa il singolo thread del ThreadPool (Calcolatore, nella figura)
- Codice del Repository
Soluzione con terminazione
- Codice per il repository: è stato introdotto un flag che dice se ci saranno altre inserzioni nel repository. Questo flag viene cambiato dal generatore prima di terminare.
- Codice del thread calcolatore e codice del main. Il thread calcolatore termina se gli arriva un'eccezione anzichè un valore estratto. Il main lancia i thread calcolatori, poi ne attende la terminazione e a quel punto manda un'interruzione anche al thread stampatore, l'ultimo rimasto attivo, bloccato su una extract dal repository dei risultati, che non sarà mai più utilizzato.
- Codice dello stampatore che termina. Si rileva l'eccezione InterruptedException conseguente all'interrupt lanciato dal main.
Soluzione che usa java.util.concurrent
Prima versione: utilizza un thread pool di java.util.concurrent ma usa ancora i Repository come precedentemete definiti:
- Codice con il main di prova
- Codice per il calcolo del singolo task
- tutto il resto del codice è uguale a quello delle soluzioni precedenti
Seconda versione: utilizza una LinkedBlockingQueue per realizzare i Repository:
- Codice per il main
- Codice per lo Stampatore (cambia il metodo chiamato per l'estrazione)
- Codice per il task (cambia il costruttore)
- tutto il resto del codice è identico
Server di upload file
Utilizzando socket TCP/IP, si realizzi un server che mette a disposizione un servizio di file upload. Il client che intende fruire del servizio si connette ed invia:
- il nome del file di cui si vuole fare l'upload
- un ritorno carrello (è la stringa “\n”)
- l'intero file
Il server, a seguito dell'arrivo di una richiesta di upload, memorizza il file inviato nella directory destinazione. Il nome della directoy destinazione va passato come argomento della riga di comando. Si relizzi anche un Client in grado di inviare un file il cui nome è passato come parametro dalla riga di comando.
Dunque sulla macchina fujih3 potremmo lanciare il comando
java serverUpload.Server /tmp
(primo ed unico parametro è la directory di upload) mentre sulla macchina fujih5 potremmo lanciare il comando
java serverUpload.Client fujih3 prova.txt
(il primo parametro è il nome dell'host che ospita il server di upload, il secondo è il nome del file di cui si richiede l'upload) e come risultato dovremmo avere che fuijh3:/tmp/prova.txt è copia identica del file prova.txt che si trova nella directory corrente per il Client su fujih5.
Si realizzi il Server prima come server single thread e successivamente come server multithreaded. In entrambi i casi, il Server deve essere in grado di eseguire l'upload di un numero arbitrario di file.
Soluzione proposta
- Server che utilizza un BufferedReader per la comunicazione sul socket (il client è lo stesso).
Instant messanger
Si realizzi un programma che implementa un instant messanger punto a punto. Lanciando il programma su due macchine diverse e passando il nome dell'altra macchina come parametro della riga di comando, si ottiene il “collegamento” fra la due macchine: ciò che viene scritto alla tastiera di una macchina deve comparire sul video dell'altra e viceversa. Per la realizzazione del programma, ognuno usi come numero di porta 1NNNN dove NNNN sono le ultime 4 cifre del proprio numero di matricola.
I due programmi lanciati sulle due macchine devono essere identici.
Qualora si tenti un contatto su una macchina dove ancora non è stata avviato il programma per l'instant messager sulla porta 1NNNN, ci si deve sospendere per 5 secondi e successivamente riprovare, per 5 volte. Se il contatto non ha successo nemmeno la quarta volta, il programma deve terminare con un messaggio di errore.
Soluzione proposta
- Codice per il Messanger
- Codice per il CopyThread
NsLookup con cache
Si realizzi un server multithreaded che accetta richieste di risoluzione di nomi di host. La risoluzione dei nomi deve prevedere l'utilizzo di una cache interna al server che mantiene tutti i risultati (<nomeserver,indirizzoIP>) delle query precedenti. Il server deve implementare un pool di thread di dimensione fissata e per il multithreading deve utilizzare le classi della java.util.concurrent. Le richieste avvengono utilizzando un ObjectStream e sono contenute in un oggetto che ha un campo String e un campo boolean. Il campo String contiene il nome dell'host da risolvere, il campo boolean dice (se true) che si accetta la risoluzione del nome dell'host effettuata mediante accesso alla cache interna del server o (se false) che non si vuole considerare la risposta eventualmente presente nella cache del server. Se non presente in cache, o se il flag nella richiesta vale false, il server effettua il lookup del nome del server utilizzando una semplice InetAddress.getByName. Il client deve stampare un messaggio con il numero di millisecondi necessari ad ottenere la risposta. A tale scopo si utilizzi il metodo statico currentTimeMillis() della classe System che restituisce il “tempo di sistema” in millisecondi, in modo da ottere il valore del tempo di sistema prima dell'invio della richiesta al server e immediatamente dopo la ricezione della risposta. La differenza fra i due tempi dà il tempo in millisecondi speso dal server per evadere la richiesta.
Soluzione proposta
- File NameQuery.java
- File NameServer.java
- File NsLookup.java
- File QueryAnswer.java
- File ServerThread.java
Trasferimento file (TFTP con UDP)
Si vuole realizzare una coppia di programmi che permettano di trasferire l’intero contenuto di un file da una macchina all’altra, utilizzando un protocollo tipo TFTP implementatao su datagram socket. Il protocollo TFTP prevede la trasmissione dell’intero file mediante la spedizione di un certo numero di pacchetti, ognuno dei quali contiene una parte del file. La spedizione di un messaggio (tranne che nel caso del primo messaggio) può avvenire solo dopo la ricezione di un ACK da parte del destinatario. In caso non venga ricevuto l’ACK entro un certo timeout (per esempio entro due secondi) il mittente deve provvedere alla ritrasmissione del pacchetto. Analogamente, il destinatario che non veda arrivare un pacchetto entro un certo timeout deve provvedere alla rispedizione dell’ultimo ACK, nell’eventualità che si sia perso.
Si dovranno quindi realizzare due programmi: un Sender che prende come parametri il file da trasmettere, l’host destinazione e la porta destinazione e trasmette il file, e un Receiver che prende come parametri il nome del file da scrivere e la porta da utilizzare e riceve il file salvandolo su disco. Per verificare che i due programmi funzionino correttamente, lanciare una shell remota su una macchina diversa da quella su cui state lavorando, poi lanciate il Sender su una macchina e il Receiver sull’altra macchina, date due nomi di file diversi e alla fine eseguite un comando diff filename1 filename2. Se ritorna il prompt della shell senza messaggi vuol dire che i file sono uguali, altrimenti c’e’ stato un errore. Provate a trasferire sia file di tipo testo che file di tipo binario (per esempio, file .class)).
Si supponga che il pacchetto da trasmettere sia modellato dalla classe TFTmessage e che questi oggetti siano serializzati innetro ai DatagramPacket utilizzando la classe ODP già vista a lezione quando abbiamo introdotto la serializzazione degli oggetti. La classe TFTPmessage prevede sia un numero di sequenza che una marca booleana che dice se devono essere spediti ancora pacchetti del file oppure se la spedizione è terminata. Tali campi vanno utilizzati per gestire le ritrasmissioni in modo corretto, sia dal lato Sender (ritrasmissione di pacchetti con dati relativi al file) sia lato Receiver (ritrasmissione dei pacchetti di tipo ACK). Utilizzando ODP, si può spedire un oggetto in un DatagramPacket creando un ODP col costruttore che prende come parametro l’oggetto da spedire e successivamente invocando sull’oggetto creato un metodo getDatagramPacket. Per ricevere un oggetto serializzato in un DatagramPacket, si usa il metodo statico getODP per ottenere un ODP dal DatagramPacket e successivamnete una getObject sull’oggetto ODP restituito dalla getODP per ottenre l’oggetto deserializzato.
Soluzione proposta
- File ODP.java
- File Sender.java
- File Receiver.java
- File TFTPmessage.java
Server NFS
Si realizzi un semplice server NFS, utilizzando esclusivamente il protocollo UDP. Il server deve accettare richieste di apertura, lettura, scrittura e chiusura di file. L'operazione richiesta deve avere effetto locale e al client remoto si deve restituire:
- un handle (che il server dovra' cachare) per le successive operazioni (di tipo String, per esempio) in caso di open
- un byte[] contenente l'array di byte letti, nel caso di read
- un errore o un codice di successo nel caso di close e di write.
In particolare, suggeriamo di utilizzare messaggi (nel payload dei pacchetti UDP) tipo:
- OPEN \n filename \n per la richiesta di apertura di un file
- READ \n handle \n nbytedaleggere \n per la richiesta di lettura di un file con handler dato
- CLOSE \n handle \n per la richiesta di chiusura di un file con handler dato
- WRITE \n handle \n seguito da un byte[] opportunamente codificato contenente i byte da scrivere per la richiesta di scrittura di un file con handler dato
- ERROR \n argomenti dell'errore separati da \n e conclusi da \n per gli errori
- OK \n per i messaggi di successo
- HANDLE \n handle \n per le risposte ai messaggi di apertura avvenuta con successo.
Vista la complessità del problema, si consiglia di strutturare la soluzione come segue:
- classe RemoteInputStream, con metodi boolean open(String filename), boolean close() e byte[] read(int len)
- classe RemoteOutputStream, con metodi boolean open(String filename), boolean close() e boolean write(byte[])
- classe UDPServerComm, con metodo DatagramPacket call (DatagramPacket dp) che si prende carico di trasmettere una richiesta e ricevere una risposta dal server, incapsulando tutti i dettagli legati all'utilizzo di socket non affidabili come i DatagramSocket, il setting del timeout e l'eventuale ritrasmissione
- class NfsServer, che implementa il server remoto.
Le prime due classi dovrebbero permettere di scrivere un cliente tipo:
public static void main(String[] args) { RemoteInputStream ris = new RemoteInputStream("fujih1"); ris.open("prova1.txt"); byte [] buffer = ris.read(100); ris.close(); System.out.println("letti "+buffer.length+" bytes:"+new String (buffer)); }
per leggere parte del file che si trova su fujih1 se sul fujih1 sta girando un NfsServer.
Soluzione
- classe NfsServer.java
- classe RemoteStream.java
- classe RemoteInputStream.java
- classe RemoteOutputStream.java
- classe StreamRef.java
- classe RemotePacket.java
- classe UDPServerComm.java
Chat con multicast
Si realizzi un client chat che permetta la comunicazione di semplici messaggi di testo fra più utenti. Il programma deve fare uso di multicast UDP. La chat avviene utilizzando un indirizzo di multicast passato come parametro della riga di comando. I messaggi inviati alla chat dal singolo utente vengono semplicemente inviati al gruppo di multicast. Tutti i messaggi diretti all'indirizzo di multicast vengono copiati sullo standard output.
Successivamente si modifichi il programma in modo da utilizzare broadcast invece che multicast per l'implementazione delle stesse funzionalità.
Si faccia in modo che i messaggi stampati a video comincino sempre con l'indicazione dell'indirizzo della macchina (o meglio il nome) da cui si è ricevuto il messaggio. Per esempio, a terminale dovremmo vedere una cosa tipo:
fujih4:~> java chatMulticast/Chat 225.67.1.91 23232 ciao From /131.114.11.154: ciao Come va From /131.114.11.154: Come va From /131.114.11.151: hola
Soluzione proposta
Test Datagram
Questo non è un esercizio vero e proprio. E' la realizzazione di una classe che sottoclassa DatagramSocket e implementa un metodo send che “perde” (ovvero non trasmette) una percentuale prefissata di pacchetti. Utilizzando questa classe invece della DatagramSocket standard si può facilmente verificare il funzionamento di un software che utilizza protocolli basati su UDP.
- codice della classe DatagramPacket
ContoCorrente RMI
Si realizzi un oggetto ContoCorrente e lo si renda accessibile da remoto utilizzando la tecnologia RMI, che permetta di effettuare operazioni di
long saldo() // restitiusce il saldo long prelievo(long cifra) // preleva la cifra e restituisce il saldo long deposito(long cifra) // deposita la cifra e restituisce il saldo String ultimiMovimenti(int n) // restituisce gli ultimi n movimenti, uno per riga
Si realizzino:
- un'interfaccia con la signature dell'oggetto remoto
- l'oggetto da pubblicare con RMI
- un main che crea il registry e pubblica l'oggetto
- un cliente che accetta come parametri dalla riga di comando il nome dell'host su cui si trova l'oggetto, il nome dell'oggetto, un codice operazione e eventuali parametri dell' operazione ed effettui l'operazione richiesta.
Si faccia in modo che ciascuna delle operazioni dell'oggetto conto corrente prenda un certo tempo (per esempio 20 secondi) e si controlli, lanciando piu' clienti contemporaneamente che le operazioni avvengano effettivamente in mutua esclusione e rispettando la semantica propria di un contocorrente.
Soluzione proposta
- Codice dell'interfaccia CC remoto
- Codice del CC remoto
- Codice del main che pubblica l'oggetto remoto
- Codice del cliente
Asta RMI
Si realizzi un server che implementa il battitore di un'asta. Il server deve permettere la registrazione di un cliente e l'esecuzione di un'offerta. La registrazione del cliente comporta la registrazione di una callback che il battitore usa per comunicare una nuova offerta al cliente. Il cliente esegue un'offerta iniziale (per esempio prendendo il valore dell'offerta dalla riga di comando). Successivamente si mette in attesa. La callback scrive a schermo le offerte, man mano che queste avvengono. L'utente che decide di fare una nuova offerta la batte a terminale e di conseguenza il cliente la invia al battitore.
In particolare, si consideri che la callback deve essere rappresentata da un oggetto che implementa l'interfaccia:
public interface ClientInterface extends Remote { public void offertaAvvenuta(int howMuch) throws RemoteException; }
e l'oggetto battitore deve implementare l'interfaccia:
public interface InterfacciaBattitore extends Remote { /** * used to make an offer. * @param name the name of who makes the offer * @param howMuch the amount of the offer * @return the current amount offered, 0 if the offer made is the currently accepted one * @throws RemoteException */ public int offer(String name, int howMuch) throws RemoteException; /** * this is used to register the callback; * @param name the name of who makes the offer * @param cif */ public void register(String name, ClientInterface cif) throws RemoteException; }
Purchè il meccanismo delle callback venga implementato correttamente, si può trascurare di trattare la terminazione dell'asta.
Il dettaglio del comportamento di client e server, nonchè del metodo che sull'oggetto remoto registra una nuova offerta, è dato dai flow chart che seguono.
Soluzione proposta
- Interfaccia per l'oggetto callback
- Interfaccia del battitore
- Codice del cliente (fa offerte random iniziali finchè una non risulta vincente, poi aspetta notifiche dal battitore (tramite la callback) e nuove offerte da effettuare dal terminale)
- Codice del battitore
- Codice del main che pubblica il battitore