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à:


Figura 3-1: Formato di un messaggio

Il linguaggio che verrà usato è Java, in quanto dotato di molti vantaggi rispetto a possibili concorrenti:

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.


Figura 3-2: Interfaccia utente dell'esempio 1

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


Figura 3-3: Stream di input e output

Tutto ciò che viene scritto sul canale tramite l'OutputStream viene letto dall'altra parte dal corrispondente InputStream.

Gli stream hanno diverse proprietà:


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'array buf, 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 la read(), che è astratta.

public int read(byte[] buf, int off, int len) throws IOException;

Legge fino a riempire l'array buf, a partire dalla posizione off, con len 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 la read(), 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 posizione off, e consiste di len 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;

Chiama flush() e poi chiude lo stream, liberando le risorse associate. Eventuali dati scritti prima della close() vengono comunque trasmessi, grazie alla chiamata di flush().

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à.


Figura 3-4: Stream di filtro

Aspetti importanti degli stream di filtro:


Figura 3-5: Concatenazione di stream di filtro

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 a in.

Variabili

protected InputStream in;

Reference all'InputStream a cui è attaccato.

Metodi più importanti

Gli stessi di InputStream. 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 a out.

Variabili

protected OutputStream out;

Reference all'OutputStream a cui è attaccato.

Metodi più importanti

Gli stessi di OutputStream. 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.


Figura 3-6: Stream di filtro per il buffering

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 un BufferedInputStream attaccato a in (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 un BufferedOutputStream attaccato a out (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 metodo flush().

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:


Figura 3-7: Uso di DataInputStream e DataOutputStream

Costruttori

public DataInputStream(InputStream in);

Crea un DataInputStream attaccato a in.

public DataOutputStream(OutputStream out);

Crea un DataOutputStream attaccato a out.

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 un PrintStream attaccato a out.

public PrintStream(OutputStream out, boolean autoflush);

Se autoflush è impostato a true, lo stream effettua un flush() 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).


Figura 3-8: Uso di 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).


Figura 3-9: Buffering nell'accesso a un file

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


Figura 3-10: Lettura e scrittura ASCII su file
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.


Figura 3-11: Scrittura ASCII, bufferizzata, su file
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")));



Torna al sommario | Vai avanti