Protocollo UDP in Java: datagrammi, DatagramSocket e DatagramPacket
Dalla connessione stabile TCP al mondo senza connessione di UDP: ogni pacchetto viaggia da solo, con IP e porta già scritti sopra. Costruiamo un server che risponde in maiuscolo.
Nella lezione precedente abbiamo costruito un client e un server TCP: una comunicazione affidabile, ordinata, con una connessione stabile che resta aperta finché non la chiudiamo esplicitamente. Questa prova è particolarmente utile per capire il ruolo del protocollo di trasporto sottostante, perché oggi vediamo l'esatto opposto: il protocollo UDP (User Datagram Protocol), dove non esiste alcuna connessione persistente, e ogni singolo messaggio deve portare con sé tutte le informazioni necessarie a raggiungere il destinatario. Costruiremo di nuovo un'applicazione client-server, questa volta basata su datagrammi: il client invierà una stringa, il server la restituirà convertita in maiuscolo.
Con TCP avete visto il telefono: compongo numero, aspetto risposta, parlo, chiudo. Con UDP vediamo la posta: scrivo indirizzo su busta, imbucò, non so se arriva, non so se arriva in ordine. Perché vi mostro entrambi? Perché nella vita reale scegliete voi quale usare. Se fate un DNS, un videogioco a 60fps, una chiamata VoIP — usate UDP. Se fate un sito, una banca, una chat che non può perdere messaggi — TCP. Oggi capiamo quando la velocità vale più dell'affidabilità.
1. Il Protocollo UDP in Java
UDP è un protocollo di livello di trasporto che si distingue nettamente da TCP per il suo approccio alla comunicazione. A differenza di TCP, UDP non è orientato alla connessione: tra client e server non viene stabilito alcun canale persistente prima dell'invio dei dati.
Quando un'applicazione utilizza UDP, i messaggi vengono inviati sotto forma di datagrammi, ciascuno dei quali è indipendente dagli altri. Non esiste un meccanismo di handshaking iniziale, e il protocollo non garantisce che i pacchetti arrivino effettivamente a destinazione né che arrivino nello stesso ordine in cui sono stati inviati.
Questo non significa che UDP sia privo di controlli: viene comunque verificata l'integrità dei dati tramite checksum. Tuttavia, eventuali pacchetti persi o duplicati non vengono gestiti automaticamente dal protocollo: se la tua applicazione ha bisogno di quella garanzia, deve implementarla da sé, a livello applicativo.
Proprio grazie a questa semplicità strutturale, UDP risulta estremamente veloce ed efficiente. Per questo motivo viene utilizzato in numerose applicazioni reali, come il servizio DNS, lo streaming multimediale e tutte quelle situazioni in cui la rapidità di trasmissione è più importante dell'affidabilità assoluta — esattamente i casi d'uso che avevamo già anticipato parlando delle famiglie di socket.
User Datagram Protocol: protocollo di trasporto senza connessione, non affidabile, non ordinato, ma velocissimo. Ogni datagramma è indipendente, contiene già IP e porta destinazione, verifica solo checksum. Ideale quando velocità > affidabilità.
| Aspetto | TCP (lezione precedente) | UDP (questa lezione) |
|---|---|---|
| Connessione | Stabilita prima dello scambio dati (three-way handshake) | Assente: ogni datagramma viaggia per conto proprio |
| Affidabilità | Garantita: nessuna perdita, nessuna duplicazione, ordine preservato | Non garantita: possibili perdite, duplicazioni, riordini |
| Classi Java | Socket, ServerSocket | DatagramSocket, DatagramPacket |
| Velocità | Più lenta, per via del controllo di affidabilità | Più veloce, grazie all'assenza di controlli |
| Esempi d'uso | Web, email, trasferimento file | DNS, streaming multimediale, gaming online |
Quando digiti profgiagnotti.altervista.org il browser fa una richiesta DNS per sapere l'IP. Se usasse TCP: handshake (3 pacchetti) + richiesta + risposta + chiusura (4 pacchetti) = 8 pacchetti per un IP. Con UDP: 1 richiesta + 1 risposta = 2 pacchetti. 4 volte più veloce. Se il pacchetto si perde? Il client lo rimanda dopo 2 secondi. Per una query da 50 byte, UDP è imbattibile.
2. Comunicazione datagram e modello client-server
La comunicazione UDP è spesso definita datagram-based. Ogni messaggio contiene al proprio interno tutte le informazioni necessarie per raggiungere il destinatario, come indirizzo IP e numero di porta. Non esiste quindi uno stato condiviso tra client e server: a differenza di quanto accadeva con lo stream socket TCP, qui non c'è un accept() che "ricorda" chi è connesso.
In un sistema client-server basato su UDP, il client invia una richiesta al server specificando l'indirizzo e la porta di destinazione. Il server, una volta ricevuto il datagramma, elabora il contenuto e invia una risposta utilizzando le informazioni presenti nel pacchetto ricevuto: è il pacchetto stesso, non una connessione aperta, a dire al server dove rispondere.
Questo modello rende la comunicazione semplice e leggera, ma richiede attenzione nella progettazione dell'applicazione, poiché la gestione di errori, ritrasmissioni o perdita di dati deve essere eventualmente implementata a livello applicativo.
Se all'orale vi chiedo "disegnate TCP vs UDP", voglio vedere: TCP = linea continua tra client e server, con scritta "connessione aperta". UDP = tante freccette separate, ognuna con etichetta "IP:porta + dati". In TCP il server ricorda chi è connesso (accept). In UDP il server non ricorda nulla: ogni volta legge IP e porta dal pacchetto che arriva. È questa la differenza concettuale più importante.
3. Le classi Java per la comunicazione UDP
Java fornisce un supporto diretto alla comunicazione UDP attraverso il package java.net, lo stesso che abbiamo già usato per TCP. Le due classi fondamentali, questa volta, sono DatagramSocket e DatagramPacket.
| Classe | Ruolo |
|---|---|
| DatagramSocket | Rappresenta il socket UDP vero e proprio. Può essere creato senza specificare una porta (tipico del client, che ne riceve una dinamica) oppure associato a una porta specifica (tipico del server) |
| DatagramPacket | Rappresenta il singolo pacchetto di dati che viaggia sulla rete: contiene i dati da trasmettere, l'indirizzo IP del destinatario e la porta. In ricezione, permette anche di recuperare indirizzo e porta del mittente |
DatagramSocket è il "telefono" (il dispositivo fisico che invia/riceve). DatagramPacket è la "busta chiusa" che parte già indirizzata — a differenza di una telefonata TCP, dove prima componi il numero e resti in linea, qui spedisci direttamente la busta e nient'altro. In questa fase conviene concentrarsi prima sul funzionamento generale delle due classi, prima di vederle applicate nel codice completo.
4. Realizzazione di un'applicazione client-server UDP
Per comprendere concretamente il funzionamento di UDP in Java, realizziamo una semplice applicazione client-server. Il client invierà una stringa di testo al server, e il server risponderà restituendo la stessa stringa convertita in maiuscolo. L'esempio è volutamente semplice, ma consente di osservare tutti i passaggi fondamentali della comunicazione UDP.
4.1 Implementazione del server UDP
Il server UDP ha il compito di restare in ascolto su una porta ben definita — in questo esempio la 12345 — ricevere i messaggi inviati dai client ed elaborare una risposta. Dopo aver creato il DatagramSocket, il server entra in un ciclo continuo in cui attende la ricezione dei pacchetti: quando un messaggio arriva, il contenuto viene estratto dal datagramma, convertito in maiuscolo e inviato nuovamente al client, usando l'indirizzo IP e la porta letti direttamente dal pacchetto ricevuto. Se il messaggio ricevuto è la stringa "0", il server termina l'esecuzione e chiude il socket.
Ho scelto questo esempio perché può essere facilmente modificato in seguito — per esempio calcolando qualcos'altro invece del maiuscolo — senza dover toccare la parte di comunicazione, che resta sempre la stessa. Volete fare un server che conta le vocali? Cambiate solo toUpperCase() con la vostra logica. Volete fare un eco-server? Rimandate indietro la stessa stringa. La parte UDP resta identica: receive → elabora → send.
4.2 Descrizione del codice UDPServer.java
- Creazione del socket: viene creato un oggetto DatagramSocket sulla porta 12345; si definiscono due buffer (uno per la ricezione, uno per la trasmissione, entrambi da 1024 byte) e una variabile booleana per gestire il ciclo di esecuzione del server.
- Attesa e ricezione dati dal client: il server attende la ricezione di un pacchetto UDP tramite receive(); il messaggio viene estratto dal datagramma e stampato a console.
- Preparazione della risposta: il messaggio ricevuto viene convertito in maiuscolo, il server identifica indirizzo IP e porta del client dal pacchetto stesso, e costruisce un nuovo DatagramPacket da inviare come risposta.
- Gestione dell'uscita: se il client invia "0", il server termina l'elaborazione, esce dal ciclo e chiude il socket.
Perché 1024? È una dimensione comoda per messaggi di testo. Ma UDP ha un limite teorico di 65507 byte di payload (65535 - header IP - header UDP). Se inviate più di 1024 byte e il buffer è più piccolo, i dati in eccesso vengono troncati silenziosamente senza errore — uno dei bug più subdoli di UDP. Per testi brevi 1024 va bene, per file usate buffer più grande o frammentate voi a livello applicativo.
4.3 Implementazione del client UDP
Il client ha un comportamento più semplice rispetto al server. Dopo aver creato il socket, chiede all'utente di inserire una stringa da tastiera; il messaggio viene poi inviato al server sotto forma di datagramma. Successivamente, il client si mette in attesa della risposta: quando il pacchetto arriva, il messaggio viene estratto e visualizzato a schermo, e il socket viene chiuso.
Durante la prova ho trovato un errore nella prima versione del codice: il messaggio di richiesta input conteneva un carattere "n" residuo (probabilmente un \n rimasto a metà da una copia frettolosa), che sarebbe finito stampato a schermo alla lettera. Ho corretto il problema verificando riga per riga il testo passato a println(), ed è un buon promemoria: controlla sempre le stringhe letterali prima di eseguire. In laboratorio ho visto studenti perdere 20 minuti perché avevano scritto "Inserisci una stringan:" invece di "Inserisci una stringa:" — copiate con attenzione.
Attenzione — perché serve substring() qui e non nel server
Nel server avevamo usato new String(dati, 0, lunghezza), che estrae subito solo i byte realmente ricevuti. Nel client, il codice costruisce prima la String dall'intero buffer con new String(datagrammaRicevuto.getData()), e solo dopo la taglia con substring(0, numCaratteri). Il risultato finale è identico, ma se dimentichi quel substring() ti ritroverai in coda alla risposta gli eventuali byte "vuoti" rimasti nel buffer da 1024 posizioni: un errore silenzioso, difficile da notare finché non stampi la lunghezza esatta della stringa.
Esempio: invii "ciao" (4 byte) in buffer da 1024. Se fai new String(buffer) senza tagliare, ottieni "ciao" + 1020 caratteri strani (spazi, \0, residui precedenti). Con getLength() sai che sono arrivati 4 byte, con substring(0,4) prendi solo "ciao". Nel server abbiamo usato il costruttore con offset e lunghezza, che fa la stessa cosa in un colpo solo: new String(data, 0, length) è più elegante, preferitelo.
4.4 Descrizione del codice UDPClient.java
- Configurazione della comunicazione: si definiscono i buffer per trasmissione e ricezione, i riferimenti del server (indirizzo IP e porta 12345) e si crea il DatagramSocket del client.
- Invio del messaggio al server: il client chiede all'utente una stringa, la converte in byte e la invia tramite un DatagramPacket indirizzato al server.
- Attesa e ricezione della risposta: il client si blocca in attesa della risposta; una volta ricevuto il pacchetto, il messaggio viene estratto e stampato.
- Chiusura della connessione: dopo aver ricevuto la risposta, il client chiude il socket e termina l'esecuzione.
5. Il caso limite: terminare il server
Ho provato volutamente anche un caso limite, perché è il tipo di dettaglio che negli esempi didattici viene spesso lasciato implicito: cosa succede se l'utente, dal client, digita esattamente la stringa "0"? Il server la riceve, la converte comunque in maiuscolo e risponde normalmente ("0" maiuscolo è ancora "0"), ma subito dopo aver inviato la risposta controlla il messaggio originale ricevuto: se corrisponde a "0", stampa un messaggio di commiato, porta la variabile attivo a false, e al giro successivo del ciclo while il server esce e chiude il socket. È un meccanismo di terminazione volutamente elementare, adatto a un esempio didattico: in un'applicazione reale utilizzeresti probabilmente un comando dedicato, distinto dai normali messaggi applicativi.
Invece di usare "0" come stringa normale, potreste usare un comando tipo "/quit" o "EXIT". Così evitate che un utente che vuole convertire davvero "0" in maiuscolo faccia spegnere il server per sbaglio. Esempio: if (ricevuto.equalsIgnoreCase("/quit")) attivo = false; — più esplicito e sicuro. In applicazioni reali, i comandi di controllo sono sempre distinti dai dati.
6. Esecuzione e test dell'applicazione
Per testare l'applicazione è sufficiente avviare prima il server e, successivamente, il client, utilizzando Visual Studio Code o un qualsiasi ambiente di sviluppo Java. Inserendo una stringa dal client, il server risponderà restituendo la versione in maiuscolo. Se il client invia il valore "0", il server terminerà la propria esecuzione.
Nel file .zip che trovi nelle risorse ho creato due differenti client testando la possibilità che si possano connettere contemporaneamente. L'output a console è il seguente:
Con TCP servivano thread perché accept() creava un socket per ogni client e la connessione restava aperta. Con UDP non c'è connessione: il server fa solo receive() → elabora → send(). Quindi può gestire Client1, poi Client2, poi ancora Client1 senza thread — basta che i pacchetti arrivino uno dopo l'altro. Ho testato due client in contemporanea proprio per farvi vedere questa differenza: con UDP non serve complicarsi la vita per il multi-client base.
6.1 TCP vs UDP fianco a fianco - stessa app, filosofie opposte
Ho confrontato due approcci differenti prima di scegliere questo — TCP nella lezione precedente, UDP in questa — proprio per farteli vedere fianco a fianco: stesso obiettivo (uno scambio di messaggi tra client e server), due filosofie opposte. Con TCP abbiamo aperto una connessione, mantenuta per tutta la comunicazione, e chiusa esplicitamente alla fine. Con UDP non c'è mai stata alcuna connessione: solo pacchetti autosufficienti, spediti e ricevuti uno alla volta. Tenere bene a mente questa differenza ti aiuterà moltissimo quando, in futuro, dovrai scegliere quale protocollo usare per una tua applicazione di rete.
| Aspetto | TCP (L2) | UDP (L3) |
|---|---|---|
| Socket server | ServerSocket(12345) + accept() bloccante | DatagramSocket(12345) + receive() bloccante |
| Socket client | Socket("localhost",12345) connessione | DatagramSocket() porta dinamica |
| Invio | PrintWriter.println() su stream | DatagramPacket + send() con IP:porta |
| Ricezione | BufferedReader.readLine() | DatagramPacket + receive() + getData() |
| Chiusura | close() chiude connessione | close() libera porta |
| Multi-client | Serve thread per ogni client | Gestito naturalmente senza thread (base) |
"Perché con UDP non serve thread per due client?" Risposta: perché non c'è connessione aperta. Il server UDP è come un ufficio postale: arriva busta, la leggo, rispondo, dimentico tutto. Arriva altra busta, stessa cosa. Con TCP è come telefonata: se parlo con Cliente A, la linea è occupata, Cliente B deve aspettare o devo aprire seconda linea (thread). Questa analogia vi fa prendere 30.
7. 📌 Concetti chiave
- UDP è senza connessione: ogni datagramma è indipendente e autosufficiente, senza handshake iniziale né garanzie di consegna o ordine. Ogni pacchetto porta IP e porta.
- UDP verifica integrità con checksum, ma non gestisce perdite o duplicazioni: se serve affidabilità, la implementi tu a livello applicativo.
- DatagramSocket è il socket UDP: senza porta (client, dinamica) o con porta fissa (server). DatagramPacket è il pacchetto: dati + IP + porta destinazione in invio, IP + porta mittente in ricezione.
- Modello client-server UDP senza stato: il server ricava indirizzo e porta del client direttamente dal pacchetto ricevuto (
getAddress(),getPort()), non da una connessione aperta. - Buffer e getLength(): usare sempre
getLength()per sapere quanti byte sono arrivati davvero, altrimenti restano byte residui nel buffer da 1024. Meglionew String(data, 0, length)chenew String(data)+ substring. - Caso limite "0": meccanismo didattico di terminazione — in produzione usare comando dedicato tipo "/quit".
- Rispetto a TCP, UDP è più semplice e veloce, gestisce multi-client base senza thread, ma lascia all'applicazione responsabilità di affidabilità, ordine e gestione errori. Scelta dipende da compromesso velocità vs affidabilità.
Vuoi passare all'azione? Accedi alla repository ufficiale GitHub del corso per scaricare il file del laboratorio pratico
🛠️ Esercizi Socket UDP →Vuoi consultare la sezione dedicata ai laboratori sul sito? Accedi cliccando il pulsante qui sotto.
📊 Sezione dedicata ai LaboratoriVuoi consultare la sezione dedicata alle guide sul sito? Accedi cliccando il pulsante qui sotto.
🧪 Guida preparazione ambiente di lavoro →📚 Prossimo passo - Thread e multi-client TCP
Nella prossima lezione torniamo a TCP ma lo rendiamo davvero professionale: server multi-thread che gestisce N client contemporanei con while(true) + new Thread(). Se questa lezione su UDP è chiara, avrete capito perché a volte UDP basta e a volte serve TCP con thread. Esercizio: modificate UDPServer per contare le vocali invece di fare maiuscolo — se ci riuscite senza toccare la parte di comunicazione, avete capito la separazione logica.