VI. Descrizione informale delle operazioni di rete
Presentiamo qui descrizioni narrative delle operazioni svolte durante le tre fasi principali dell’uso della rete: apertura, controllo di flusso e chiusura.
A. Apertura
Per stabilire una connessione per la trasmissione di dati, deve essere scambiata una coppia di RFC. Un RTS deve andare dal lato di ricezione al lato di invio, e un STR deve essere emesso dal lato di invio verso il lato di ricezione. Inoltre il lato di ricezione, nel suo RTS, deve specificare un numero di link. Queste RFC (RFC è un termine generico che comprende RTS e STR) possono essere emesse in qualsiasi sequenza temporale. Occorre inoltre prevedere l’accodamento delle chiamate in sospeso (cioè RFC che non sono state gestite dal programma utente). In questo modo, quando un utente ha finito con una connessione, può scegliere di esaminare la successiva chiamata in sospeso proveniente da un altro processo e decidere se accettare o rifiutare la richiesta di connessione. Sorge un problema perché l’utente potrebbe non scegliere di esaminare le sue chiamate in sospeso; queste servirebbero quindi soltanto a occupare spazio di coda nell’NCP. Più avanti verranno menzionate diverse soluzioni alternative a questo problema.
Utilizzando lo schema delle chiamate di sistema prototipali descritte sopra, immaginiamo almeno quattro sequenze temporali per ottenere una connessione aperta con successo:
- L’utente può emettere una LISTEN, indicando che è disposto a considerare la connessione con chiunque gli invii una RFC. Quando arriva una RFC, l’utente viene avvisato. L’utente decide quindi se desidera connettersi a questo socket ed emette una ACCEPT o una CLOSE in base a tale decisione. Una CLOSE ‘rifiuta’ la connessione, come discusso in "Chiusura". Una ACCEPT indica che è disposto a connettersi; viene emessa una RFC e la connessione diventa completamente aperta.
- Nell’elaborare la richiesta di LISTEN di un utente, l’NCP scopre che esiste una chiamata in sospeso per quel socket locale. L’utente viene avvisato immediatamente e può emettere ACCEPT o CLOSE, come sopra.
- L’utente emette una CONNECT, specificando un particolare socket remoto a cui desidera connettersi. Viene emessa una RFC. Se il processo remoto accetta la richiesta, risponde restituendo una RFC. Quando questa RFC di conferma viene ricevuta, la connessione è aperta.
- Ricevuta una CONNECT, l’NCP può scoprire che esiste una chiamata in sospeso dal socket remoto specificato verso il socket locale in questione. Viene emessa una RFC di conferma e la connessione è aperta.
In tutti i casi precedenti l’utente viene avvisato quando la connessione è aperta, ma il flusso di dati non può iniziare finché non viene allocato spazio di buffer e trasmesso un comando ALL.
Ciascuno di questi scenari di connessione verrà interrotto se arriva un CLS, come discusso in "Chiusura".
1. Code delle chiamate in sospeso
È essenziale implementare una qualche forma di accodamento delle RFC in sospeso. Un modo semplice per rendersene conto è esaminare una tipica sequenza LISTEN-CONNECT. Un lato emette una LISTEN, l’altro una CONNECT. Se la LISTEN viene emessa prima che arrivi la RFC proveniente dalla CONNECT remota, va tutto bene. Tuttavia, a causa della natura asincrona della rete, non possiamo mai garantire che si verifichi questa sequenza di eventi. Se le chiamate non vengono accodate e la RFC arriva prima che venga emessa la LISTEN, verrà rifiutata; se arriva dopo, verrà accettata. Ci troviamo quindi in una situazione estremamente ambigua.
A meno di disporre di uno spazio di coda infinito, è auspicabile un qualche meccanismo per ripulire le code dalle vecchie RFC che l’utente non si è mai preso la briga di esaminare. Un metodo ovvio ma informale consiste nell’annotare l’istante in cui ciascuna RFC viene inserita nella coda e poi rifiutare periodicamente tutte le RFC che hanno superato un limite di tempo arbitrario. Un’altra idea, che probabilmente dovrebbe essere inclusa nel contesto di qualsiasi schema, è che l’NCP invii un CLS su tutte le connessioni attive o chiamate in sospeso quando un utente esegue il logout o va in crash.
Lo schema utilizzato in questa descrizione può sembrare a prima vista poco intuitivo, ma riteniamo che sia più realistico di altre proposte. In sostanza, quando viene emessa una CONNECT, l’NCP presume che questo socket desideri comunicare con il socket remoto specificato e soltanto con quello. Elimina quindi dalla coda delle chiamate in sospeso tutte le RFC non corrispondenti, rispedendo dei CLS. Analogamente, quando la connessione è nello stato RFC-SENT (è stata emessa una CONNECT), tutte le RFC non corrispondenti vengono rifiutate. Se viene eseguita una sequenza LISTEN-ACCEPT o LISTEN-CLOSE, le restanti chiamate in sospeso non vengono rimosse dalla coda, nell’aspettativa che l’utente possa voler accettare queste richieste in futuro.
Sebbene quest’ultimo metodo possa sembrare arbitrario e/o inutilmente restrittivo, non abbiamo ancora escogitato uno scenario che sarebbe impedito da questo metodo, supponendo di avere a che fare con un programmatore competente (cioè uno che sia attento alle race condition e alla natura asincrona della rete). Naturalmente lo schema o gli schemi scelti da un particolare sito dipendono fortemente dall’implementazione; suggeriamo di prevedere l’accodamento delle RFC per un periodo di tempo almeno dello stesso ordine di grandezza di quello per cui vengono conservate nello schema di ripulitura su CONNECT menzionato sopra.
B. Controllo di flusso
Dati significativi possono fluire su una connessione solo quando questa è completamente aperta (cioè sono state scambiate due RFC e la chiusura non è iniziata). Assumiamo che gli NCP dispongano di un buffer per ricevere i dati in arrivo e che esista una quantità significativa che possono annunciare (per singola connessione) indicando la dimensione dei messaggi che sono in grado di gestire. Assumiamo inoltre che il lato di invio regoli la propria trasmissione in base agli annunci di tale dimensione.
Quando una connessione viene aperta, una cella (chiamata 'Their Size') viene impostata a zero. Il lato di ricezione deciderà quanto spazio può allocare e invierà un messaggio ALL che specifica tale spazio. Il lato di invio incrementerà 'Their Size' dello spazio allocato e potrà quindi inviare messaggi di lunghezza minore o uguale a 'Their Size'. Quando i messaggi vengono trasmessi, la lunghezza del messaggio viene sottratta da 'Their Size'. Quando il lato di ricezione alloca altro spazio di buffer (ad es. quando un messaggio viene prelevato dall’utente, liberando così dello spazio nel buffer di sistema), il numero di bit rilasciati viene inviato al lato di invio tramite un messaggio ALL.
Pertanto a 'Their Size' non è mai consentito diventare negativo e nessuna trasmissione può avvenire se 'Their Size' è uguale a zero.
Si noti che le lunghezze specificate nei messaggi ALL sono incrementi, non la dimensione assoluta del buffer di ricezione. Ciò è reso necessario dalla natura full duplex del protocollo di controllo di flusso. Il campo lunghezza del messaggio ALL può essere lungo 32 bit (nota: è un intero senza segno), fornendo così la possibilità di un "pozzo di bit" praticamente infinito, qualora ciò fosse mai desiderato.
C. Chiusura
Così come per aprire una connessione sono necessarie due RFC, per chiuderla sono necessari due CLS. La chiusura avviene in varie circostanze e serve a diversi scopi. Per semplificare l’analisi delle race condition, distinguiamo quattro casi: interruzione, rifiuto, terminazione da parte del ricevente, terminazione da parte del mittente.
Un utente interrompe ("aborts") una connessione quando emette una CONNECT e poi una CLOSE prima che la CONNECT sia confermata. Tipicamente un utente interromperà dopo un’attesa prolungata della conferma; anche il suo sistema può interrompere per lui se va in crash.
Un utente rifiuta ("refuses") una connessione quando emette una LISTEN e, dopo essere stato avvisato di un potenziale chiamante, emette una CLOSE. Vengono rifiutate anche tutte le richieste di connessione verso un socket che attende una chiamata da un socket particolare.
Una volta stabilita una connessione, entrambi i lati possono terminarla. La sequenza di eventi richiesta suggerisce che i tentativi di CLOSE da parte del lato di ricezione vadano considerati come richieste ("requests") che il lato di invio soddisfa sempre il prima possibile. Tutti i dati non ancora passati all’utente, o ancora in transito sulla rete, vengono scartati. Le richieste di CLOSE da parte del lato di invio vengono soddisfatte non appena è completata tutta la trasmissione dei dati.
1. Interruzione
Possiamo distinguere tre casi:
- a) Nel caso più semplice, inviamo una RFC seguita più tardi da un CLS. L’altro lato risponde con un CLS e il tentativo di connessione termina.
- b) Il processo remoto può accettare la connessione nello stesso momento in cui il processo locale la interrompe. In questo caso il processo remoto riterrà che il processo locale stia terminando una connessione aperta.
- c) Il processo remoto può rifiutare la connessione nello stesso momento in cui il processo locale la interrompe. In questo caso il processo remoto riterrà che il processo locale stia confermando il suo rifiuto.
2. Rifiuto
Dopo aver ricevuto una RFC, l’host locale può rispondere con una RFC o con un CLS, oppure può non rispondere. (L’host locale potrebbe aver già inviato la propria RFC, ecc.) Se l’host locale invia un CLS, si dice che l’host locale sta rifiutando ("refusing") la richiesta di connessione.
Richiediamo che per chiudere una connessione vengano scambiati comandi CLS, per cui è necessario che l’host locale mantenga la voce della tabella di rendezvous finché non viene restituito un CLS di conferma.
3. Terminazione da parte del mittente
Quando l’utente sul lato di invio emette una chiamata di sistema CLOSE, il suo NCP deve accettarla immediatamente, ma non può inviare un comando CLS finché tutti i dati nei buffer locali non sono stati passati all’host remoto. È quindi necessario verificare sia 'buffer-empty' sia 'RFNM-received' prima di inviare il comando CLS. Come di consueto, il CLS deve essere confermato prima che la voce possa essere cancellata.
4. Terminazione da parte del ricevente
Quando l’utente sul lato di ricezione emette una chiamata di sistema CLOSE, il suo NCP la accetta e invia immediatamente il comando CLS. Tuttavia possono ancora arrivare dati, e questi dati devono essere scartati. Il lato di invio, alla ricezione del CLS, deve terminare immediatamente il flusso di dati.