Parte III — Trasporto affidabile e TCP · Capitolo 10

TCP: gestione della connessione

~34 min di lettura4 widget interattivi

In questo capitolo

  1. Porte e connessioni TCP
  2. La numerazione dei dati
  3. L'apertura: il Three-Way Handshake
  4. Recupero dagli errori in apertura
  5. La chiusura della connessione
  6. La macchina a stati e TIME_WAIT
  7. Le primitive Berkeley socket
  8. Verifica le tue conoscenze

1. Porte e connessioni TCP

Per offrire affidabilità e controllo di flusso, il TCP è orientato alla connessione. Prima di trasferire il byte stream, i due sistemi terminali devono quindi creare e riconoscere uno stato condiviso; al termine devono rilasciarlo senza confondere dati vecchi e dati nuovi. Questa gestione è interamente end-to-end: riguarda i due host, non i nodi attraversati.

Il punto di accesso locale al TCP non è identificato dal solo numero di porta. La concatenazione fra indirizzo IP dell'host e numero di porta costituisce un end-point, detto anche socket address. La notazione delle slide è immediata:

end-point = indirizzo IP + porta

137.204.57.85:80

Una connessione è univocamente determinata dall'associazione di due end-point. Per esempio, la coppia 137.204.57.85:80 ↔ 192.168.10.99:10364 identifica una connessione diversa da quella che raggiunge lo stesso server e la stessa porta partendo da un altro indirizzo o da un'altra porta del client.

ProprietàSignificato operativo
full-duplexi due host possono trasmettere simultaneamente nelle due direzioni
punto-puntola connessione associa esattamente due end-point; TCP non gestisce il multicast
end-to-endlo stato della connessione è mantenuto nei sistemi terminali
Una connessione = coppia di end-point host client 192.168.10.99 :10364 :10365 host server 137.204.57.85 :80 connessione 1 connessione 2 Lo stesso end-point 137.204.57.85:80 partecipa a più connessioni. A distinguerle è la coppia completa: questo è il multiplexing delle connessioni.
Tavola 10.1 — Multiplexing su un end-point condiviso. La porta 80 del server non identifica da sola una connessione: ciascuna linea è distinta dalla coppia completa di socket address.
Idea chiave

Un end-point può appartenere a più connessioni sullo stesso host. Non c'è ambiguità perché l'identità della connessione comprende entrambi gli estremi: è la coppia, non il singolo indirizzo locale, a essere univoca.

2. La numerazione dei dati

Il TCP considera i dati applicativi come un flusso di byte, non come una sequenza di messaggi. Di conseguenza numera i singoli byte in uno spazio a 32 bit: il contatore avanza modulo 232, cioè modulo 4 Gbyte. Un segmento porta quindi il numero del primo byte trasportato, mentre la quantità di spazio consumata dipende dai byte contenuti.

La rete fra sorgente e destinazione non è un canale sequenziale. Un segmento può essere ritardato, duplicato e consegnato quando segmenti successivi sono già arrivati. Il riuso di un numero è sicuro soltanto quando non esistono più in rete vecchi segmenti con lo stesso numero di sequenza.

Velocità del flussoTempo per esaurire lo spazio di 4 Gbyte
2 Mbit/s4,8 ore
100 Mbit/s5,7 minuti
1 Gbit/s34 secondi

Questi valori mostrano perché i 32 bit non rendano il problema puramente teorico: aumentando la velocità, il contatore torna rapidamente sugli stessi valori. Il protocollo introduce perciò il Maximum Segment Lifetime (MSL), un limite al tempo di vita di un segmento. Il valore iniziale era 2 minuti; implementazioni più recenti usano 30 secondi.

Tre vincoli da tenere distinti

La numerazione risolve il riordinamento del byte stream, ma il suo riuso impone tre verifiche diverse. Tenerle separate evita di attribuire all'ISN un compito che appartiene al tempo di vita dei segmenti, o viceversa.

