Trasporto orientato alla connessione - TCP

Principi del trasferimento dati affidabile
...

A questo punto della trattazione, svilupperemo in modo complementare i lati mittente e ricevente di un protocollo di trasporto affidabile rendendolo via via più complesso.
Il lato mittente del protocollo di trasporto sarà invocato con rdt_send() mentre il lato ricevente con rdt_rcv(). rdt sta per reliable data transfer.

Quando un pacchetto raggiunge il lato ricevente del canale (di comunicazione affidabile) viene chiamata rdt_rcv(). Nel momento in cui rdt voglia consegnare i dati al livello superiore (applicazione) verrà utilizzata la chiamata deliver_data(). Spiegheremo solo il trasporto unidirezionale e non bidirezionale (full-duplex), che non è concettualmente più complesso. È comunque importante notare che i lati mittente e ricevente del protocollo rdt avranno la necessità di scambiarsi informazioni in entrambe le direzioni. Vedremo anche che oltre a scambiarsi pacchetti contenenti dati da trasferire, i due lati di rdt si scambieranno pacchetti di controllo utilizzando la chiamata udt_send() (UDT: unreliable data trasfer).

Costruzione di un protocollo affidabile
...

rdt 1.0
...

Vedremo le varie versioni di questo protocollo sotto forma di automi a stati finiti (FSM - finite state machine)
Pasted image 20230921113216.png
Lo stato del mittente è rappresentato dalla figura (a), mentre lo stato del ricevente è rappresentato dalla figura (b).

Panoramica sulla notazione
  • Freccia tratteggiata: stato iniziale della macchina
  • Testo sopra la linea continua: ciò che scatena l'evento- Testo sotto la linea continua: conseguenze dell'evento scatenato
  • Freccia continua: passaggio ad altro stato (in questo caso è lo stesso)
  • (lambda maiuscola): nessuna operazione

Lato mittente si attendono i pacchetti dal livello superiore tramite l'evento rdt_send() che:

  • crea un pacchetto packet = make_pkt(data)
  • e lo invia sul canale con udt_send(packet)

Lato ricevente si attendono i pacchetti dal livello sottostante tramite l'evento rdt_rcv(packet) che:

  • estrae i dati dal pacchetto con extract(packet, data)
  • e vengono consegnati al livello superiore con deliver_data(data)
Nota
  • Abbiamo supposto che il ricevente possa ricevere dati alla stessa velocità con cui vengono inviati (non è necessario un controllo del flusso)
  • Il canale è perfettamente affidabile: non c'è motivo per cui il ricevente debba fornire informazioni al mittente (feedback riguardo la ricezione dei pacchetti)
rdt 2.0
...

In questa versione del protocollo viene affrontato il problema del rilevamento dell'errore: se un pacchetto si corrompe durante il tragitto verso la destinazione, il ricevente se ne rende conto grazie al campo checksum che viene aggiunto al protocollo rdt2.0. Se il ricevente nota che il pacchetto è corrotto, lo comunica al mittente che lo re-invia. Il ricevente comunicherà al mittente di aver ricevuto il pacchetto con un feedback positivo (acknowledgement - ACK) o con un feedback negativo (NAK).
Pasted image 20230921114339.png
Il mittente (a), attende pacchetti dal livello superiore, quando essi arrivano viene prodotto un pacchetto (come in rdt1.0) e inviato via udt_send.
Il mittente, fatto ciò, passa all'altro stato e attende di ricevere un ACK o un NAK per il pacchetto appena inviato:

  • rdt_rcv(rcvpkt), riceve il pacchetto inviatogli dal ricevente, tale pacchetto viene passato alla funzione isNAK(rcvpkt) che controlla se esso è un NAK (notifica negativa), se è vero che è un NAK: il pacchetto creato nel primo stato della macchina viene inviato nuovamente (rimanendo in attesa di ACK per il nuovo pacchetto inviato);
  • se il mittente riceve feedback positivo (ACK) allora passa nuovamente allo stato in cui attende i pacchetti dal livello superiore.

Il ricevente (b), ha ancora un solo stato, inizialmente è in attesa di ricevere pacchetti dal livello sottostante (come per rdt1.0). Quando li riceve controlla se il pacchetto ricevuto è corrotto o no:

  • rdt_rcv(rcvpkt) riceve il pacchetto, esso viene passato a corrupt(rcvpkt). Se il risultato del controllo booleano è vero, come conseguenza si ha la creazione di un pacchetto di tipo NAK che viene inviato;
  • altrimenti se il pacchetto ricevuto, passato alla funzione notcorrupt(rcvpkt) dà come risultato booleano vero, come conseguenza viene creato un pacchetto di ACK e viene inviato.

