Il problema: trasmettitore veloce, ricevitore lento
Immagina un server che trasferisce dati a 1 Gbps verso un client che ha un processore lento e un’applicazione che legge i dati dal buffer ogni 100 ms. Il buffer del ricevitore si riempie rapidamente. Se il mittente continua a inviare senza sapere quanto spazio è disponibile, i dati in eccesso vengono scartati e devono essere ritrasmessi — spreco di banda e aumento della latenza.
Il controllo del flusso è il meccanismo che impone al mittente di non superare la capacità di ricezione dell’altro endpoint. Diverso dal controllo della congestione (che riguarda la rete): qui si tratta di proteggere il buffer del ricevitore.
Receive Window (rwnd)
Il campo Window Size nell’header TCP (16 bit, estendibile con Window Scale option fino a 1 GB) comunica al mittente quanti byte può ancora inviare senza ricevere un ACK:
rwnd = spazio_buffer_ricevitore_libero Regola del mittente: LastByteSent − LastByteAcked ≤ rwnd Dove: LastByteSent = ultimo byte inviato LastByteAcked = ultimo byte riscontrato con ACK La differenza è il numero di byte "in volo" (inviati ma non ancora confermati)
Il ricevitore aggiorna rwnd in ogni ACK che invia. Man mano che l’applicazione legge i dati dal buffer, il buffer si libera e rwnd aumenta — il mittente può inviare di più. Se l’applicazione è lenta a consumare i dati, rwnd si riduce — il mittente rallenta automaticamente.
Buffer ricevitore: [████████████░░░░░░░░]
↑ dati non letti ↑ spazio libero = rwnd
Dopo che l'applicazione legge:
Buffer ricevitore: [████░░░░░░░░░░░░░░░░]
↑ rwnd aumenta → ACK con window più grande
Window Scale Option
Il campo Window Size è 16 bit → massimo 65535 byte. Per reti ad alta latenza e banda elevata (es. fibra intercontinentale, satellitare) questo è insufficiente. L’opzione Window Scale (RFC 7323) aggiunge un moltiplicatore (da 0 a 14): rwnd_effettiva = rwnd × 2^shift_count. Viene negoziata durante il three-way handshake.
Zero Window e Window Probe
Quando il buffer del ricevitore è completamente pieno, il ricevitore invia un ACK con rwnd = 0: Zero Window. Il mittente si ferma — non può inviare dati.
Dopo Zero Window, il mittente attende. Quando il buffer si libera, il ricevitore invia un Window Update con rwnd > 0. Ma se questo update viene perso (IP è best-effort!), il mittente aspetta per sempre e il ricevitore aspetta dati che non arrivano: deadlock.
La soluzione è il Window Probe: il mittente avvia un timer (Persist Timer). Alla scadenza, invia un segmento da 1 byte per “stimolare” il ricevitore a inviare un ACK aggiornato con il valore corrente di rwnd. Il Persist Timer usa backoff esponenziale per evitare di inondare la rete.
Zero Window timeline: Ricevitore: rwnd=0 → invia ACK(rwnd=0) Mittente: si ferma, avvia Persist Timer [Persist Timer scade] Mittente: invia Window Probe (1 byte) Ricevitore: buffer ancora pieno → ACK(rwnd=0) [Persist Timer scade, backoff] Mittente: invia Window Probe (1 byte) Ricevitore: buffer liberato → ACK(rwnd=4096) ← mittente riprende
Silly Window Syndrome
Un altro problema sorge quando mittente o ricevitore lavorano con porzioni di dati molto piccole — ad esempio un’applicazione che scrive 1 byte alla volta nel socket TCP. Inviare segmenti TCP con 1 byte di payload è estremamente inefficiente: header TCP (20B) + IP (20B) = 40 byte di overhead per 1 byte di dato. Efficienza = 1/41 ≈ 2.4%.
Algoritmo di Nagle (RFC 896) — lato mittente
if dati_disponibili ≥ MSS:
invia subito un segmento MSS-sized
else if tutti_gli_ACK_precedenti_ricevuti:
invia subito (nessun segmento in volo)
else:
accumula i dati nel buffer, attendi ACK
(invia solo quando hai MSS byte o ACK arriva)In pratica: il primo segmento parte subito, quelli successivi vengono aggregati finché o si raggiunge MSS o arriva l’ACK del segmento precedente. Riduce drasticamente il numero di piccoli segmenti.
Le applicazioni interattive a bassa latenza (SSH, terminali remoti, giochi online, tastiere su rete) devono inviare ogni keystroke immediatamente. L’algoritmo di Nagle introdurrebbe un ritardo inaccettabile. Si disabilita con il socket option TCP_NODELAY (setsockopt). Nginx, Redis e la maggior parte dei server ad alte performance usano TCP_NODELAY.
Clark’s Solution — lato ricevitore
Il problema può emergere anche dal lato ricevitore: se il buffer si libera di soli pochi byte, il ricevitore potrebbe annunciare subito una piccola window — e il mittente invierebbe un piccolo segmento. Clark’s Solution: il ricevitore non aggiorna la window finché il buffer libero non raggiunge almeno min(MSS, metà del buffer totale). Questo evita di indurre il mittente a inviare piccoli segmenti.
| Problema | Lato | Soluzione |
|---|---|---|
| Applicazione mittente scrive dati piccoli | Mittente | Algoritmo di Nagle (RFC 896) |
| Applicazione ricevente legge dati piccoli (buffer quasi pieno) | Ricevitore | Clark’s Solution (ritarda window update) |
- Il controllo del flusso protegge il buffer del ricevitore (non la rete — quello è controllo della congestione)
- rwnd: campo Window Size nell’header TCP — indica quanti byte il mittente può ancora inviare. Regola:
LastByteSent − LastByteAcked ≤ rwnd - Zero Window: il mittente si ferma. Persist Timer + Window Probe evitano il deadlock se il Window Update viene perso
- Silly Window Syndrome: segmenti piccoli con overhead enorme — inefficiente
- Nagle: aggrega segmenti piccoli lato mittente. Si disabilita con
TCP_NODELAYper app interattive - Clark’s Solution: il ricevitore ritarda l’aggiornamento della window finché non c’è spazio sufficiente