Problema Meccanismo richiamato
segmenti ritardati o duplicati nella stessa incarnazione numerazione dei singoli byte nello spazio a 32 bit
riuso di un numero mentre un vecchio segmento è ancora in rete limite temporale espresso dal Maximum Segment Lifetime
sovrapposizione fra incarnazioni successive degli stessi end-point ISN variabile e sincronizzazione durante l'apertura

Incarnazioni successive della stessa connessione

Il pericolo non è soltanto il giro completo del contatore dentro una connessione. Dopo un crash, gli stessi due end-point possono creare una nuova istanza, o incarnazione, mentre in rete sopravvivono segmenti ritardati dell'istanza precedente. Se i numeri coincidessero, un vecchio segmento potrebbe apparire valido nella nuova connessione.

All'apertura ciascun host sceglie allora un Initial Sequence Number (ISN). Per ridurre le sovrapposizioni l'ISN varia nel tempo, per esempio seguendo un contatore interno con un offset casuale. Considerando che un riavvio richiede più di un MSL, il rischio si riduce fortemente; resta però necessario che i due host sincronizzino i rispettivi ISN all'apertura.

Attenzione

«Numerazione a 32 bit» non significa che il TCP possa ignorare la storia della connessione. I numeri vengono riusati: MSL, ISN variabile e sincronizzazione servono insieme a impedire che un segmento di una vecchia incarnazione venga accettato come nuovo.

3. L'apertura: il Three-Way Handshake

Aprire una connessione non equivale a inviare un semplice avviso. La rete può perdere, duplicare o ritardare i pacchetti; inoltre entrambi gli host devono conoscere il numero iniziale scelto dall'altro. Il TCP risolve il problema con il Three-Way Handshake (TWH), uno scambio in tre passi che sincronizza entrambe le direzioni.

  1. A → B: SYN=1, SeqN=x, ACK=0. A annuncia il proprio ISN x.
  2. B → A: SYN=1, ACK=1, SeqN=y, AckN=x+1. B conferma x e annuncia il proprio ISN y.
  3. A → B: SYN=0, ACK=1, SeqN=x+1, AckN=y+1. A conferma y; la sincronizzazione è completa.
host A host B 1. SYN=1, SeqN=x, ACK=0 2. SYN=1, ACK=1, SeqN=y, AckN=x+1 3. SYN=0, ACK=1, SeqN=x+1, AckN=y+1 Il SYN occupa una posizione nello spazio di sequenza: dopo x si usa x+1, dopo y si usa y+1. Il solo ACK non occupa spazio di numerazione e non viene ritrasmesso autonomamente.
Tavola 10.2 — Il Three-Way Handshake. Il secondo messaggio conferma il numero di A e propone quello di B; il terzo prova a B che anche il suo numero iniziale è stato ricevuto.

Il primo segmento che porta dati da A usa SeqN=x+1, cioè lo stesso valore del campo AckN ricevuto nel passo precedente. Analogamente, il flusso di B comincia da y+1. Il SYN occupa una posizione nello spazio dei numeri di sequenza; un segmento che porta soltanto un ACK non la occupa e l'ACK non viene ritrasmesso come entità autonoma.

Per l'esame

Il TWH non serve genericamente a «vedere se l'altro risponde»: serve a sincronizzare due numeri di sequenza iniziali. Saper ricostruire i tre segmenti con x, y, x+1 e y+1 chiarisce anche perché siano necessari proprio tre passi.

4. Recupero dagli errori in apertura

Il TWH resta efficace quando i segmenti di apertura si perdono. Se il primo SYN non arriva a B, A non riceve alcuna risposta: allo scadere del time-out invia nuovamente il SYN con lo stesso numero iniziale. Se invece si perde il SYN+ACK, A ritrasmette il SYN e B risponde di nuovo con SYN+ACK.