Il protocollo rdt2.0 sembrerebbe funzionare, ma presenta ancora un grave problema: non è stato considerato il caso in cui i pacchetti ACK o NAK siano corrotti. Come minimo vanno aggiunti dei bit di checksum ai pacchetti ACK/NAK per dare la possibilità al mittente di riconoscere eventuali errori nei pacchetti. Il problema fondamentale riguarda come il protocollo debba comportarsi nel caso in cui riceve pacchetti di ACK/NAK corrotti. Vediamo due possibili soluzioni:

  • Aggiunta di un numero sufficiente di bit di checksum che consente al mittente non solo di rilevare il problema, ma anche di correggerlo. Questo risolve il problema per pacchetti corrotti, ma non perduti.
  • Un’altra soluzione è il re-invio da parte del mittente del pacchetto per cui è stato ricevuto un ACK/NAK alterato. Questo introduce un nuovo problema: nel canale saranno immessi dei pacchetti duplicati. Se il ricevente ha inviato un ACK che viene alterato, il mittente vedrà un ACK/NAK e invia di nuovo il pacchetto, ma il ricevente non sa se quel pacchetto contiene nuovi dati o se è la ritrasmissione (non necessaria, ad esempio) di dati già ricevuti.
    Una soluzione al problema esposto per ultimo è quella di aggiungere ai pacchetti inviati dal mittente un bit che contiene il numero di sequenza del pacchetto. Al ricevente basterà controllare il numero di sequenza per vedere se quel pacchetto è già stato ricevuto. Avendo ipotizzato che il canale non smarrisce pacchetti e che si tratta di un protocollo stop-and-wait, non serve includere nei pacchetti di ACK/NAK il numero di sequenza dei pacchetti cui si riferiscono i feedback. Il mittente sa che l’ACK/NAK ricevuto si riferisce all’ultimo pacchetto inviato (il motivo è che il mittente resta in attesa dopo aver inviato il pacchetto, quindi l'ACK che attende è quello per quel pacchetto appena inviato).

Macchina a stati finiti del mittente:
Pasted image 20230921121608.png
qui vediamo che dopo aver inviato il pacchetto, il mittente si mette in attesa per ricevere ACK o NAK.

  • se riceve NAK o se il pacchetto è corrotto, invia nuovamente il pacchetto con numero di sequenza 0;
  • altrimenti se riceve ACK e il pacchetto non è corrotto, invia il pacchetto numero di sequenza 1, se questo invio va a buon fine, allora il contatore dei pacchetti si azzera.
    Si noti che non è necessario avere numeri di sequenza maggiori di 1. Infatti se il mittente invia il pacchetto 0, attende la conferma per il pacchetto 0, dopo invierà il pacchetto 1 e attende di ricevere feedback per quel pacchetto. A questo punto il destinatario ha già ricevuto il pacchetto 1, quindi si può inviare nuovamente il pacchetto con numero di sequenza 0.

Macchina a stati finiti per il destinatario:
Pasted image 20230921115742.png
il destinatario dal primo stato, attende il pacchetto con numero di sequenza 0 per passare allo stato successivo (&& has_seq0(rcvpkt), inoltre verifica che il pacchetto non sia corrotto:

  • se il pacchetto ricevuto non è corrotto, estrae i dati dal pacchetto, li consegna al livello superiore e crea un pacchetto di ACK sndpkt = make_pkt(ACK, checksum) e lo invia;
  • altrimenti invia un pacchetto di NAK e resta in attesa del pacchetto 0 (sndpkt = make_pkt(NAK, checksum)).

Come abbiamo notato, il protocollo appena descritto, che chiamiamo rdt 2.1 usa acknowledgement positivi quando non riceve pacchetti alterati e acknowledgement negativi quando riceve pacchetti alterati.
Possiamo eliminare i NAK: il ricevente usa solo ACK quando riceve pacchetti non alterati, quando riceve un pacchetto alterato invia un ACK che riguarda il pacchetto precedente, il mittente si ritroverà 2 ACK per lo stesso pacchetto inviato, quindi sa che il ricevente non ha ricevuto il pacchetto successivo al pacchetto cui si riferiscono gli ACK duplicati ricevuti.

Esempio:

Mittente invia pacchetto 0.
Ricevente invia ACK per 0.

Mittente invia pacchetto 1.
Il destinatario ha ricevuto un pacchetto corrotto, quindi informa il mittente che l'ultimo pacchetto non corrotto ricevuto è il pacchetto 0. Quindi invia ACK per 0.
Il ricevente riceve ACK per 0 e dice "ma io ho inviato il pacchetto 1. Invio nuovamente il pacchetto 1".

Arricchiamo il rdt 2.1 con questa funzione, facendolo diventare rdt 2.2.
Macchina a stati finiti del mittente:
Pasted image 20230921120604.png
notiamo che qui il mittente, dopo aver inviato il pacchetto con numero di sequenza 0, attende solo per ACK.

  • Se riceve ACK per il pacchetto inviato prima (oppure un pacchetto corrotto), vuol dire che il destinatario non ha ancora ricevuto il pacchetto corrente non alterato, allora il mittente invia nuovamente il pacchetto con numero di sequenza corrente;
  • se riceve un pacchetto non corrotto e con l'ACK per il pacchetto corrente, allora passa nello stato successivo in cui aspetta il pacchetto con numero di sequenza successivo.

Macchina a stati finiti del destinatario:
Pasted image 20230921122333.png
Se si riceve pacchetto corrotto si invia ACK per il pacchetto precedente (l'ultimo ricevuto).

rdt 3.0
...

Sarà un protocollo per trasferimento dati affidabile con perdita di pacchetti ed errori sui bit.
Adesso assumiamo che sul canale i pacchetti possano andare persi.
Esistono molti approcci per la gestione della perdita di pacchetti, in questa sede il libro di testo di Kurose-Ross, assume che sia il mittente a gestire la perdita dei pacchetti.

Se i pacchetti spediti dal mittente vanno persi o che vanno persi gli ACK relativi ai pacchetti inviati, il mittente non sa se essi siano effettivamente andati smarriti o se siano vittima di un lungo ritardo. La soluzione è la ritrasmissione. Quanto tempo dovrebbe attendere il mittente prima di ritrasmettere una pacchetto? Almeno per il minimo tempo di andata e ritorno tra mittente e destinatario.

L’idea è di far in modo che il mittente ritrasmetta il pacchetto quando, in termini di tempo, sarebbe già dovuta arrivare la risposta da parte del ricevente. Tuttavia spedire la ritrasmissione tenendo conto di questo lasso tempo rallenterebbe di molto il canale di comunicazione. Per cui si utilizza un lasso di tempo probabile dopo il quale il pacchetto debba essere ritrasmesso. Ciò vuol dire che il mittente potrebbe rispedire un pacchetto che non sia mai stato smarrito (e che sia ancora in viaggio) o di cui non è stato smarrito l’ACK (e che sia ancora in viaggio). Il problema dei pacchetti duplicati è già risolto da rdt 2.2. Per il calcolo del tempo stimato viene aggiunto un nuovo componente al protocollo: un timer.
Il mittente dovrà essere in grado:

  1. Di inizializzare il contatore ogni volta che viene trasmesso un pacchetto (anche quando viene ritrasmesso)
  2. Di rispondere ad un interrupt (segnale) generato dal timer con l’azione appropriata
  3. Di fermare il contatore.

Macchina a stati finiti del mittente:
Pasted image 20230921123547.png
nell'attesa di ACK per il pacchetto inviato il mittente ci resta se:

  • riceve ACK per un pacchetto precedente a quello effettivamente inviato
  • riceve un pacchetto corrotto di ACK
  • si verifica timeout (scade il timer)
    nei primi due casi, le azioni intraprese sono nulle (), il che vuol dire che per ritrasmettere il pacchetto corrente si attende lo scadere del timer, così da poterlo anche riavviare.

La macchina a stati finiti del destinatario resta invariata rispetto a rdt 2.2.

Osservazioni rdt 3.0
...

Il protocollo rdt3.0 ha delle prestazioni che oggi sarebbero poco accettate. Il problema di prestazioni non buone risiede nel fatto rdt3.0 è di tipo stop-and-wait.
Vediamo un esempio per misurarne le prestazioni.
Supponiamo che ci siano due host: uno sulla costa orientale degli USA è uno sulla costa occidentale.
Il ritardo di propagazione di andate e ritorno (RTT) e di circa 30 ms.
Supponiamo che i  due sistemi siano connessi da un canale con tasso trasmissivo di (bit al secondo).
I pacchetti hanno dimensione di () inclusi dati e campo di intestazione.
d_t = \frac {8000bit} { 10^9 bps} = 8\mu sPasted image 20230921125024.png
Il primo bit del pacchetto viene trasmesso al tempo .
L’ultimo bit entra nel canale dopo .
Il pacchetto viaggia per .
L’ultimo bit giunge al ricevente dopo .
Per semplicità immaginiamo che i pacchetti di ACK sono estremamente piccoli e che il loro tempo di trasmissione sia trascurabile (istantaneo): gli ACK giungeranno a destinazione dopo Si noti che in un arco di il mittente ha inviato pacchetti per . Che è molto basso.
L’utilizzo (utilizzazione) del mittente è .
E si calcola con la formula: Il mittente è stato attivo solo per 2,7 centesimi dell’1 per cento del tempo.
La soluzione a questo problema è permettere al mittente di inviare più pacchetti invece di aspettare di ricevere gli ACK.
Pasted image 20230921125324.png
Se si consente di inviare tre pacchetti senza l’attesa di ACK, l’utilizzazione del mittente triplica. Questa tecnica è nota come pipelining. Cosa deve cambiare:

  1. L’intervallo dei numeri di pacchetti deve variare
  2. Il mittente e il ricevente devono poter memorizzare in un buffer più di un pacchetto. Il mittente deve memorizzare i pacchetti inviati per cui non è ancora stato ricevuto ACK.
  3. La quantità di numeri di sequenza necessari e di dimensione del buffer variano a seconda di come reagisce il protocollo ai pacchetti smarriti, alterati o troppo in ritardo. Ci sono due approcci: Go-Back-N e ripetizione selettiva.

Navigazione: