In questa guida “JAVA – Applicazioni con SOCKET TCP” realizziamo delle applicazioni di rete a livello di Socket con protocollo TCP in JAVA. Nella sezione download si possono scaricare i sorgenti degli esempi fatti

In questa guida “JAVA – Applicazioni con SOCKET TCP” realizziamo delle applicazioni di rete a livello di Socket con protocollo TCP in JAVA. Nella sezione download si possono scaricare i sorgenti degli esempi fatti

Indice dei contenuti

Introduzione

La programmazione di rete consente la comunicazione tra dispositivi tramite protocolli come TCP/IP. In Java, questa comunicazione è gestita attraverso Socket TCP, che garantiscono un trasferimento affidabile dei dati tra client e server.

Prima di iniziare, è consigliabile preparare l’ambiente di lavoro seguendo le indicazioni disponibili qui

Il Package java.net

Il package java.net fornisce classi e metodi per creare applicazioni di rete in Java, semplificando operazioni come apertura di socket, connessioni client-server, gestione di URL e molto altro.

Struttura dei package in Java

Un package è un contenitore logico di classi e interfacce che consente di:

  • Evitare conflitti di nomi in progetti di grandi dimensioni.
  • Migliorare modularità e leggibilità del codice.
  • Favorire la riusabilità di librerie e moduli predefiniti.

Esempio di dichiarazione di package:

package com.miosito.utilities;

public class MyUtility {
    // Codice della classe
}

Esempio di importazione di un package:

import com.miosito.utilities.MyUtility;

public class Main {
    public static void main(String[] args) {
        MyUtility utility = new MyUtility();
        // Uso delle funzionalità di MyUtility
    }
}

Classi principali del package java.net

  • InetAddress: rappresenta indirizzi IP.
  • Socket e ServerSocket: implementano la comunicazione client-server.
  • URL e URLConnection: gestiscono risorse web e connessioni HTTP.

Gestione dell’I/O in Java con il Package java.io

Il package java.io consente di leggere e scrivere dati da file, memoria o connessioni di rete. Include classi per gestire:

Byte stream: InputStream, OutputStream -> gestione dell’I/O di byte

Character stream: Reader, Writer -> gestione dell’I/O di caratteri

Buffering: BufferedReader, BufferedWriter ->Forniscono un buffer per ottimizzare le operazioni di lettura e scrittura di caratteri, migliorando le prestazioni rispetto ai normali flussi di input/output.

File: File, FileInputStream, FileOutputStream ->non gestisce direttamente la lettura o la scrittura, ma fornisce metodi per manipolare file e directory

Oggetti: ObjectInputStream, ObjectOutputStream ->Consentono di leggere e scrivere oggetti interi facilitando la serializzazione e deserializzazione.

Esempio: lettura di un file riga per riga

import java.io.*;

public class FileReaderExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Errore durante la lettura del file: " + e.getMessage());
        }
    }
}

Esempio: scrittura su un file

import java.io.*;

public class FileWriterExample {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Questo è un esempio di output");
            System.out.println("Scrittura completata con successo.");
        } catch (IOException e) {
            System.err.println("Errore durante la scrittura del file: " + e.getMessage());
        }
    }
}

Gestione delle eccezioni

In Java, le eccezioni si gestiscono tramite try, catch, finally e throw.

  • try-catch: intercetta errori nel blocco try.
  • finally: esegue codice indipendentemente dal verificarsi di eccezioni.
  • throw: genera manualmente un’eccezione.
  • throws: dichiara che un metodo può generare un’eccezione controllata.

Esempio: eccezione personalizzata

class EtàNonValidaException extends Exception {
    public EtàNonValidaException(String message) {
        super(message);
    }
}

public class VerificaEtà {
    public static void main(String[] args) {
        try {
            verificaEta(16);
        } catch (EtàNonValidaException e) {
            System.err.println("Errore: " + e.getMessage());
        }
    }

    public static void verificaEta(int eta) throws EtàNonValidaException {
        if (eta < 18) throw new EtàNonValidaException("Età non valida, devi essere maggiorenne.");
        System.out.println("Età valida.");
    }
}

Realizzazione di Client e Server TCP

In questa sezione vedremo come utilizzare le classi di Java per implementare un server e un client TCP. Esploreremo le seguenti classi fondamentali:

  • Classe InetAddress
  • Classe ServerSocket
  • Classe Socket

Classe InetAddress

