W3docs

Socket Java

Apri connessioni TCP client in Java con la classe Socket: lettura, scrittura, timeout e un esempio completo.

Alla base di HTTP e di ogni altro protocollo applicativo si trova il socket: una connessione TCP bidirezionale grezza tra due endpoint. La classe java.net.Socket di Java rappresenta il lato client — la si crea, la si connette a un host e a una porta, e poi si leggono e scrivono byte attraverso normali InputStream/OutputStream. Questo è il livello di rete più basso a cui si ricorre quando si parla un protocollo personalizzato o si comunica con un servizio che non è HTTP.

Questo capitolo tratta ciò che un socket connesso mette a disposizione, i due modi per connettersi (e perché uno è più sicuro), come leggere e scrivere testo e byte grezzi, un esempio completo e funzionante di client che parla con un server, i timeout e i problemi che affliggono il codice reale, e dove i socket si collocano rispetto all'HttpClient di livello superiore che hai già incontrato.

Cosa offre un Socket

Un Socket connesso è una pipe con due stream:

  • socket.getOutputStream() — i byte scritti viaggiano verso l'altro endpoint.
  • socket.getInputStream() — i byte scritti dall'altro endpoint arrivano qui.

TCP garantisce che i byte arrivino in modo affidabile e in ordine. Non impone però alcuna struttura dei messaggi — un socket è uno stream di byte, non di messaggi. Il framing (dove finisce un messaggio e dove inizia il successivo) è compito tuo: usa newline, prefissi di lunghezza o un protocollo di livello superiore. Se hai bisogno che i confini dei messaggi siano preservati invece di uno stream, questo è il compito dei socket datagram (UDP), che sacrificano ordinamento e affidabilità a favore di pacchetti discreti.

Gli stream provengono direttamente dal sistema I/O di Java: getInputStream() restituisce un semplice InputStream e getOutputStream() un semplice OutputStream, quindi ogni wrapper che conosci — BufferedReader, PrintWriter, DataInputStream — funziona su un socket esattamente come funziona su un file.

Connessione

// Style 1: connect in the constructor
Socket socket = new Socket("example.com", 80);

// Style 2: create then connect with a timeout (preferred)
Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000);

La seconda forma consente di impostare un timeout di connessione — senza di esso, un host irraggiungibile può bloccare il thread per il valore predefinito del sistema operativo (spesso un minuto o più). Una volta connesso, avvolgi gli stream in lettori/scrittori bufferizzati e scegli esplicitamente un set di caratteri.

Lettura e scrittura di testo

var out = new PrintWriter(
        new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
var in = new BufferedReader(
        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

out.println("ping");                 // autoFlush=true sends it immediately
String reply = in.readLine();        // blocks until a line arrives

readLine() blocca finché non arrivano dati (o la fine dello stream) — la caratteristica saliente della classica API socket bloccante. Chiudi sempre il socket (try-with-resources) per rilasciare la connessione.

Lettura di byte grezzi

Il testo è comodo, ma molti protocolli sono binari — dati immagine, frame con prefisso di lunghezza, un formato wire personalizzato. In questi casi, salta i reader e lavora direttamente con gli stream:

try (Socket socket = new Socket()) {
    socket.connect(new InetSocketAddress("example.com", 80), 2000);
    OutputStream out = socket.getOutputStream();
    InputStream in = socket.getInputStream();

    out.write("PING".getBytes(StandardCharsets.UTF_8));
    out.flush();                          // streams are not auto-flushed

    byte[] buffer = new byte[4096];
    int n = in.read(buffer);              // bytes read, or -1 at end-of-stream
    if (n > 0) {
        String chunk = new String(buffer, 0, n, StandardCharsets.UTF_8);
        System.out.println(chunk);
    }
}

Due aspetti influenzano ogni lettura a livello di byte:

  • read(byte[]) restituisce quanti byte ha effettivamente ricevuto, il che non è necessariamente quanto hai richiesto. Una singola scrittura dall'altro lato può arrivare come più letture, e più scritture possono arrivare come una sola lettura — TCP coalizza e divide a piacere. Per ottenere un numero fisso di byte devi iterare in un ciclo, oppure avvolgere lo stream in DataInputStream e chiamare readFully().
  • Un valore di ritorno di -1 significa che il peer ha chiuso il suo lato (fine dello stream), non "nessun dato al momento." Questo è il segnale per smettere di leggere.

I timeout che contano

Un socket bloccante può bloccarsi in due posti distinti, e richiedono due timeout distinti:

Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000); // connect timeout
socket.setSoTimeout(5000);                                       // read timeout
  • Il timeout di connessione (il secondo argomento di connect) limita quanto tempo può impiegare l'handshake TCP. Senza di esso, un host irraggiungibile blocca il thread per il valore predefinito del sistema operativo — spesso un minuto o più.
  • Il timeout di lettura (setSoTimeout) limita quanto a lungo una singola chiamata read/readLine può bloccarsi in attesa di dati. Quando scade, la chiamata lancia una SocketTimeoutException senza chiudere il socket, così puoi decidere se riprovare o rinunciare. Senza di esso, un peer silenzioso ti blocca per sempre.

Il codice di rete reale dovrebbe impostare entrambi. I due errori per cui devi essere pronto sono UnknownHostException (il DNS non è riuscito a risolvere il nome) e la famiglia IOException (connessione rifiutata, reimpostata o scaduta); vedi eccezioni Java per i pattern di gestione.

Un esempio pratico: un client che parla con un echo server su loopback

Questo programma avvia un echo server usa-e-getta su un thread in background collegato all'indirizzo di loopback, poi — il vero focus del capitolo — connette un Socket client, invia una riga e legge la risposta. È una conversazione TCP completa all'interno di una singola JVM, senza rete esterna.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Il lato client si riduce a tre passaggi: costruire un Socket, chiamare connect() con un indirizzo e una porta, poi leggere e scrivere sui suoi stream. Tutto ciò che HTTP faceva per te nei capitoli precedenti — righe di richiesta, intestazioni, codici di stato — è sparito; un socket sposta byte grezzi e nient'altro.
  • connect(new InetSocketAddress(...), 2000) ha impostato un timeout di connessione di 2 secondi. Il costruttore senza timeout new Socket(host, port) si bloccherebbe sul valore predefinito del sistema operativo se l'host fosse irraggiungibile, quindi la forma con timeout esplicito è l'abitudine più sicura per qualsiasi rete reale.
  • Il protocollo era una convenzione, non una funzionalità: il client ha scritto una riga e ne ha letta una perché entrambi i lati concordavano che le righe sono messaggi. TCP ha consegnato uno stream di byte ordinato; il framing con newline che lo trasformava in "messaggi" era interamente definito dall'applicazione.
  • readLine() si è bloccato finché non è arrivata la risposta del server. Questo modello thread-per-connessione, blocca-fino-ai-dati è semplice e corretto, ed è esattamente il costo che i virtual thread mirano a rendere economico quando il numero di connessioni cresce.
  • getRemoteSocketAddress() e getLocalSocketAddress() hanno mostrato entrambi gli endpoint della connessione attiva — la porta di loopback del server e la porta locale assegnata dal sistema operativo al client. Ogni connessione TCP è identificata da quella coppia di endpoint. Il ServerSocket lato server costruisce il listener che ha accettato questa connessione.

Quando usare un socket grezzo

Un Socket è lo strumento giusto quando non esiste una libreria che già parla il tuo protocollo:

  • Stai implementando o consumando un protocollo TCP personalizzato (un server di gioco, un formato wire di un message broker, una porta admin basata su righe).
  • Devi comunicare con un servizio non-HTTP — SMTP, un protocollo testuale stile Redis, un server di linea legacy.

Per qualsiasi cosa che sia HTTP, non costruire richieste manualmente su un socket. Usa il moderno HttpClient, che ti offre connection pooling, redirect, HTTP/2 e TLS gratuitamente. La relazione è la stessa che esiste tra i byte stream grezzi e i reader di livello superiore costruiti su di essi: scendi al livello inferiore solo quando quello superiore non riesce a esprimere ciò di cui hai bisogno.

Esercizio

Pratica
Un client legge da un server usando 'socket.getInputStream()' avvolto in un 'BufferedReader', inviando un comando terminato da newline e aspettandosi una risposta terminata da newline. Occasionalmente una risposta è divisa in due segmenti TCP e il client la legge in modo errato. Qual è la comprensione corretta?
Un client legge da un server usando 'socket.getInputStream()' avvolto in un 'BufferedReader', inviando un comando terminato da newline e aspettandosi una risposta terminata da newline. Occasionalmente una risposta è divisa in due segmenti TCP e il client la legge in modo errato. Qual è la comprensione corretta?
Was this page helpful?