PerditaCiò che osserva l'hostRecupero
SYN di AA non riceve SYN+ACKtime-out di A e nuovo invio del SYN
SYN+ACK di BA non riceve la risposta; B non riceve l'ACK finaletime-out e ripetizione dello scambio di apertura
risposta assente in modo persistentescadono più time-out consecutivibackoff esponenziale fino al numero massimo di tentativi

La cattura mostrata nelle slide rende visibili la scadenza del time-out del SYN, l'aumento progressivo dell'attesa tramite backoff esponenziale e infine un numero massimo di tentativi. Se il socket di destinazione non esiste, il rifiuto viene invece segnalato da un segmento con RST.

Leggere l'apertura in una cattura

Una traccia Wireshark permette di seguire il protocollo nello stesso ordine del diagramma: prima il SYN, poi il SYN+ACK, infine l'ACK. Le slide mettono in evidenza anche le opzioni presenti nei primi due segmenti, perché sono specificate proprio durante l'apertura.

  1. SYN — primo pacchetto di apertura della connessione e annuncio dell'ISN del client.
  2. SYN+ACK — risposta del server, conferma del primo ISN e annuncio del secondo.
  3. ACK — conferma finale che completa la sincronizzazione dei due numeri.
  4. assenza di risposta — scadenza del time-out, nuovo SYN e intervalli crescenti fino al massimo numero di tentativi.
  5. RST — rifiuto esplicito quando il socket di destinazione non esiste.

Perché il TWH è robusto

Lo schema resiste anche all'instaurazione contemporanea da parte dei due host e ignora pacchetti di apertura ritardatari. La robustezza discende dal fatto che ogni lato deve sia proporre il proprio numero sia confermare quello ricevuto: un vecchio SYN isolato non completa da solo la sincronizzazione bilaterale.

Nota del redattore

Il time-out di apertura introduce qui il principio, mentre il capitolo 11 studierà il Retransmission Time-Out usato durante il trasferimento. In entrambi i casi l'assenza della conferma è ciò che rende osservabile una perdita.

5. La chiusura della connessione

Il TCP cerca una chiusura ordinata, o soft release, che non perda dati. Il fatto che un'applicazione non abbia più nulla da inviare non implica che abbia finito di ricevere: nella direzione opposta possono esserci dati già in viaggio o ancora da trasmettere.

Per questo il rilascio è trattato in modalità simplex: le due direzioni sono chiuse indipendentemente. L'host che termina la propria trasmissione invia FIN=1 ed esegue la chiusura attiva. Quando riceve l'ACK del FIN considera chiusa la direzione in uscita; se l'ACK non arriva entro il tempo previsto, rilascia comunque la connessione. L'altro host è inizialmente il lato della chiusura passiva e può continuare a inviare finché non emette il proprio FIN.

host A host B FIN=1, SeqN=x, ACK=1, AckN=y ACK=1, AckN=x+1 A→B chiusa FIN=1, SeqN=w ACK=1, AckN=w+1 B→A chiusa fra i due FIN, B può ancora inviare dati verso A
Tavola 10.3 — Chiusura separata nelle due direzioni. Il primo FIN chiude soltanto il flusso A→B; il flusso B→A resta disponibile fino al secondo FIN.

Chiusura separata. A invia FIN e riceve ACK; la direzione A→B è chiusa. B continua a trasmettere e solo in seguito invia il proprio FIN, che A conferma. I due rilasci sono nettamente distinti nel tempo.

Chiusura congiunta. Quando B riceve il FIN di A, è già pronto a terminare anche la propria direzione. La risposta può quindi associare la conferma del FIN ricevuto alla chiusura B→A, riducendo la separazione fra le due procedure.

Chiusura simultanea. A e B decidono indipendentemente di chiudere e inviano FIN quasi nello stesso momento. Ciascuno riceve il FIN dell'altro e ne invia l'ACK: l'incrocio dei FIN non elimina la necessità di confermare entrambe le direzioni.

Perdita dell'ACK del FIN

Se l'ACK del primo FIN si perde, la sorgente del FIN attende il time-out e ripete il FIN. Il destinatario può quindi inviare nuovamente la conferma. In ogni caso, trascorso il tempo previsto senza ACK, la sorgente del FIN rilascia comunque la connessione: la procedura non può restare bloccata indefinitamente.

La sequenza logica del rilascio separato

Passo Evento Direzioni disponibili dopo il passo
1 A invia FIN=1 B può ancora trasmettere verso A
2 B conferma il FIN di A A considera chiusa la propria uscita
3 B termina i dati residui e invia il proprio FIN anche la direzione B→A entra in chiusura
4 A conferma il FIN di B entrambe le direzioni sono state rilasciate
Per l'esame

La parola decisiva è simplex. TCP offre una connessione full-duplex, ma la chiude come due flussi unidirezionali indipendenti. Un FIN significa «non invierò altri dati in questa direzione», non «da questo istante nessuno dei due può più trasmettere».

6. La macchina a stati e TIME_WAIT

L'apertura, il trasferimento e la chiusura possono essere descritti con una macchina a stati finiti. La macchina completa del TCP distingue numerosi casi; per seguire il percorso ordinario bastano gli stati principali riportati nel widget. La rappresentazione completa mostrata nelle slide è attribuita a Fall e Stevens, TCP/IP Illustrated, Vol. 1, 2nd Ed.

Lo stato TIME_WAIT

Al termine della chiusura attiva l'host non passa subito a CLOSED: resta in TIME_WAIT per un intervallo pari a 2 MSL. Durante questa attesa potrebbe ricevere nuovamente il FIN dell'altro host, per esempio perché l'ACK precedente si è perso, e può quindi confermarlo ancora.

Il secondo obiettivo è garantire l'estinzione dei segmenti appartenenti a incarnazioni precedenti. Dopo la chiusura, la stessa coppia di end-point potrebbe creare una nuova connessione e riusare numeri già impiegati; l'attesa di due massimi tempi di vita separa le due incarnazioni e impedisce ai vecchi segmenti di attraversare quel confine.

Timer TCPRuolo indicato nelle slide
TIME_WAITmantiene lo stato per 2 MSL dopo la chiusura attiva
Retransmission Time-Outtemporizzatore di ritrasmissione
Keepalive Timertemporizzatore keepalive della connessione
Persist Timertemporizzatore persist della connessione
Idea chiave

TIME_WAIT non è una chiusura «rimasta a metà». È una fase intenzionale della chiusura attiva: conserva per 2 MSL la memoria minima necessaria a confermare un FIN ritrasmesso e a proteggere la prossima incarnazione.

7. Le primitive Berkeley socket

Le Berkeley socket sono la tipica interfaccia fra applicazione e TCP nei sistemi operativi. Le primitive trasformano il ciclo della connessione in operazioni che ricordano l'apertura, l'uso e la chiusura di un file, ma i percorsi del server e del client non sono simmetrici.

ProcessoPrimitivaFunzione
serversocketcrea una nuova entità end-point
bindassocia indirizzo IP e porta alla socket
listenpone la socket in ascolto e crea la lista delle richieste
acceptattende e accetta la richiesta di un client
sendtrasmette dati sulla connessione stabilita
receivericeve dati dalla connessione stabilita
closechiude la connessione e rilascia l'indirizzo della socket
clientsocketcrea una nuova entità end-point
connectblocca il client durante il tentativo e lo sblocca a connessione instaurata
sendtrasmette dati sulla connessione stabilita
receivericeve dati dalla connessione stabilita
closechiude la connessione e rilascia l'indirizzo della socket

Workflow: apertura passiva e apertura attiva

Il server prepara l'apertura passiva con socket, bind e listen, poi si ferma in accept. Il client esegue l'apertura attiva con connect. Una volta instaurata la connessione, le due applicazioni alternano send e receive; infine chiamano close.

  1. preparazione del serversocket(), bind() e listen() creano e rendono raggiungibile l'end-point passivo.
  2. attesaaccept() pone il server in attesa di una richiesta accodata.
  3. apertura attiva — il client crea la propria socket e invoca connect() verso il socket address del server.
  4. connessione instaurataaccept() restituisce la socket connessa e le parti possono eseguire send() e receive().
  5. rilascioclose() avvia la chiusura e rilascia l'indirizzo della socket.

Server iterativo e server concorrente

Un server iterativo usa un ciclo infinito per servire più richieste successive, ma le gestisce in sequenza: completa il lavoro su una connessione prima di tornare ad accettarne un'altra. Un server concorrente esegue invece uno SPAWN: genera un processo o thread worker per ogni connessione e torna immediatamente in ascolto.

ModelloDopo acceptEffetto
iterativoil server stesso esegue receive, send e closele richieste sono servite in sequenza
concorrenteun worker process/thread gestisce la connessioneil server torna subito ad attendere altre richieste

Codice lato client: invio di una stringa

from socket import *
serverName = "hostname"
serverPort = 55000
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName,serverPort))
sentence = raw_input("Input lowercase sentence: ")
clientSocket.send(sentence)
modifiedSentence = clientSocket.recv(1024)
print "From Server:", modifiedSentence
clientSocket.close()

Codice lato server iterativo

from socket import *
serverPort = 55000
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(("",serverPort))
serverSocket.listen(5)
print "The server is ready to receive"
while 1:
    connectionSocket, addr = serverSocket.accept()
    sentence = connectionSocket.recv(1024)
    capitalizedSentence = sentence.upper()
    connectionSocket.send(capitalizedSentence)
    connectionSocket.close()

Codice lato server concorrente

from socket import *
from thread import *
# Multithread server - function to serve client requests
def clientthread(conn):
    sentence = conn.recv(1024)
    capitalizedSentence = sentence.upper()
    conn.send(capitalizedSentence)
    conn.close()
# Main program
serverPort = 55000
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(("",serverPort))
serverSocket.listen(5)
print "The server is ready to receive"
while 1:
    connectionSocket, addr = serverSocket.accept()
    start_new_thread(clientthread,(connectionSocket))

Il listato annotato permette di seguire le primitive del server concorrente una per una. Il codice conserva sintassi e nomi delle slide: l'obiettivo è riconoscere il workflow, dal socket di ascolto alla creazione del worker.

Nota del redattore

Le primitive rendono visibile il confine fra applicazione e trasporto. connect e accept espongono l'apertura; close avvia il rilascio ordinato; i dettagli dei segmenti SYN, ACK e FIN restano responsabilità del TCP.

Verifica le tue conoscenze

Che cos'è un end-point TCP e che cos'è una socket address?

Un end-point è la concatenazione fra l'indirizzo IP dell'host e un numero di porta; la sua rappresentazione, per esempio 137.204.57.85:80, è una socket address. Una connessione è determinata univocamente dall'associazione di due end-point.

Che cosa significano full-duplex, punto-punto ed end-to-end?

Full-duplex: i dati possono fluire nelle due direzioni. Punto-punto: una connessione associa due soli end-point e TCP non gestisce multicast. End-to-end: la connessione è mantenuta dai due host terminali. Un end-point può comunque essere condiviso da più connessioni mediante multiplexing.

Perché TCP numera i byte e quanto è grande lo spazio di numerazione?

Perché considera i dati applicativi come un byte stream, non come messaggi separati. Ogni byte occupa una posizione in uno spazio a 32 bit, usato come contatore modulo 232, cioè modulo 4 Gbyte.

Che cos'è MSL e perché condiziona il riuso dei numeri di sequenza?

Il Maximum Segment Lifetime è il massimo tempo di vita attribuito a un segmento: inizialmente 2 minuti, 30 secondi in implementazioni più recenti. Un numero può essere riusato in sicurezza soltanto dopo che i vecchi segmenti con lo stesso numero non esistono più in rete.

Quanto tempo serve a esaurire lo spazio di 4 Gbyte alle velocità indicate?

A 2 Mbit/s servono 4,8 ore; a 100 Mbit/s 5,7 minuti; a 1 Gbit/s appena 34 secondi. L'aumento della velocità rende concreto il problema del riuso dei numeri.

Perché l'ISN varia e perché deve essere sincronizzato?

L'ISN varia nel tempo, per esempio mediante contatore interno e offset casuale, per ridurre la sovrapposizione fra incarnazioni successive della stessa connessione. Deve essere sincronizzato perché ciascun host deve conoscere il numero iniziale scelto dall'altro per interpretare correttamente byte e conferme.

Quali sono i tre passi del Three-Way Handshake?

1) A invia SYN=1, SeqN=x, ACK=0. 2) B risponde SYN=1, ACK=1, SeqN=y, AckN=x+1. 3) A conclude con SYN=0, ACK=1, SeqN=x+1, AckN=y+1. Lo scambio sincronizza entrambi gli ISN.

Perché il primo segmento dati usa il numero indicato dall'ACK precedente?

Perché il SYN occupa una posizione nello spazio dei numeri. Dopo il SYN con SeqN=x, il primo byte usa x+1, proprio il valore richiesto da AckN=x+1. Un segmento che porta soltanto ACK non occupa invece spazio di numerazione e l'ACK non viene ritrasmesso autonomamente.

Come recupera TCP la perdita di un SYN o di un SYN+ACK?

Chi attende la risposta lascia scadere un time-out e ripete il segmento di apertura. In assenza persistente di risposta i tentativi seguono un backoff esponenziale e terminano raggiunto il numero massimo. Il TWH resiste inoltre ad aperture contemporanee e ignora pacchetti d'apertura ritardatari.

Che cosa significa soft release e perché la chiusura è simplex?

La soft release è una chiusura ordinata che cerca di non perdere dati. È simplex perché le due direzioni sono rilasciate indipendentemente: dopo il FIN di A, B può ancora inviare ad A finché non chiude a sua volta con un FIN.

Che differenza c'è fra chiusura separata, congiunta e simultanea?

Nella separata i due FIN sono distanti e fra essi una direzione resta attiva. Nella congiunta chi riceve il primo FIN è già pronto a chiudere anche la propria direzione. Nella simultanea entrambi inviano FIN indipendentemente e poi confermano il FIN ricevuto.

Che cosa accade se si perde l'ACK di un FIN?

La sorgente del FIN attende il time-out e può trasmettere nuovamente il FIN; il destinatario invia ancora l'ACK. Se la conferma continua a mancare, dopo il tempo previsto la sorgente del FIN rilascia comunque la connessione.

Perché TIME_WAIT dura 2 MSL?

Dopo la chiusura attiva, TIME_WAIT mantiene lo stato per 2 MSL. In questo intervallo l'host può confermare un FIN ritrasmesso per perdita dell'ACK e garantisce l'estinzione di segmenti di precedenti incarnazioni prima che la connessione venga ricreata.

Quali primitive usa un server Berkeley socket?

socket crea l'end-point, bind associa IP e porta, listen prepara la lista delle richieste, accept attende e accetta un client, send/receive scambiano dati e close chiude la connessione e rilascia l'indirizzo.

Quali primitive usa il client e che cosa fa connect?

Il client usa socket, connect, send/receive e close. connect blocca il processo mentre tenta l'apertura verso il server e lo sblocca quando la connessione è instaurata.

Che differenza c'è fra server iterativo e concorrente?

Il server iterativo serve le richieste una dopo l'altra nello stesso ciclo. Il server concorrente genera con uno SPAWN un processo o thread worker per ogni connessione e torna immediatamente in ascolto di nuove richieste.