La classe InetAddress di Java è utilizzata per rappresentare gli indirizzi IP. Mette a disposizione diversi metodi per astrarre l’indirizzo IP, consentendo di lavorare sia con gli indirizzi numerici che con quelli alfanumerici (nomi host). La classe InetAddress non ha costruttori pubblici, quindi per creare un’istanza di questa classe si utilizzano metodi statici.

Un metodo statico in Java è un metodo che appartiene alla classe e non a un’istanza specifica della classe. Può essere chiamato senza la necessità di creare un oggetto della classe stessa. I metodi statici possono accedere solo ai membri statici della classe e non possono fare riferimento a this, poiché non esiste un’istanza.

Ecco alcuni dei metodi principali di InetAddress:

  • public static InetAddress getByName(String host)
    Restituisce un oggetto InetAddress che rappresenta l’indirizzo dell’host specificato. L’host può essere un nome di dominio o un indirizzo IP numerico. Se il parametro è null, si fa riferimento all’indirizzo della macchina locale.
  • public static InetAddress getLocalHost()
    Restituisce l’oggetto InetAddress corrispondente alla macchina locale. Se la macchina non è registrata nel DNS o è protetta da un firewall, l’indirizzo restituito sarà quello di loopback (127.0.0.1).
  • public String getHostName()
    Restituisce il nome dell’host corrispondente all’indirizzo IP dell’oggetto InetAddress. Se il nome non è noto, viene cercato tramite il DNS. Se la ricerca fallisce, verrà restituito l’indirizzo IP numerico.
  • public String getHostAddress()
    Restituisce l’indirizzo IP numerico dell’oggetto InetAddress sotto forma di stringa.
  • public byte[] getAddress()
    Restituisce l’indirizzo IP come una matrice di byte. L’ordinamento dei byte è “high byte first”, come tipico nelle comunicazioni di rete.

Ecco un esempio di utilizzo della classe InetAddress:

try {
    // Ottieni l'indirizzo IP di un host specificato
    InetAddress inetAddress = InetAddress.getByName("www.google.com");
    System.out.println("Indirizzo IP: " + inetAddress.getHostAddress());
    
    // Ottieni l'indirizzo della macchina locale
    InetAddress localHost = InetAddress.getLocalHost();
    System.out.println("Indirizzo locale: " + localHost.getHostAddress());
} catch (UnknownHostException e) {
    System.err.println("Host non trovato.");
}

Classe ServerSocket

La classe ServerSocket viene utilizzata per creare un server che accetta connessioni da parte di client. È un punto di ascolto che rimane in attesa di richieste di connessione su una porta specificata.

Il costruttore di ServerSocket richiede come parametro il numero di porta su cui il server sarà in ascolto:

ServerSocket server = new ServerSocket(1234);

Il metodo principale che viene utilizzato con ServerSocket è accept(), che fa sì che il server rimanga in attesa di una connessione da parte di un client. Quando un client si connette, viene restituito un oggetto Socket che rappresenta il canale di comunicazione tra il server e il client.

Ecco un esempio di utilizzo della classe ServerSocket:

try {
    // Crea un ServerSocket sulla porta 1234
    ServerSocket server = new ServerSocket(1234);
    
    // Attende una connessione da parte di un client
    Socket client = server.accept();
    System.out.println("Connessione stabilita con: " + client.getInetAddress());
    
    // Dopo aver stabilito la connessione, chiude il server
    server.close();
} catch (IOException e) {
    System.err.println("Errore nel server: " + e.getMessage());
}

In questo esempio, il server si mette in ascolto sulla porta 1234. Quando un client si connette, viene creato un oggetto Socket che rappresenta la connessione client-server, e il server si chiude subito dopo aver accettato la connessione.


Classe Socket

La classe Socket rappresenta un canale di comunicazione tra un client e un server. Un oggetto Socket viene creato quando un client si connette a un server tramite un ServerSocket.

Il costruttore di Socket richiede l’indirizzo del server e il numero di porta come parametri:

Socket clientSocket = new Socket("localhost", 1234);

Una volta che il client è connesso, è possibile inviare e ricevere dati tramite gli stream di input e output associati al socket. Ecco un esempio di utilizzo di Socket nel client:

try {
    // Crea un socket che si connette al server sulla porta 1234
    Socket socket = new Socket("localhost", 1234);
    
    // Ottieni i flussi di input e output
    OutputStream output = socket.getOutputStream();
    PrintWriter writer = new PrintWriter(output, true);
    
    // Invia un messaggio al server
    writer.println("Ciao, server!");
    
    // Chiudi la connessione
    socket.close();
} catch (IOException e) {
    System.err.println("Errore nel client: " + e.getMessage());
}

