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-duplex | i due host possono trasmettere simultaneamente nelle due direzioni |
| punto-punto | la connessione associa esattamente due end-point; TCP non gestisce il multicast |
| end-to-end | lo stato della connessione è mantenuto nei sistemi terminali |
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.
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 flusso | Tempo per esaurire lo spazio di 4 Gbyte |
|---|---|
2 Mbit/s | 4,8 ore |
100 Mbit/s | 5,7 minuti |
1 Gbit/s | 34 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.
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 |
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.
«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.
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.
SYN=1, SeqN=x, ACK=0. A annuncia il proprio ISN x.SYN=1, ACK=1, SeqN=y, AckN=x+1. B conferma x e annuncia il proprio ISN y.SYN=0, ACK=1, SeqN=x+1, AckN=y+1. A conferma y; la sincronizzazione è completa.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.
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.
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.
| Perdita | Ciò che osserva l'host | Recupero |
|---|---|---|
| SYN di A | A non riceve SYN+ACK | time-out di A e nuovo invio del SYN |
| SYN+ACK di B | A non riceve la risposta; B non riceve l'ACK finale | time-out e ripetizione dello scambio di apertura |
| risposta assente in modo persistente | scadono più time-out consecutivi | backoff 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.
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.
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.
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.
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.
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.
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.
| 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 |
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».
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.
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 TCP | Ruolo indicato nelle slide |
|---|---|
| TIME_WAIT | mantiene lo stato per 2 MSL dopo la chiusura attiva |
| Retransmission Time-Out | temporizzatore di ritrasmissione |
| Keepalive Timer | temporizzatore keepalive della connessione |
| Persist Timer | temporizzatore persist della connessione |
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.
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.
| Processo | Primitiva | Funzione |
|---|---|---|
| server | socket | crea una nuova entità end-point |
bind | associa indirizzo IP e porta alla socket | |
listen | pone la socket in ascolto e crea la lista delle richieste | |
accept | attende e accetta la richiesta di un client | |
send | trasmette dati sulla connessione stabilita | |
receive | riceve dati dalla connessione stabilita | |
close | chiude la connessione e rilascia l'indirizzo della socket | |
| client | socket | crea una nuova entità end-point |
connect | blocca il client durante il tentativo e lo sblocca a connessione instaurata | |
send | trasmette dati sulla connessione stabilita | |
receive | riceve dati dalla connessione stabilita | |
close | chiude la connessione e rilascia l'indirizzo della socket |
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.
socket(), bind() e listen() creano e rendono raggiungibile l'end-point passivo.
accept() pone il server in attesa di una richiesta accodata.
connect() verso il socket address del server.
accept() restituisce la socket connessa e le parti possono eseguire send() e receive().
close() avvia la chiusura e rilascia l'indirizzo della socket.
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.
| Modello | Dopo accept | Effetto |
|---|---|---|
| iterativo | il server stesso esegue receive, send e close | le richieste sono servite in sequenza |
| concorrente | un worker process/thread gestisce la connessione | il server torna subito ad attendere altre richieste |
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()
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()
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.
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.
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.
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é 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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.