In questa guida “JAVA – Applicazioni con SOCKET TCP” realizziamo delle applicazioni di rete a livello di Socket con protocollo TCP in JAVA. Qui in basso e nella sezione download si possono scaricare i sorgenti
socket-TCP.zip (102 download )
Sommario
Introduzione
Prima di iniziare è utile preparare l’ambiente di lavoro. Tutte le indicazioni sono disponibili alla pagina: https://profgiagnotti.it/guida-alla-creazione-dellambiente-di-lavoro-per-applicazioni-con-socket-in-java/
Gestione della Comunicazione di Rete in Java: Il Package java.net
Il linguaggio Java dispone del package java.net per la gestione della comunicazione di rete. Questo package offre una serie di classi e funzionalità progettate per semplificare le operazioni di rete nei programmi Java.
Struttura dei Package in Java
Un package in Java è un modo per organizzare e raggruppare classi, interfacce ed altre entità di un programma in una struttura logica. I package offrono numerosi vantaggi, tra cui:
- Evitare conflitti di nomi, soprattutto nei progetti di grandi dimensioni con molteplici sviluppatori.
- Migliorare la modularità del codice, facilitando la leggibilità e la manutenzione del programma.
- Favorire la riusabilità del codice, permettendo l’uso efficiente di librerie o moduli predefiniti.
In breve, un package può essere considerato come un “contenitore” che raggruppa codice correlato, rendendo più facile la manutenzione e la scalabilità di un programma.
Ad esempio, il package java.net raggruppa componenti direttamente correlati alla comunicazione di rete, rendendo il codice più intuitivo e coerente.
Sintassi e Utilizzo dei Package in Java
I package in Java sono gestiti tramite le seguenti operazioni principali:
Dichiarazione del Package
All’inizio di un file Java, si utilizza la parola chiave package
seguita dal nome del package. Questo specifica la posizione logica della classe o dell’interfaccia.
Esempio:
package com.miosito.utilities;
public class MyUtility {
// Codice della classe
}
In questo esempio, la classe MyUtility
appartiene al package com.miosito.utilities.
Importazione di un Package
Per utilizzare classi definite in un altro package, è necessario importarle usando la parola chiave import
.
Esempio:
import com.miosito.utilities.MyUtility;
public class Main {
public static void main(String[] args) {
MyUtility utility = new MyUtility();
// Utilizzo delle funzionalità di MyUtility
}
}
Grazie alla direttiva import
, il programma può accedere agevolmente alle classi e alle funzionalità definite nel package com.miosito.utilities
.
Approfondimento sul Package
Il package java.net include alcune delle classi più utilizzate per la programmazione di rete, come ad esempio:
-
URL
: Fornisce una rappresentazione facile da manipolare di un URL. -
URLConnection
: Gestisce connessioni per risorse rappresentate da URL. -
Socket
eServerSocket
: Implementano la comunicazione client-server tramite socket.
Grazie a queste classi, sviluppatori possono creare applicazioni con funzionalità di rete avanzate, come browser web, client FTP, o server HTTP personalizzati.
Gestione dell’I/O in Java con il Package java.io
Il package java.io
fa parte della libreria standard di Java e fornisce un insieme di classi e interfacce per la gestione dell’input e dell’output (I/O). Grazie a questo package, è possibile leggere e scrivere dati da e verso diverse fonti, come file, flussi di rete e memoria.
Le classi presenti in java.io
permettono di gestire dati in vari formati, inclusi byte e caratteri, semplificando le operazioni di lettura e scrittura in modo efficiente.
Eccezioni nei metodi del package java.net
I metodi delle classi appartenenti al package java.net
possono sollevare eccezioni specifiche che derivano dalla classe java.io.IOException
. Queste eccezioni vengono sollevate in caso di errori di I/O durante le operazioni di rete, come problemi di connessione o timeout. È importante gestire adeguatamente queste eccezioni per garantire la stabilità e l’affidabilità dell’applicazione.
Classi principali del package java.io
Di seguito sono elencate alcune delle principali classi del package java.io
e la loro funzione:
- InputStream e OutputStream: Classi base per la gestione dell’I/O di byte da varie sorgenti e verso diverse destinazioni.
- Reader e Writer: Classi base per la gestione dell’I/O di caratteri, che consentono di operare direttamente su testo anziché su byte.
- File: Classe che non gestisce direttamente la lettura o la scrittura, ma fornisce metodi per manipolare file e directory, ad esempio per la creazione, cancellazione e controllo di esistenza.
- BufferedReader e BufferedWriter: Forniscono un buffer per ottimizzare le operazioni di lettura e scrittura di caratteri, migliorando le prestazioni rispetto ai normali flussi di input/output.
- FileInputStream e FileOutputStream: Permettono la lettura e la scrittura di byte da e verso file.
- ObjectInputStream e ObjectOutputStream: Consentono di leggere e scrivere oggetti interi come sequenze di byte, facilitando la serializzazione e deserializzazione.
Esempi di utilizzo
Lettura da un file di testo
Un esempio pratico di utilizzo di BufferedReader
per leggere un file riga per riga:
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(newFileReader("example.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (IOException e) {
System.err.println("Errore durante la lettura del file: " +e.getMessage());
}
}
}
Scrittura su un file di testo
Un esempio di utilizzo di BufferedWriter
per scrivere un testo 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");
writer.close();
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
In Java, le eccezioni vengono gestite attraverso un meccanismo strutturato che utilizza i blocchi try
, catch
, finally
e l’istruzione throw
. La gestione delle eccezioni consente di intercettare e gestire errori che potrebbero verificarsi durante l’esecuzione di un programma, migliorando così la robustezza e la stabilità dell’applicazione.
1. Blocco try-catch
Il blocco try
viene utilizzato per racchiudere il codice che potrebbe generare un’eccezione, mentre il blocco catch
intercetta e gestisce l’eccezione se questa viene sollevata. Se un’eccezione viene sollevata all’interno del blocco try
, il controllo passa immediatamente al blocco catch
.
try {
int risultato = 10 / 0; // Questo genererà un'eccezione ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Errore: Divisione per zero non permessa.");
}
2. Blocco finally
Il blocco finally
è opzionale e viene utilizzato per eseguire il codice che deve essere eseguito indipendentemente dal fatto che si verifichi o meno un’eccezione. Questo è particolarmente utile per operazioni di “pulizia”, come la chiusura di file o connessioni di rete.
try {
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
// Operazioni di lettura
} catch (IOException e) {
System.out.println("Errore nella lettura del file.");
} finally {
System.out.println("Chiusura risorse...");
}
3. Lanciare Eccezioni con throw
Il costrutto throw
viene utilizzato per generare manualmente un’eccezione all’interno di un metodo o di un blocco di codice. Quando un’eccezione viene lanciata, il controllo del programma viene immediatamente trasferito al blocco catch
più vicino, se presente, o viene propagata più in alto nella chiamata del metodo.
public void verificaEta(int eta) {
if (eta < 18) {
throw new IllegalArgumentException("Età non valida, devi essere maggiorenne.");
}
}
4. Dichiarare Eccezioni con throws
Se un metodo può generare un’eccezione controllata (checked exception), deve dichiararla nella sua firma usando throws
. Questo avvisa il chiamante del metodo che deve gestire o propagare l’eccezione.
public void leggiFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
String linea = reader.readLine();
// Altri processi
}
Eccezioni Controllate (Checked) vs Non Controllate (Unchecked)
-
Eccezioni Controllate (Checked): Devono essere gestite o dichiarate nel metodo utilizzando
throws
. Queste eccezioni derivano dalla classeException
, ad esempioIOException
oSQLException
. -
Eccezioni Non Controllate (Unchecked): Non sono obbligatorie da gestire. Derivano dalla classe
RuntimeException
, comeNullPointerException
oArrayIndexOutOfBoundsException
. Possono essere evitate scrivendo codice più robusto e facendo attenzione a non accedere a riferimenti nulli o ad indici fuori dai limiti.
Creazione di Eccezioni Personalizzate
In Java è possibile creare eccezioni personalizzate estendendo la classe Exception
(per eccezioni controllate) o RuntimeException
(per eccezioni non controllate). Questo ti consente di definire le tue eccezioni specifiche, rispondendo meglio alle esigenze della tua applicazione.
Esempio di 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); // Lancerà l'eccezione personalizzata
} 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 un Client e di un Server TCP in Java
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 oggettoInetAddress
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’oggettoInetAddress
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’oggettoInetAddress
. 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’oggettoInetAddress
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
Adesso abbiamo tutti gli elementi per creare un server ed un client che possano comunicare atrraverso il protocollo TCP
L’esercizio prevede l’implementazione del client e del server in modo che il server rimanga in ascolto su una porta scelta a piacere (ad esempio 12345) di una singola connessione da parte di un client.
Dopo aver stabilito la connessione il client invia il messaggio al server “Ciao, server!” e riceverà la risposta dal server “Risposta dal server: Ciao /x.x.x.x, ho ricevuto da te questo messaggio: Ciao, server!”
x.x.x.x rappresenta l’IP del client. Sulla console del client vedremo visualizzato: “Server in ascolto sulla porta 12345… Connessione accettata da /127.0.0.1 Messaggio dal client: Ciao, server!”
Implementiamo il server (Server.java). Vediamo l’algoritmo:
- Dichiaro il package in cui saranno contenuti i file Server.java e Client.java
- importo i package necessari (java.io e java.net)
- Definisco la classe Server
- Definisco il main
- inserisco il codice nel costrutto try-catch per gestire le eccezioni
- Creo un server socket che ascolta sulla porta 12345 e stampo sulla console il messaggio di server in ascolto
- Accetto una connessione e stampo sulla console il messaggio di accettazione con l’IP del client
- Creo gli stream di input/output per ricevere e inviare dati
- Ricevo il messaggio dal client
- Rispondo al client
- Chiudo le risorse
- gestisco l’eccezione in catch
- inserisco il codice nel costrutto try-catch per gestire le eccezioni
- chiudo il main
- Definisco il main
- chiudo la classe
Nota In questo esempio è stato 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(). [Provare ad implementare questa soluzione]
Server.java
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
-
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.
- Viene creato un
-
Accettazione della Connessione:
- Il metodo
accept()
è utilizzato per aspettare che un client si connetta. Quando il client si connette, viene restituito un oggettoSocket
che rappresenta la connessione client-server.
- Il metodo
-
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.
- Vengono creati due stream: uno di input per leggere i messaggi dal client (
-
Chiusura delle Risorse:
- Una volta terminata la comunicazione, i flussi e i socket vengono chiusi per liberare le risorse.
Client.java
-
Connessione al Server:
- Il client si connette al server usando un oggetto
Socket
, passando l’indirizzo “localhost” e la porta 12345.
- Il client si connette al server usando un oggetto
-
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
).
- Il client invia il messaggio “Ciao, server!” al server tramite il flusso di output (
-
Chiusura delle Risorse:
- Una volta ricevuta la risposta dal server, il client chiude i flussi e il socket per terminare la connessione.
Uscita della 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:
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