In questo esempio, il client si connette al server in ascolto sulla porta 1234, invia un messaggio e poi chiude la connessione.

Quando si comunica attraverso connessioni TCP, i dati vengono suddivisi in pacchetti (IP packet), quindi è consigliabile utilizzare degli stream “bufferizzati” evitando così di avere pacchetti contenenti poche informazioni. La definizione dei due stream, rispettivamente di input e di output, da parte di un server verso un client, è per esempio la seguente:

  • BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
  • DataOutputStream out = new DataOutputStream((client.getOutputStream()));

Dopo questa definizione l’uso dei socket diventa quindi trasparente: su questi oggetti si utilizzano i metodi readLine() e println()!

Spiegazione passo per passo:

client.getInputStream(): client è un oggetto di classe Socket che rappresenta una connessione tra il server e il client. Il metodo getInputStream() del socket restituisce un flusso di input (InputStream) associato al socket, che consente di leggere i dati inviati dal client. L’InputStream è il flusso di byte grezzi che arrivano dalla connessione.

new InputStreamReader(client.getInputStream()):L’InputStreamReader è un adattatore che converte un flusso di byte (InputStream) in un flusso di caratteri (Reader). Questo passaggio è necessario perché il InputStream lavora a livello di byte, mentre tu vuoi leggere i dati a livello di caratteri (ad esempio, stringhe di testo). L’InputStreamReader traduce i byte in caratteri secondo una codifica (di default UTF-8 o quella specificata).

new BufferedReader(new InputStreamReader(client.getInputStream())):Il BufferedReader avvolge l’InputStreamReader e fornisce un meccanismo di lettura più efficiente. Il BufferedReader utilizza un buffer interno che permette di leggere dati in blocchi più grandi, riducendo il numero di accessi al sistema di I/O, che può essere costoso in termini di prestazioni. Il BufferedReader fornisce anche comodi metodi come readLine() per leggere intere linee di testo.

Nell’esempio sotto:

  • Il server si aspetta di ricevere del testo dal client.
  • Ogni volta che il client invia una linea di testo e termina con un carattere di nuova linea (\n), il BufferedReader legge quella linea e il server la stampa.

import java.io.*;
import java.net.*;

// Crea un BufferedReader per leggere i dati inviati dal client
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));

// Legge una linea di testo dal client
String line = in.readLine();
System.out.println("Messaggio dal client: " + line);

        

Primo Esercizio

Obiettivo: il client invia un messaggio al server e riceve una risposta che include il suo IP.

Server.java

Vediamo l’algoritmo:

  1. Dichiaro il package in cui saranno contenuti i file Server.java e Client.java
  2. importo i package necessari (java.io e java.net)
  3. Definisco la classe Server
    • Definisco il main
      1. inserisco il codice nel costrutto try-catch per gestire le eccezioni
        1. Creo un server socket che ascolta sulla porta 12345 e stampo sulla console il messaggio di server in ascolto
        2. Accetto una connessione e stampo sulla console il messaggio di accettazione con l’IP del client
        3. Creo gli stream di input/output per ricevere e inviare dati
        4. Ricevo il messaggio dal client
        5. Rispondo al client
        6. Chiudo le risorse
      2. gestisco l’eccezione in catch
    • chiudo il main
  4. chiudo la classe

Nota In questo esempio viene realizzato solo il metodo main. Ovviamente per rendere il codice più leggibile, riusabile e strutturato correttamente si sarebbero dovuti implementare, ad esempio i metodi attendi() e comunica() oltre al main().

Il server rimarrà in ascolto sulla porta 12345 e, quando un client si connette, riceverà un messaggio e risponderà con l’IP del client e il messaggio ricevuto.

//server.java
import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) {
        try {
            // Creo il ServerSocket che ascolta sulla porta 12345
            ServerSocket serverSocket = new ServerSocket(12345);
            System.out.println("Server in ascolto sulla porta 12345...");
            
            // Attendo una connessione da parte di un client
            Socket clientSocket = serverSocket.accept();
            System.out.println("Connessione accettata da " + clientSocket.getInetAddress());
            
            // Creo gli stream di input e output per comunicare con il client
            BufferedReader input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            PrintWriter output = new PrintWriter(clientSocket.getOutputStream(), true);
            
            // Ricevo il messaggio inviato dal client
            String clientMessage = input.readLine();
            System.out.println("Messaggio dal client: " + clientMessage);
            
            // Rispondo al client con l'IP del client e il messaggio ricevuto
            String response = "Risposta dal server: Ciao " + clientSocket.getInetAddress().getHostAddress() +
                    ", ho ricevuto da te questo messaggio: " + clientMessage;
            output.println(response);
            
            // Chiudo le risorse
            input.close();
            output.close();
            clientSocket.close();
            serverSocket.close();
            
        } catch (IOException e) {
            System.err.println("Errore: " + e.getMessage());
        }
    }
}

Client.java

Il client si connetterà al server sulla porta 12345, invierà il messaggio “Ciao, server!” e riceverà la risposta dal server.

import java.io.*;
import java.net.*;

public class Client {
    public static void main(String[] args) {
        try {
            // Mi connetto al server sulla porta 12345
            Socket socket = new Socket("localhost", 12345);
            
            // Creo gli stream di input e output per comunicare con il server
            BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter output = new PrintWriter(socket.getOutputStream(), true);
            
            // Invia il messaggio "Ciao, server!"
            String message = "Ciao, server!";
            output.println(message);
            System.out.println("Messaggio inviato al server: " + message);
            
            // Ricevo la risposta dal server
            String response = input.readLine();
            System.out.println("Risposta dal server: " + response);
            
            // Chiudo le risorse
            input.close();
            output.close();
            socket.close();
            
        } catch (IOException e) {
            System.err.println("Errore: " + e.getMessage());
        }
    }
}

Spiegazione del Codice

Server.java

  1. Creazione del ServerSocket:
    • Viene creato un ServerSocket che ascolta sulla porta 12345.
    • La console stampa “Server in ascolto sulla porta 12345…” per informare che il server è pronto a ricevere connessioni.
  2. Accettazione della Connessione:
    • Il metodo accept() è utilizzato per aspettare che un client si connetta. Quando il client si connette, viene restituito un oggetto Socket che rappresenta la connessione client-server.
  3. Comunicazione:
    • Vengono creati due stream: uno di input per leggere i messaggi dal client (BufferedReader) e uno di output per inviare la risposta al client (PrintWriter).
    • Il server legge il messaggio inviato dal client e risponde con un messaggio che include l’indirizzo IP del client.
  4. Chiusura delle Risorse:
    • Una volta terminata la comunicazione, i flussi e i socket vengono chiusi per liberare le risorse.

Client.java

  1. Connessione al Server:
    • Il client si connette al server usando un oggetto Socket, passando l’indirizzo “localhost” e la porta 12345.
  2. Comunicazione:
    • Il client invia il messaggio “Ciao, server!” al server tramite il flusso di output (PrintWriter).
    • Poi, il client riceve la risposta dal server tramite il flusso di input (BufferedReader).
  3. Chiusura delle Risorse:
    • Una volta ricevuta la risposta dal server, il client chiude i flussi e il socket per terminare la connessione.

Output in Console

Server (in console):

Server in ascolto sulla porta 12345...
Connessione accettata da /127.0.0.1
Messaggio dal client: Ciao, server!

Client (in console):

Messaggio inviato al server: Ciao, server!
Risposta dal server: Ciao 127.0.0.1, ho ricevuto da te questo messaggio: Ciao, server!

Implementazione dei Metodi attendi() e comunica()

Per migliorare la struttura del codice e renderlo più leggibile e riutilizzabile, possiamo estrarre il codice del server nelle funzioni attendi() e comunica(), come suggerito di seguito:

Server.java con Metodi Separati

import java.io.*;
import java.net.*;

public class Server {

    public static void main(String[] args) {
        try {
            // Chiamata ai metodi separati
            ServerSocket serverSocket = new ServerSocket(12345);
            System.out.println("Server in ascolto sulla porta 12345...");
            Socket clientSocket = attendi(serverSocket);
            comunica(clientSocket);
        } catch (IOException e) {
            System.err.println("Errore: " + e.getMessage());
        }
    }

    public static Socket attendi(ServerSocket serverSocket) throws IOException {
        // Attende la connessione del client
        return serverSocket.accept();
    }

    public static void comunica(Socket clientSocket) throws IOException {
        // Gestisce la comunicazione con il client
        BufferedReader input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        PrintWriter output = new PrintWriter(clientSocket.getOutputStream(), true);
        
        String clientMessage = input.readLine();
        System.out.println("Messaggio dal client: " + clientMessage);
        
        String response = "Risposta dal server: Ciao " + clientSocket.getInetAddress().getHostAddress() +
                ", ho ricevuto da te questo messaggio: " + clientMessage;
        output.println(response);
        
        input.close();
        output.close();
        clientSocket.close();
    }
}

Nella sezione Download dedicata puoi scaricare i codici sorgente di applicazioni Client / Server con utilizzo di Socket TCP

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *