3) Utilizzo di Java per lo sviluppo di applicazioni di rete |
In questa parte del corso si affronta il problema
dello sviluppo di applicazioni di rete di tipo client-server.
Tale architettura software è praticamente una scelta obbligata
al giorno d'oggi, in quanto si adatta perfettamente alle attuali
reti di elaboratori (costituite da molteplici host indipendenti
e non più da un singolo mainframe al quale sono connessi
vari terminali).
In particolare, si vedrà:
Il linguaggio che verrà usato è Java, in quanto dotato di molti vantaggi rispetto a possibili concorrenti:
Socket
e ServerSocket
);
Exception
);
Thread
).
Per le sue caratteristiche, questo linguaggio permette di ottenere
con facilità le funzioni base richieste a un applicazione
di rete, e di concentrarsi sugli aspetti più caratterizzanti
dell'applicazione stessa.
Per poter affrontare lo sviluppo di applicazioni di rete, è necessario approfondire la conoscenza del linguaggio nei seguenti settori:
3.1) Ripasso di AWT |
Vediamo ora un'applicazione
(cioè un programma che può girare da solo, appoggiandosi
a una macchina virtuale, senza bisogno di una pagina HTML che
lo richiama).
Le applicazioni non hanno le limitazioni degli applet, che tipicamente:
Un'applicazione è una classe al cui interno esiste un metodo
main(String args[])
che viene eseguito all'avvio.
Esempio 1 |
Questa semplicissima applicazione presenta alcuni campi testo
e alcuni bottoni, ed è predisposta per la gestione degli
eventi.
Nel nostro caso, il main()
crea una nuova istanza
di un oggetto della classe BaseAppE1
, che estende
Frame
(in pratica una finestra).
import java.awt.*; public class BaseAppE1 extends Frame { Label label1, label2, label3, label4; TextField textField1, textField2; TextArea textArea1, textArea2; Button button1, button2, button3, button4, button5, button6; //-------------------------------------------------- public BaseAppE1() { this.setLayout(null); label1 = new Label("Label1:"); label1.reshape(110, 5, 40, 15); this.add(label1); textField1 = new TextField(); textField1.reshape(150, 10, 200, 15); this.add(textField1); label2 = new Label("Label2:"); label2.reshape(370, 5, 40, 15); this.add(label2); textField2 = new TextField(); textField2.reshape(410, 10, 30, 15); this.add(textField2); label3 = new Label("Label3:"); label3.reshape(20, 25, 100, 15); this.add(label3); textArea1 = new TextArea(); textArea1.reshape(20, 40, 560, 50); this.add(textArea1); label4 = new Label("Label4:"); label4.reshape(20, 95, 100, 15); this.add(label4); textArea2 = new TextArea(); textArea2.reshape(20, 110, 560, 300); this.add(textArea2); button1 = new Button("Button1"); button1.reshape(70, 430, 60, 20); this.add(button1); button2 = new Button("Button2"); button2.reshape(150, 430, 60, 20); this.add(button2); button3 = new Button("Button3"); button3.reshape(230, 430, 60, 20); this.add(button3); button4 = new Button("Button4"); button4.reshape(310, 430, 60, 20); this.add(button4); button5 = new Button("Button5"); button5.reshape(390, 430, 60, 20); this.add(button5); button6 = new Button("Button6"); button6.reshape(470, 430, 60, 20); this.add(button6); resize(600, 460); show(); } //-------------------------------------------------- public static void main(String args[]) { new BaseAppE1(); } //-------------------------------------------------- public boolean handleEvent(Event event) { if (event.id == Event.WINDOW_DESTROY) { hide(); // hide the Frame dispose(); // tell windowing system to free resources System.exit(0); // exit return true; } if (event.target == button1 && event.id == Event.ACTION_EVENT) { button1_Clicked(event); } if (event.target == button2 && event.id == Event.ACTION_EVENT) { button2_Clicked(event); } if (event.target == button3 && event.id == Event.ACTION_EVENT) { button3_Clicked(event); } if (event.target == button4 && event.id == Event.ACTION_EVENT) { button4_Clicked(event); } if (event.target == button5 && event.id == Event.ACTION_EVENT) { button5_Clicked(event); } if (event.target == button6 && event.id == Event.ACTION_EVENT) { button6_Clicked(event); } return super.handleEvent(event); } //-------------------------------------------------- void button1_Clicked(Event event) { textArea2.setText(textArea2.getText() + "Hai premuto bottone 1\n"); } //-------------------------------------------------- void button2_Clicked(Event event) { textArea2.setText(textArea2.getText() + "Hai premuto bottone 2\n"); } //-------------------------------------------------- void button3_Clicked(Event event) { textArea2.setText(textArea2.getText() + "Hai premuto bottone 3\n"); } //-------------------------------------------------- void button4_Clicked(Event event) { textArea2.setText(textArea2.getText() + "Hai premuto bottone 4\n"); } //-------------------------------------------------- void button5_Clicked(Event event) { textArea2.setText(textArea2.getText() + "Hai premuto bottone 5\n"); } //-------------------------------------------------- void button6_Clicked(Event event) { textArea2.setText(textArea2.getText() + "Hai premuto bottone 6\n"); } } |
3.2) Input/Output in Java |
L'I/O in Java è definito in termini
di stream (flussi).
Gli stream sono un'astrazione di alto livello per rappresentare
la connessione a un canale di comunicazione.
Il canale di comunicazione può essere costituito fra entità molto diverse, le più importanti delle quali sono:
Grazie all'astrazione rappresentata dagli stream, le operazioni
di I/O dirette a (o provenienti da) uno qualunque degli oggetti
di cui sopra sono realizzate con la stessa interfaccia.
Uno stream rappresenta un punto terminale
di un canale di comunicazione unidirezionale, e può leggere
dal canale (InputStream
) o scrivervi (OutputStream
):
Tutto ciò che viene scritto sul canale tramite l'OutputStream
viene letto dall'altra parte dal corrispondente InputStream
.
Gli stream hanno diverse proprietà:
OutputStream
viene letto
nello stesso ordine dal corrispondente InputStream
;
RandomAccessFile
, che però non è
uno stream, offre tale tipo di accesso);
InputStream
) o scrivere
(OutputStream
) ma non entrambe le cose. Se ambedue
le funzioni sono richieste, ci vogliono 2 distinti stream: questo
è un caso tipico delle connessioni di rete, tant'è
che da una connessione (Socket
) si ottengono due
stream, uno in scrittura e uno in lettura;
3.2.1) Classe InputStream |
E' la classe astratta dalla quale derivano
tutti gli stream finalizzati alla lettura da un canale di comunicazione.
Costruttore
public InputStream();
Vuoto.
Metodi più importanti
public abstract int read() throws IOException;
Legge e restituisce un byte (range 0-255) o si blocca se non c'è niente da leggere. Restituisce il valore -1 se incontra la fine dello stream.
public int read(byte[] buf) throws IOException;
Legge fino a riempire l'arraybuf
, o si blocca se non ci sono abbastanza dati. Restituisce il numero di byte letti, o il valore -1 se incontra la fine dello stream. L'implementazione di default chiama tante volte laread()
, che è astratta.
public int read(byte[] buf, int off, int len) throws IOException;
Legge fino a riempire l'arraybuf
, a partire dalla posizioneoff
, conlen
byte (o fino alla fine dell'array). Si blocca se non ci sono abbastanza dati. Restituisce il numero di byte letti, o il valore -1 se incontra la fine dello stream. L'implementazione di default chiama tante volte laread()
, che è astratta.
public int available() throws IOException;
Restituisce il numero di byte che possono essere letti senza bloccarsi (cioè che sono disponibili):
public void close() throws IOException;
Chiude lo stream e rilascia le risorse ad esso associate. Eventuali dati non ancora letti vengono perduti.
Questi metodi, e anche gli altri non elencati, sono supportati
anche dalle classi derivate, e quindi sono disponibili sia che
si legga da file, da un buffer di memoria o da una connessione
di rete.
3.2.2) Classe OutputStream |
E' la classe astratta dalla quale derivano
tutti gli Stream finalizzati alla scrittura su un canale di comunicazione.
Costruttore
public OutputStream();
Vuoto.
Metodi più importanti
public abstract void write(int b) throws IOException;
Scrive un byte (8 bit) sul canale di comunicazione. Il parametro èint
(32 bit) per convenienza (il risultato di espressioni su byte è intero), ma solo gli 8 bit meno significativi sono scritti.
public void write(byte[] buf) throws IOException;
Scrive un array di byte. Blocca il chiamante finché la scrittura non è completata..
public void write(byte[] buf, int off, int len) throws IOException;
Scrive la parte di array che inizia a posizioneoff
, e consiste dilen
byte. Blocca il chiamante finché la scrittura non è completata..
public void flush() throws IOException;
Scarica lo stream, scrivendo effettivamente tutti i dati eventualmente mantenuti in un buffer locale per ragioni di efficienza. Ciò deriva dal fatto che in generale si introduce molto overhead scrivendo pochi dati alla volta sul canale di comunicazione:
public void close() throws IOException;
Chiamaflush()
e poi chiude lo stream, liberando le risorse associate. Eventuali dati scritti prima dellaclose()
vengono comunque trasmessi, grazie alla chiamata diflush()
.
Esempio 2 |
Applicazione che copia ciò che viene immesso tramite lo
standard input sullo
standard output
.
A tal fine si usano due stream accessibili sotto forma di field
(cioè variabili) nella classe
statica (e quindi sempre disponibile, anche senza
instanziarla) System
. Essi sono System.in
e System.out
.
import java.io.*; public class InToOut { //-------------------------------------------------- public static void main(String args[]) { int charRead; try { while ((charRead = System.in.read()) != -1) { System.out.write(charRead); System.out.println("");//Ignorare questa istruzione (ma non toglierla) } } catch (IOException e) { //non facciamo nulla } } } |
Il programma si ferma immettendo un particolare carattere (End Of File, EOF ) che segnala fine del file:
3.2.3) Estensione della funzionalità degli Stream |
Lavorare con singoli byte non è molto
comodo. Per fornire funzionalità di I/O di livello più
alto, in Java si usano estensioni degli stream dette stream
di filtro. Uno stream di filtro (che esiste sia
per l'input che per l'output) è una estensione del corrispondente
stream di base, si attacca ad uno stream esistente e fornisce
ulteriori funzionalità.
Aspetti importanti degli stream di filtro:
InputStream
e OutputStream
,
e quindi supportano tutti i loro metodi;
InputStream
o OutputStream
al quale passano di norma i metodi della superclasse: chiamare
write()
su uno stream di filtro significa chiamare
write()
sull'OutputStream
attaccato;
Il vantaggio principale di questa strategia, rispetto a estendere
direttamente le funzionalità di uno stream di base, è
che tali estensioni possono essere applicate a tutti i tipi di
stream, in quanto sono sviluppate indipendentemente da essi.
3.2.3.1) Classe FilterInputStream |
Estende InputStream
ed è
a sua volta la classe da cui derivano tutti gli stream di filtro
per l'input. Si attacca a un InputStream
.
Costruttore
protected FilterInputStream(InputStream in);
Crea uno stream di filtro attaccato ain
.
Variabili
protected InputStream in;
Reference all'InputStream
a cui è attaccato.
Metodi più importanti
Gli stessi diInputStream
. Di fatto l'implementazione di default non fa altro che chiamare i corrispondenti metodi dell'InputStream
attaccato.
3.2.3.2) Classe FilterOutputStream |
Estende OutputStream
ed è
a sua volta la classe da cui derivano tutti gli stream di filtro
per l'output. Si attacca a un OutputStream
.
Costruttore
protected FilterOutputStream(OutputStream out);
Crea uno stream di filtro attaccato aout
.
Variabili
protected OutputStream out;
Reference all'OutputStream
a cui è attaccato.
Metodi più importanti
Gli stessi diOutputStream
. Di fatto l'implementazione di default non fa altro che chiamare i corrispondenti metodi dell'OutputStream
attaccato.
Vari stream di filtro predefiniti sono disponibili in Java. I
più utili sono descritti nel seguito.
3.2.3.3) BufferedInputStream e BufferedOutputStream |
Forniscono, al loro interno, meccanismi di
buffering per rendere più efficienti le operazioni di I/O.
Essi diminuiscono il numero di chiamate al sistema operativo (ad
esempio nel caso di accessi a file) o di TPDU spediti (nel caso
delle connessioni di rete).
Costruttori
public BufferedInputStream(InputStream in);
Crea unBufferedInputStream
attaccato ain
(con un buffer interno di 512 byte).
public BufferedInputStream(InputStream in, int size);
Secondo costruttore in cui si specifica la dimensione del buffer interno.
public BufferedOutputStream (OutputStream out);
Crea unBufferedOutputStream
attaccato aout
(con un buffer interno di 512 byte).
public BufferedOutputStream (OutputStream out, int size);
Secondo costruttore in cui si specifica la dimensione del buffer interno.
Metodi più importanti
Gli stessi degli stream da cui derivano. In più,BufferedOutputStream
implementa il metodoflush()
.
3.2.3.4) DataInputStream e DataOutputStream |
Forniscono metodi per leggere e scrivere dati
a un livello di astrazione più elevato (ad esempio interi,
reali, stringhe, booleani, ecc.) su un canale orientato al byte.
I valori numerici vengono scritti in network byte order (il byte più significativo viene scritto per primo), uno standard universalmente accettato. In C, ciò si ottiene mediante l'uso di apposite funzioni:
htons()
: host to network short;
htonl()
: host to network long;
ntohs()
: network to host short;
ntohl()
: network to host long.
DataInputStream
e DataOutputStream
Costruttori
public DataInputStream(InputStream in);
Crea unDataInputStream
attaccato ain
.
public DataOutputStream(OutputStream out);
Crea unDataOutputStream
attaccato aout
.
Metodi più importanti
public writeBoolean(boolean v) throws IOException;
public boolean readBoolean() throws IOException;
public writeByte(int v) throws IOException;
public byte readByte()throws IOException;
public writeShort(int v) throws IOException;
public short readShort()throws IOException;
public writeInt(int v) throws IOException;
public int readInt() throws IOException;
public writeLong(long v) throws IOException;
public long readLong()throws IOException;
public writeFloat(float v) throws IOException;
public float readFloat() throws IOException;
public writeDouble(double v) throws IOException;
public double readDouble() throws IOException;
public writeChar(int v) throws IOException;
public char readChar() throws IOException;
Letture e scritture dei tipi di dati primitivi.
public writeChars(String s) throws IOException;
Scrittura di una stringa.
public String readLine() throws IOException;
Lettura di una linea di testo, terminata con CR, LF oppure CRLF.
3.2.3.5) PrintStream |
Fornisce metodi per scrivere, come sequenza
di caratteri ASCII, una rappresentazione di tutti i tipi primitivi
e degli oggetti che implementano il metodo toString()
,
che viene in tal caso usato.
Costruttori
public PrintStream(OutputStream out);
Crea unPrintStream
attaccato aout
.
public PrintStream(OutputStream out, boolean autoflush);
Seautoflush
è impostato atrue
, lo stream effettua unflush()
ogni volta che incontra la fine di una linea di testo (CR, LF oppure CR+LF).
Metodi più importanti
public void print(...) throws IOException;
Il parametro può essere ogni tipo elementare, un array di caratteri, una stringa, un Object.
public void println(...) throws IOException;
Il parametro può essere ogni tipo elementare, un array di caratteri, una stringa, un Object.
println()
dopo la scrittura va a linea nuova (appende CR, LF oppure CR+LF).
PrintStream
Esempi di uso di FilterStream
3.2.3.6) Esempi di uso degli stream di filtro |
Vediamo ora alcuni usi tipici degli stream
di filtro. In generale ci sono due modi di crearli, a seconda
che si vogliano mantenere o no dei riferimenti agli stream a cui
vengono attaccati.
Efficienza
Si usano gli stream BufferedInputStream
e BufferedOutputStream
,
che offrono la funzione di buffering. Supponiamo di lavorare con
un FileOutputStream
(che vedremo nel seguito).
La prima possibilità è di mantenere in una apposita
variabile anche il riferimento allo stream cui si attacca il filtro
(cioè al FileOutputStream
):
FileOutputStream fileStream; BufferedOutputStream bufferedStream; fileStream = new FileOutputStream("prova.txt"); bufferedStream = new BufferedOutputStream(fileStream);
La seconda possibilità è creare il FileOutputStream
direttamente dentro il costruttore dello stream di filtro :
BufferedOutputStream bufferedStream;
bufferedStream = new BufferedOutputStream(new FileOutputStream("prova.txt"));
Si noti che in questo caso non vi è modo, nel resto del
codice, di riferirsi esplicitamente al FileOutputStream
.
Lettura e scrittura ASCII
Si usano DataInputStream
(chiamando ad esempio readLine()
)
e PrintStream
(chiamando ad esempio println(...)
).
DataInputStream asciiIn; PrintStream asciiOut; asciiIn = new DataInputStream(new FileInputStream("in.txt")); asciiOut = new PrintStream(new FileOutputStream("out.txt"));
Efficienza e scrittura caratteri ASCII
Usiamo un PrintStream
attaccato a un BufferedOutputStream
attaccato a sua volta a un FileOutputStream
.
PrintStream asciiOut; asciiOut = new PrintStream(new BufferedOutputStream( new FileOutputStream("out.txt")));
Analogo discorso per la lettura bufferizzata:
DataInputStream asciiIn; asciiIn = new DataInputStream(new BufferedInputStream( new FileInputStream("in.txt")));