Socket in Java - Client Server TCP | Tecnologia Anno 5 Modulo 2 Lezione 2 | Prof. Giagnotti
Tecnologia · Anno 5 · Modulo 2 · Lezione 2
✍ Autore: profgiagnotti · Materia: Tecnologia · Pratica Java Socket

Socket in Java: client e server TCP riga per riga

Dalla teoria dei socket alla pratica: package java.net e java.io, gestione eccezioni, InetAddress, ServerSocket, Socket e il nostro primo scambio reale di messaggi tra client e server.

Anno 5 Modulo 2 Lezione 2 Java Socket TCP java.net ~28 min
OBIETTIVI DI APPRENDIMENTO
✓Conoscere la struttura dei package Java e il ruolo dei package java.net e java.io nella programmazione di rete
✓Distinguere byte stream, character stream, stream bufferizzati e saperli applicare alla lettura/scrittura di file e socket
✓Gestire le eccezioni con try/catch/finally/throw/throws, comprese le eccezioni personalizzate
✓Utilizzare le classi InetAddress, ServerSocket e Socket per realizzare una comunicazione client-server TCP completa
✓Scrivere, eseguire e leggere un programma client e un programma server Java che si scambiano messaggi, riconoscendo il ruolo di ogni singola riga

Nella lezione precedente abbiamo visto la teoria dei socket: cosa sono, come si identificano tramite la coppia IP:porta, come funziona il ciclo operativo di uno stream socket lato client e lato server. Ho mantenuto il codice volutamente semplice per facilitarne la lettura in questa lezione, perché il nostro obiettivo oggi è un altro: passare dalla teoria alla pratica, scrivendo un vero client e un vero server TCP in Java, riga per riga, capendo esattamente cosa fa ciascuna istruzione. Se non hai ancora configurato l'ambiente di lavoro con Visual Studio Code e il JDK, recupera prima la guida dedicata: da qui in avanti diamo per scontato che tu abbia già un progetto Java pronto all'uso.

💬 Nota docente - Come lavoreremo oggi

Ragazzi, oggi niente teoria astratta: scriviamo codice che funziona davvero. Vi chiedo una cosa sola: non copiate-incollate a caso. Ogni riga che vi mostro ha un commento che spiega perché c'è. Leggetela, capite cosa fa, poi scrivetela voi. Alla fine della lezione avrete un server che gira sulla 12345 e un client che gli parla — ed è vostro, non mio. Se riuscite a spiegare a voce cosa fa accept() e perché usiamo BufferedReader, avete già passato l'interrogazione.

IMMAGINE 01 - DA TEORIA SOCKET A CODICE JAVA
Da Teoria Socket a Codice Java - java.net + java.io Percorso: Teoria IP:porta e quintupla → Package java.net ServerSocket/Socket → Package java.io stream bufferizzati → Applicazione client-server finale DA TEORIA SOCKET → CODICE JAVA java.net = canali | java.io = flussi bufferizzati | java = server + client JAVA ☕ 1. TEORIA SOCKET 📚 • IP:porta → socket address • Quintupla: (IPc,Pc,IPs,Ps,Proto) • Stream TCP vs Datagram UDP • accept() blocca, nuovo fd 192.168.1.10:52341 → 1.2.3.4:443 TCP Modulo 2 - Già fatto Base concettuale si mappa in 2. java.net - CANALI CORE ServerSocket = telefono in attesa ServerSocket(12345) .accept() → Socket Socket = telefonata attiva Socket("IP", 12345) getInput/OutputStream() 1 ServerSocket = 1 porta N Socket = N client (figli) + flussi 3. java.io - FLUSSI • FileInputStream / Output • BufferedReader / Writer • readLine() riga per riga • println() + flush() new BufferedReader( InputStreamReader( socket.getInput())) Buffer = velocità ★ = app 4. APP FINALE 🚀 Echo Server while(true){ Socket s=ss.accept(); new Thread( ()->gestisci(s)).start(); } Thread per client Concorrente ★ FLUSSO DATI REALE - Cosa succede quando scrivi "Ciao" CLIENT Java out.println("Ciao"); out.flush(); ↓ BufferedWriter → OutputStreamWriter(UTF-8) → ↓ Socket OutputStream → TCP buffer kernel → RETE - 3-way già fatto - Byte stream SERVER Java String r = in.readLine(); ↑ BufferedReader.readLine() blocca fino a \n ↑ InputStreamReader decodifica UTF-8 ↑ Socket InputStream ← TCP buffer kernel r = "Ciao" → System.out.println(r) Echo: out.println("Echo: "+r)

1. La programmazione di rete in Java

La programmazione di rete consente la comunicazione tra dispositivi tramite protocolli come TCP/IP. In Java questa comunicazione è gestita attraverso i socket TCP, che — come abbiamo visto nella lezione precedente — garantiscono un trasferimento affidabile e ordinato dei dati tra client e server. Java mette a disposizione due package fondamentali per questo scopo: java.net, dedicato alla comunicazione di rete vera e propria, e java.io, dedicato alla lettura e scrittura dei dati che viaggiano su quella comunicazione.

💡 Tips - Ambiente di lavoro

Prima di iniziare, è consigliabile preparare l'ambiente di lavoro seguendo la guida VS Code + JDK. Create un progetto vuoto, due file Server.java e Client.java nella stessa cartella. Niente package custom per ora, tutto in default — così evitiamo complicazioni con classpath. Se usate IntelliJ, stesso discorso: progetto Java semplice, JDK 17+.

2. Il package java.net

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

2.1 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, e favorire la riusabilità di librerie e moduli predefiniti. Ogni file sorgente Java che dichiara di appartenere a un package deve iniziare con l'istruzione package, mentre per usare classi definite in un altro package si utilizza l'istruzione import.

2.1.1 Esempio di dichiarazione di package:

JAVA - DICHIARAZIONE PACKAGE
package com.miosito.utilities; // dichiara che questa classe appartiene al package com.miosito.utilities public class MyUtility { // definizione della classe pubblica MyUtility // corpo della classe: qui andrebbero attributi e metodi }

2.1.2 Esempio di importazione di un package:

JAVA - IMPORT PACKAGE
import com.miosito.utilities.MyUtility; // importa la classe MyUtility dal suo package public class Main { // classe principale del programma public static void main(String[] args) { // punto di ingresso dell'applicazione MyUtility utility = new MyUtility(); // crea un'istanza di MyUtility // da qui in poi si possono usare i metodi di "utility" } }
🏠 Analogia - Package = cassetti dell'armadio

Un package è come un cassetto etichettato: metti tutte le classi che riguardano la rete in java.net, quelle che riguardano i file in java.io. Se non ci fossero cassetti, avresti 10.000 classi buttate sul pavimento con lo stesso nome e non troveresti più nulla. Quando fai import java.net.* stai dicendo "apri il cassetto rete e fammi usare tutto quello che c'è dentro".

2.1.3 Classi principali del package java.net

ClasseA cosa serve
InetAddressRappresenta un indirizzo IP, numerico o come nome host — per risolvere DNS
Socket e ServerSocketImplementano la comunicazione client-server basata su stream TCP — cuore della lezione
URL e URLConnectionGestiscono risorse Web e connessioni HTTP a più alto livello — livello applicativo

3. 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 organizzate per tipo di dato e per funzione:

CategoriaClassi principaliA cosa serve
Byte streamInputStream, OutputStreamGestiscono l'I/O a livello di singoli byte — grezzo, per immagini, file binari
Character streamReader, WriterGestiscono l'I/O a livello di caratteri — per testo, con codifica UTF-8
BufferingBufferedReader, BufferedWriterAggiungono un buffer interno per ottimizzare lettura/scrittura, migliorando prestazioni + metodo readLine()
FileFile, FileInputStream, FileOutputStreamManipolano file e directory, apertura file fisici
OggettiObjectInputStream, ObjectOutputStreamLeggono/scrivono interi oggetti Java, con serializzazione
🔍 Perché due package? net + io

java.net apre il tubo (il socket). java.io fa passare l'acqua nel tubo (i dati). Senza net non hai connessione. Senza io hai connessione muta, non puoi leggere/scrivere. È come telefono: net fa squillare e stabilisce la linea, io è la voce che ci passa dentro.

Questo esempio è nato proprio da una prova che ho effettuato in laboratorio, ed è il modo più diretto per vedere all'opera BufferedReader e BufferedWriter: leggere un file riga per riga, e scrivere un file da zero.

3.1 Esempio — lettura di un file riga per riga

Il metodo readLine() restituisce null quando il file è terminato: è la condizione che usiamo per fermare il ciclo while.

JAVA - LETTURA FILE CON BUFFEREDREADER
import java.io.*; // importa tutte le classi del package java.io public class FileReaderExample { // classe di esempio per la lettura di un file public static void main(String[] args) { // metodo main, punto di ingresso // try-with-resources: il BufferedReader viene chiuso automaticamente alla fine del blocco try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) { String line; // variabile che conterrà, di volta in volta, la riga letta while ((line = reader.readLine()) != null) { // legge una riga; esce quando non ce ne sono più System.out.println(line); // stampa a console la riga appena letta } } catch (IOException e) { // intercetta eventuali errori di lettura (es. file non trovato) System.err.println("Errore durante la lettura del file: " + e.getMessage()); // stampa l'errore } } }
💡 Tips - try-with-resources

Notate le parentesi tonde dopo try? È il try-with-resources introdotto in Java 7: tutto quello che apri lì dentro (file, socket, stream) viene chiuso automaticamente alla fine, anche se c'è un'eccezione. Prima di Java 7 dovevate chiudere tutto a mano nel finally. Usatelo sempre: niente più reader.close() dimenticato che lascia file bloccati.

3.2 Esempio — scrittura su un file

Anche qui il try-with-resources garantisce la chiusura automatica dello stream, anche in caso di eccezione.

JAVA - SCRITTURA FILE CON BUFFEREDWRITER
import java.io.*; // importa tutte le classi del package java.io public class FileWriterExample { // classe di esempio per la scrittura di un file public static void main(String[] args) { // metodo main, punto di ingresso // apre (o crea) output.txt in scrittura, chiudendolo automaticamente alla fine try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) { writer.write("Questo è un esempio di output"); // scrive la stringa nel file System.out.println("Scrittura completata con successo."); // conferma a console } catch (IOException e) { // intercetta eventuali errori di scrittura System.err.println("Errore durante la scrittura del file: " + e.getMessage()); // stampa l'errore } } }
IMMAGINE 02 - BYTE STREAM VS CHARACTER STREAM VS BUFFERED
Byte Stream vs Character Stream vs Buffered - Matrioska Java Da byte grezzo → char UTF-8 → riga veloce = incapsulamento a strati 1. BYTE STREAM InputStream / OutputStream Grezzo, 8 bit socket.getInputStream() ritorna byte[]: 0x43 0x69 0x61 0x6F 0x43 0x69 0x61 0x6F Problema: byte ≠ carattere con UTF-8 é = 2 byte, 读 = 3 byte Se leggi byte a byte spacchi int b = in.read() // -1 = EOS Lento, 1 syscall per byte ❌ avvolge new InputStreamReader(is, UTF-8) 2. CHARACTER STREAM Reader / Writer + Charset Traduce byte → char new InputStreamReader(is,"UTF-8") byte → char corretto C i a o é → 5 char non più 0x43 0x69... Vantaggio: é, 中, 😀 gestiti OK ✅ Non spacchi più UTF-8 char c = reader.read() Ancora lento: 1 char = 1 syscall Manca readLine() ❌ avvolge new BufferedReader(reader) 3. BUFFERED ★ BufferedReader / Writer Buffer 8KB + metodi utili new BufferedReader(reader) legge 8192 byte in 1 volta! Buffer [8192 char] ██████░░░░ 1 syscall = 8K Vantaggio: String line = br.readLine() ✅ Blocca fino a \n, veloce 100x while((line=br.readLine())!=null) Usa sempre questo in socket! MATRIOSKA - Incapsulamento a strati (decorator pattern) BufferedReader [readLine() + buffer 8K] ← tu usi questo InputStreamReader [byte → char UTF-8] InputStream [byte grezzo da socket.getInputStream()] Codice finale: new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8")) → 1 riga = 3 strati!

4. Gestione delle eccezioni

In Java, le eccezioni si gestiscono tramite quattro parole chiave: try-catch intercetta gli errori che si verificano nel blocco try; finally esegue del codice indipendentemente dal fatto che si sia verificata un'eccezione o meno; throw genera manualmente un'eccezione; throws dichiara, nella firma di un metodo, che quel metodo può generare un'eccezione controllata, lasciando a chi lo chiama la responsabilità di gestirla.

⚠️ Errore comune - throw vs throws

throw = azione, lanci un'eccezione dentro il metodo (throw new Exception()). throws = dichiarazione, dici nella firma "questo metodo PUÒ lanciare eccezione" (void metodo() throws IOException). Un piccolo errore in questa riga può cambiare completamente il comportamento del programma: dimenticare un throws nella firma di un metodo che lancia un'eccezione controllata è uno degli errori di compilazione più comuni.

4.1 Esempio — eccezione personalizzata

JAVA - ECCEZIONE PERSONALIZZATA
class EtaNonValidaException extends Exception { // eccezione personalizzata: estende Exception public EtaNonValidaException(String message) { // costruttore con messaggio descrittivo super(message); // passa messaggio alla superclasse } } public class VerificaEta { // classe che usa l'eccezione personalizzata public static void main(String[] args) { try { verificaEta(16); // prova età non valida } catch (EtaNonValidaException e) { System.err.println("Errore: " + e.getMessage()); } } // dichiara "throws" perché può lanciare eccezione controllata public static void verificaEta(int eta) throws EtaNonValidaException { if (eta < 18) throw new EtaNonValidaException("Età non valida, devi essere maggiorenne."); System.out.println("Età valida."); } }
🔧 Quando creare eccezioni custom

Non per ogni errore. Create eccezione custom quando volete distinguere un errore di dominio (età non valida, voto fuori range, saldo insufficiente) da un errore tecnico (IOException). Così chi chiama il metodo può fare catch(EtaNonValidaException) diverso da catch(IOException). Nei socket useremo principalmente IOException, ma il meccanismo è identico.

IMMAGINE 03 - GERARCHIA ECCEZIONI JAVA
ECCEZIONI JAVA Checked = devi gestire (try/catch o throws) | Unchecked = Runtime, non obbligatorio Checked (obbligatorie) Unchecked (Runtime) Error (non gestire) Throwable radice di tutto Error OutOfMemoryError, StackOverflow NON catturare ❌ Exception la super di tutto ciò che devi gestire CHECKED ★ IOException I/O fallito: file, rete, socket throws obbligatorio! Socket → qui 90% eccezioni RuntimeException Unchecked → bug tuo NullPointer, IllegalArg non obbligatorio gestirle Custom Exception extends Exception es: PortaOccupataException tu decidi checked/unchecked UnknownHostException DNS fallito: new Socket("hostinesistente") extends IOException → catch specifico prima di IOException! SocketException Connessione resettata / chiusa Connection reset, Broken pipe client chiude mentre scrivi BindException Porta già occupata Address already in use: 8080 → cambia porta o kill processo NullPointerException socket == null → NPE fix: if(socket!=null) ... REGOLA D'ORO CATCH - Ordine dal più specifico al più generico! + try-with-resources try (Socket s = new Socket(host, 8080); // auto-close in Java 7+ BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()))) { } catch (UnknownHostException e) { // 1° specifico: DNS → suggerisci controlla host } catch (IOException e) { // 2° generico I/O → log + retry } // No need close()! try-with-resources chiude da solo ⚠ Se metti IOException prima di UnknownHostException → errore compilazione: unreachable code

5. Realizzazione di Client e Server TCP

Vediamo ora come utilizzare le classi di Java per implementare un server e un client TCP, esplorando tre classi fondamentali: InetAddress, ServerSocket e Socket.

5.1 Classe InetAddress

La classe InetAddress rappresenta gli indirizzi IP e mette a disposizione diversi metodi per astrarre l'indirizzo, consentendo di lavorare sia con indirizzi numerici sia con nomi host. InetAddress non ha costruttori pubblici: per crearne un'istanza si usano metodi statici, cioè metodi che appartengono alla classe e non a una sua istanza specifica, e che quindi si possono chiamare senza creare prima un oggetto.

MetodoCosa restituisce
static InetAddress getByName(String host)L'indirizzo dell'host specificato (nome o IP); se null, restituisce macchina locale
static InetAddress getLocalHost()L'indirizzo della macchina locale; se non registrata in DNS o dietro firewall, 127.0.0.1
String getHostName()Il nome host corrispondente all'IP, via DNS se necessario
String getHostAddress()L'indirizzo IP numerico come stringa
byte[] getAddress()L'IP come array di byte, high byte first
JAVA - INETADDRESS ESEMPIO
try { // richiede al DNS l'indirizzo IP del nome host InetAddress inetAddress = InetAddress.getByName("www.google.com"); System.out.println("Indirizzo IP: " + inetAddress.getHostAddress()); // IP numerico // IP della macchina locale InetAddress localHost = InetAddress.getLocalHost(); System.out.println("Indirizzo locale: " + localHost.getHostAddress()); } catch (UnknownHostException e) { System.err.println("Host non trovato."); }
💡 Tips - Loopback e getLocalHost

Se getLocalHost() vi restituisce 127.0.0.1 invece del vostro IP LAN, non è un bug: siete dietro NAT o il nome host non è registrato in DNS. Usate InetAddress.getByName("192.168.1.10") con il vostro IP esplicito, o lasciate localhost per test locali. In laboratorio useremo sempre localhost per semplicità.

5.2 Classe ServerSocket

La classe ServerSocket viene utilizzata per creare un server che accetta connessioni da parte dei client: è un punto di ascolto che rimane in attesa di richieste su una porta specificata. Il costruttore richiede come parametro il numero di porta su cui il server sarà in ascolto:

JAVA - CREAZIONE SERVERSOCKET
ServerSocket server = new ServerSocket(1234); // server in ascolto sulla porta 1234

Il metodo principale usato con ServerSocket è accept(), che blocca l'esecuzione finché un client non si connette. Quando ciò accade, accept() restituisce un oggetto Socket che rappresenta il canale di comunicazione con quel client:

JAVA - ACCEPT BLOCCANTE
try { ServerSocket server = new ServerSocket(1234); // ServerSocket porta 1234 Socket client = server.accept(); // attende (bloccante) connessione client System.out.println("Connessione stabilita con: " + client.getInetAddress()); server.close(); // chiude: non accetterà più connessioni } catch (IOException e) { System.err.println("Errore nel server: " + e.getMessage()); }
💬 Nota docente - accept() è bloccante, capite cosa significa?

Quando chiamate accept() il vostro programma si ferma lì. Letteralmente. Non va avanti finché non arriva un client. È come stare alla cassa del supermercato ad aspettare il prossimo cliente: non potete fare altro finché non arriva. Per questo nei server veri accept() sta in un loop infinito e ogni client viene gestito in un thread separato — lo vedremo nella prossima lezione sui multi-client. Se lanciate il server e non succede nulla, è normale: sta aspettando in accept().

IMMAGINE 04 - SERVERSOCKET ACCEPT BLOCCANTE
ServerSocket.accept() BLOCCANTE Timeline: perché il server resta in attesa e serve Thread per client 🔒 1 ServerSocket server = new ServerSocket(1234); Lega porta 1234 → stato LISTEN • fd originale resta aperto 📞 2 Socket client = server.accept(); // ★ BLOCCANTE Il thread si sospende qui • CPU 0% • attende SYN dal client ⏳ FASE DI BLOCCO — Thread sospeso da JVM / OS 🔒 ⏰ 0.5s → 10s → ∞ se nessun client CPU 0% • wait() in kernel • non consuma Icona lucchetto = accept in attesa CLIENT ARRIVA → new Socket("localhost",1234) → SYN 4 🔓 SBLOCCO — accept() ritorna! Socket clientSocket = nuovo fd (es. 5) — serverSocket resta fd 3 su :1234 ORIGINALE serverSocket :1234 ancora LISTEN • accetta altri NUOVO — per questo client clientSocket fd 5 ESTABLISHED → read/write ⚠️ Perché serve Thread: se nessun client, server bloccato per sempre su accept() → while(true){ Socket c = ss.accept(); new Thread(() -> gestisci(c)).start(); } // ogni client thread dedicato Alternativa: ss.setSoTimeout(5000) → lancia SocketTimeoutException dopo 5s

5.3 Classe Socket

La classe Socket rappresenta un canale di comunicazione tra un client e un server. Un oggetto Socket viene creato lato client quando ci si connette a un server tramite indirizzo e porta:

JAVA - CREAZIONE SOCKET CLIENT
Socket clientSocket = new Socket("localhost", 1234); // connessione a localhost:1234

Una volta connesso, il client può inviare e ricevere dati tramite gli stream di input e output associati al socket:

JAVA - INVIO DATI CON PRINTWRITER
try { Socket socket = new Socket("localhost", 1234); // connessione al server OutputStream output = socket.getOutputStream(); // flusso byte grezzo PrintWriter writer = new PrintWriter(output, true); // wrapper con autoflush writer.println("Ciao, server!"); // invia riga socket.close(); } catch (IOException e) { System.err.println("Errore nel client: " + e.getMessage()); }

Perché usare stream bufferizzati

Quando si comunica attraverso connessioni TCP, i dati vengono suddivisi in pacchetti IP: per questo è consigliabile utilizzare stream bufferizzati, evitando così pacchetti contenenti poche informazioni ciascuno. La definizione tipica dei due stream, di input e output, che un server usa verso un client, è la seguente:

JAVA - STREAM BUFFERIZZATI SOCKET
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); DataOutputStream out = new DataOutputStream(client.getOutputStream()); // oppure più comune per testo: PrintWriter out = new PrintWriter(client.getOutputStream(), true);
📖 Catena stream - Spiegazione passo passo

client.getInputStream() → flusso byte grezzi dalla connessione. new InputStreamReader(...) → adattatore byte→caratteri UTF-8. new BufferedReader(...) → buffer + metodo readLine(). Matrioska: ogni livello aggiunge funzionalità. Dopo questa definizione, l'uso diventa trasparente: readLine() e println().

Nell'esempio seguente che ho testato in laboratorio:

  • Il server si aspetta di ricevere del testo dal client.
  • Ogni volta che il client invia una linea di testo e termina con nuova linea (\n), il BufferedReader legge quella linea e il server la stampa.
JAVA - LETTURA RIGA DA CLIENT
import java.io.*; // per BufferedReader, InputStreamReader import java.net.*; // per Socket // BufferedReader per leggere dati dal client riga per riga BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); String line = in.readLine(); // bloccante finché non arriva \n System.out.println("Messaggio dal client: " + line);

6. Primo Esercizio client e server che si scambiano un messaggio

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

🎯 Metodo di lavoro - Come leggere il codice

Ho preferito mostrare anche il risultato dell'esecuzione più avanti, dopo il codice completo, così puoi verificare che il tuo output combaci esattamente con il mio prima di passare oltre. Leggete prima l'algoritmo in italiano, poi il codice Java commentato riga per riga. Se capite l'algoritmo, il codice è solo traduzione.

L'algoritmo del server, passo per passo

  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
  4. Nel main, dentro un blocco try-catch per gestire le eccezioni: creo un ServerSocket in ascolto sulla porta 12345 e stampo un messaggio di conferma, accetto una connessione e stampo l'IP del client, creo gli stream di input/output per ricevere e inviare dati, ricevo il messaggio dal client, rispondo al client, chiudo tutte le risorse aperte
📌 Nota struttura

In questo primo esempio realizziamo tutto dentro il solo metodo main. Per rendere il codice più leggibile, riusabile e ben strutturato, più avanti estrarremo la logica nei metodi attendi() e comunica(). È normale iniziare semplice.

Server.java (commentato riga per riga)

JAVA - SERVER.JAVA COMPLETO
// Server.java import java.io.*; // classi per lettura/scrittura (BufferedReader, PrintWriter...) import java.net.*; // classi per la rete (ServerSocket, Socket...) public class Server { // dichiarazione classe pubblica Server public static void main(String[] args) { // punto ingresso programma try { // blocco gestione eccezioni I/O // ServerSocket: OS inizia ad ascoltare su porta 12345 ServerSocket serverSocket = new ServerSocket(12345); System.out.println("Server in ascolto sulla porta 12345..."); // accept() blocca finché client non si connette; // clientSocket = canale dedicato a QUEL client Socket clientSocket = serverSocket.accept(); System.out.println("Connessione accettata da " + clientSocket.getInetAddress()); // input: byte dal client → char → buffer BufferedReader input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); // output: testo verso client, true = autoflush a ogni println PrintWriter output = new PrintWriter(clientSocket.getOutputStream(), true); // legge riga inviata dal client (bloccante) String clientMessage = input.readLine(); System.out.println("Messaggio dal client: " + clientMessage); // costruisce risposta con IP client e messaggio ricevuto String response = "Risposta dal server: Ciao " + clientSocket.getInetAddress().getHostAddress() + ", ho ricevuto da te questo messaggio: " + clientMessage; output.println(response); // invia risposta // chiude in ordine risorse input.close(); output.close(); clientSocket.close(); serverSocket.close(); } catch (IOException e) { System.err.println("Errore: " + e.getMessage()); } } }

Client.java (commentato riga per riga)

Il client si connette al server sulla porta 12345, invia il messaggio "Ciao, server!" e riceve la risposta.

JAVA - CLIENT.JAVA COMPLETO
// Client.java import java.io.*; // classi per lettura/scrittura import java.net.*; // classi per la rete public class Client { // dichiarazione classe pubblica Client public static void main(String[] args) { try { // connessione a localhost:12345 Socket socket = new Socket("localhost", 12345); // input per leggere risposta server BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); // output per inviare testo al server, autoflush attivo PrintWriter output = new PrintWriter(socket.getOutputStream(), true); String message = "Ciao, server!"; // messaggio da inviare output.println(message); // invia System.out.println("Messaggio inviato al server: " + message); String response = input.readLine(); // attende risposta server System.out.println("Risposta dal server: " + response); // chiude risorse input.close(); output.close(); socket.close(); } catch (IOException e) { System.err.println("Errore: " + e.getMessage()); } } }
🔍 Cosa succede, spiegato per blocchi

Server.java
(1) creazione ServerSocket porta 12345 + conferma console
(2) accept() → Socket dedicato a quel client
(3) comunicazione: BufferedReader per leggere, PrintWriter per rispondere con IP client
(4) chiusura ordinata risorse

Client.java
(1) Socket("localhost",12345) connessione
(2) invio "Ciao, server!" via output, ricezione risposta via input
(3) chiusura

6.1 Output in console

OUTPUT - SERVER CONSOLE
Server in ascolto sulla porta 12345... Connessione accettata da /127.0.0.1 Messaggio dal client: Ciao, server!
OUTPUT - CLIENT CONSOLE
Messaggio inviato al server: Ciao, server! Risposta dal server: Risposta dal server: Ciao 127.0.0.1, ho ricevuto da te questo messaggio: Ciao, server!
✅ Verifica - Il tuo output combacia?

Se il vostro output è identico al mio, avete fatto tutto giusto. Se il server resta fermo su "in ascolto..." è normale: sta in accept() aspettando. Lanciate il client in un altro terminale. Se vedete "Connection refused" → avete dimenticato di lanciare prima il server. Se vedete "Address already in use" → porta 12345 già occupata da altro processo, cambiate porta o killate processo precedente.

6.2 Separare la logica in metodi: attendi() e comunica()

Inizialmente avevo impostato il codice tutto dentro il main, ed è la scelta più naturale quando si scrive un primo esempio: ma per rendere il codice più leggibile, riusabile e strutturato correttamente conviene estrarre la logica del server nei metodi attendi() e comunica(), come mostrato di seguito.

JAVA - SERVER CON METODI ATTENDI() E COMUNICA()
import java.io.*; // classi per lettura/scrittura import java.net.*; // classi per la rete public class Server { // classe Server public static void main(String[] args) { try { // crea ServerSocket porta 12345 ServerSocket serverSocket = new ServerSocket(12345); System.out.println("Server in ascolto sulla porta 12345..."); Socket clientSocket = attendi(serverSocket); // delega attesa connessione comunica(clientSocket); // delega comunicazione } catch (IOException e) { System.err.println("Errore: " + e.getMessage()); } } // dichiara throws IOException: chi chiama gestisce eccezione public static Socket attendi(ServerSocket serverSocket) throws IOException { return serverSocket.accept(); // blocca finché connessione, poi restituisce } public static void comunica(Socket clientSocket) throws IOException { 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); // qui NON chiudiamo serverSocket, responsabilità del main input.close(); output.close(); clientSocket.close(); } }
🔧 Perché throws invece di try-catch dentro il metodo?

Nota che attendi() e comunica() dichiarano throws IOException invece di gestire l'eccezione al proprio interno: è una scelta di progettazione deliberata, che lascia al chiamante — in questo caso il main — la responsabilità di decidere come reagire a un errore. È lo stesso principio, applicato in piccolo, che ritroverai in quasi ogni libreria Java ben progettata.

💬 Nota docente - Punto di arrivo della lezione

Dopo alcune prove ho preferito lasciarti proprio questa versione con i metodi separati come punto di arrivo della lezione, perché è la struttura più vicina a quella che userai davvero nei prossimi laboratori — a partire dal prossimo, in cui proveremo a gestire più client contemporaneamente. Se oggi hai capito main → attendi() → comunica(), domani aggiungiamo un while(true) + thread e hai un server multi-client.

7. 📌 Concetti chiave

  • Il package java.net fornisce le classi per la comunicazione di rete (InetAddress, Socket, ServerSocket, URL); il package java.io fornisce le classi per leggere e scrivere dati, inclusi gli stream bufferizzati che ottimizzano le prestazioni.
  • Un package è un contenitore logico di classi; si dichiara con package e si importa con import. Evita conflitti di nomi e migliora modularità.
  • Gli stream si organizzano a strati: byte stream (InputStream) → character stream (InputStreamReader) → buffered stream (BufferedReader con readLine). Matrioska di funzionalità.
  • Le eccezioni si gestiscono con try/catch/finally, si generano con throw e si dichiarano nella firma di un metodo con throws; è possibile creare eccezioni personalizzate estendendo Exception.
  • InetAddress rappresenta un indirizzo IP e si istanzia solo tramite metodi statici come getByName() e getLocalHost().
  • ServerSocket si mette in ascolto su una porta e usa accept() (bloccante) per accettare una connessione, restituendo un Socket dedicato a quel client.
  • Per comunicare in modo efficiente si usano stream bufferizzati: InputStream → InputStreamReader → BufferedReader per leggere, PrintWriter con autoflush per scrivere. readLine() legge fino a \n.
  • Separare la logica in metodi dedicati (come attendi() e comunica()) rende il codice più leggibile, testabile e riusabile rispetto a tutto concentrato nel main. throws IOException delega gestione errore al chiamante.
🐙 Laboratori Pratici

Vuoi passare all'azione? Accedi alla repository ufficiale GitHub del corso per scaricare il file del laboratorio pratico

🧪 Esercizi Socket TCP GitHub →

Vuoi consultare la sezione dedicata ai laboratori sul sito? Accedi cliccando il pulsante qui sotto.

📊 Sezione dedicata ai Laboratori

Vuoi consultare la sezione dedicata alle guide sul sito? Accedi cliccando il pulsante qui sotto.

🧪 Guida preparazione ambiente di lavoro →

📚 Prossimo passo - Server multi-client

Nella prossima lezione faremo evolvere questo server: da un solo client a N client contemporanei con thread e while(true). Se oggi il codice con attendi() e comunica() ti è chiaro, domani aggiungiamo new Thread() e il tuo server diventa davvero professionale. Esercizio: modifica Client.java per inviare il tuo nome invece di "Ciao, server!" e fai rispondere il server con "Ciao [tuo nome], IP [tuo IP]". Se ci riesci, hai capito tutto.