Le basi di una trasmissione dati affidabile

Le basi di una trasmissione dati affidabile

A coloro che si propone di vogliono comprendere le reti e i protocolli, è dedicato.

In breve

L'articolo tratta le basi della trasmissione affidabile dei dati, presentando esempi su Go, inclusi UDP e TCP. Ispirato a una volta, due volte, tre volte e al libro "Reti di computer. Approccio top-down", mentre tutti discutono solo di Tanenbaum e Olinger.

Protocollo di livello di trasporto

Garantisce una connessione logica tra i processi applicativi che vengono eseguiti su host diversi. Una connessione logica dal punto di vista delle applicazioni appare come un canale che collega direttamente i processi.

Le basi di una trasmissione dati affidabile

I protocolli di livello di trasporto sono supportati dai sistemi finali, ma non dai router di rete (eccetto — DPI). Sul lato del mittente, il livello di trasporto convertirà i dati del livello applicativo, ricevuti dal processo applicativo trasmittente, in pacchetti di livello di trasporto, chiamati segmenti.

Le basi di una trasmissione dati affidabile

Questo viene fatto suddividendo (se necessario) i messaggi del livello applicativo in frammenti e aggiungendo a ciascuno di essi un'intestazione di livello di trasporto.

Le basi di una trasmissione dati affidabile

Successivamente, il livello di trasporto invia il segmento al livello di rete del mittente, dove il segmento viene incapsulato in un pacchetto di rete (datagramma) e inviato. Sul lato ricevente, il livello di rete estrae il segmento del livello di trasporto dal datagramma e lo trasmette al livello di trasporto. Successivamente, il livello di trasporto elabora il segmento ricevuto affinché i suoi dati siano accessibili all'applicazione destinataria.

Le basi di una trasmissione dati affidabile

Principi di trasmissione affidabile dei dati

Trasmissione affidabile dei dati attraverso un canale del tutto sicuro

Il caso più semplice. La parte mittente riceve semplicemente i dati dal livello superiore, crea un pacchetto contenente i dati e lo invia nel canale.

Server

package main

import (
    "log"
    "net"
)

func main() {
    // Indirizzo IP del server e porta
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }

    // Creiamo un socket con la porta
    serverConn, err := net.ListenUDP("udp", serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // Chiusura ritardata della connessione
    defer serverConn.Close()

    // Creiamo un buffer per i dati
    buf := make([]byte, 1024)

    // Aspettiamo la connessione
    for {
        // Leggiamo la richiesta
        n, addr, err := serverConn.ReadFromUDP(buf)
        // Passiamo i dati al LIVELLO SUPERIORE: in questo caso stdout
        println(string(buf[0:n]), " da ", addr.IP.String())
        if err != nil {
            log.Fatal(err)
        }
        // Non c'è risposta, poiché è UDP + canale affidabile
    }
}

Cliente

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    // Indirizzo IP del server e porta
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }
    // Indirizzo IP locale e porta
    localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
    if err != nil {
        log.Fatal(err)
    }
    // Stabilire la connessione
    conn, err := net.DialUDP("udp", localAddr, serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // Chiusura della connessione in ritardo
    defer conn.Close()

    for {
        // Ricevere dati dal LIVELLO SUPERIORE
        fmt.Print("Inserisci una frase > ")
        var msg string
        _, err := fmt.Scanf("%s", &msg)
        if err != nil {
            log.Fatal(err)
        }
        // Viene trasferito un flusso di byte, non una stringa
        buf := []byte(msg)
        // Scrivere (trasmettere) nella connessione
        _, err = conn.Write(buf)
        if err != nil {
            log.Fatal(err)
        }
        // Un secondo
        time.Sleep(time.Second * 1)
    }
}

Trasferimento dati affidabile su un canale con possibili errori

Il passo successivo è quello di presupporre che tutti i pacchetti trasmessi siano ricevuti nell'ordine in cui sono stati inviati, ma i bit in essi possono essere danneggiati, poiché il canale a volte trasmette dati distorti.

Le basi di una trasmissione dati affidabile

In questo caso vengono applicati i meccanismi:

  • rilevamento degli errori;
  • feedback;
  • ritrasmissione.

I protocolli di trasmissione dei dati affidabili che presentano meccanismi di ritrasmissione sono noti come protocolli di richiesta automatica di ritrasmissione (Automatic Repeat reQuest, ARQ).
Inoltre, è importante considerare la possibilità di errori anche nelle ricevute, quando la parte ricevente non riceve alcuna informazione riguardo il risultato della trasmissione dell'ultimo pacchetto.
La soluzione a questo problema, utilizzata anche nel TCP, consiste nell'aggiungere a ciascun pacchetto un nuovo campo contenente il numero di sequenza del pacchetto.

Le basi di una trasmissione dati affidabile

La trasmissione affidabile dei dati su un canale non affidabile, soggetto a distorsioni e perdite di pacchetti.

Purtroppo, insieme alle distorsioni, nella rete ci sono anche perdite di pacchetti.
Per affrontare questa problematica sono necessari meccanismi di:

  • identificazione della perdita di pacchetti;
  • ritrasmissione dei pacchetti persi alla parte ricevente.

In aggiunta, oltre alla perdita di pacchetti, è necessario prevedere la possibilità di perdita della ricevuta o, se nulla è andato perduto, la sua consegna con un notevole ritardo. In tutti i casi, si effettua la stessa operazione: la ritrasmissione del pacchetto. Per controllare il tempo in questo meccanismo, si utilizza un timer che consente di determinare la fine dell'intervallo di attesa. Così, nel pacchetto net il parametro TCPKeepAlive è impostato su 15 secondi per impostazione predefinita:

// defaultTCPKeepAlive is a default constant value for TCPKeepAlive times
// See golang.org/issue/31510
const (
    defaultTCPKeepAlive = 15 * time.Second
)

Il mittente deve avviare il timer ogni volta che viene trasmesso un pacchetto (sia alla prima trasmissione che alla ritrasmissione), gestire le interruzioni del timer e fermarlo.

Quindi, abbiamo esaminato i concetti chiave dei protocolli di trasmissione dati affidabili:

  • somme di controllo;
  • numeri di sequenza dei pacchetti;
  • timer;
  • ricevute positive e negative.

Ma non è tutto!

Protocollo di trasmissione dati affidabile con canalizzazione

Nella variante che abbiamo già esaminato, il protocollo di consegna affidabile risulta molto inefficiente. Inizia a "ritardare" la trasmissione garantita dal canale di comunicazione con l'aumento del RTT. Per migliorarne l'efficacia e ottimizzare l'utilizzo della banda del canale di comunicazione, si utilizza la canalizzazione.

Le basi di una trasmissione dati affidabile

L'uso della canalizzazione porta a:

  • un aumento dell'intervallo dei numeri di sequenza, poiché tutti i pacchetti inviati (eccetto le ritrasmissioni) devono essere univocamente identificabili;
  • la necessità di aumentare i buffer sui lati mittente e destinatario.

L'intervallo dei numeri di sequenza e i requisiti delle dimensioni dei buffer dipendono dalle azioni intraprese dal protocollo in risposta a distorsioni, perdite e ritardi del pacchetto. Nel caso della canalizzazione esistono due metodi di correzione degli errori:

  • il ritorno indietro di N pacchetti;
  • la ritrasmissione selettiva.

Il ritorno indietro di N pacchetti è il protocollo di finestra scorrevole.

Le basi di una trasmissione dati affidabile

Il mittente deve gestire tre tipi di eventi:

  • chiamata tramite un protocollo di livello superiore. Quando viene chiamata la funzione di invio dati dal "livello superiore", la parte mittente verifica innanzitutto il grado di riempimento della finestra (cioè la presenza di N messaggi inviati in attesa di ricevere conferma). Se la finestra risulta non piena, viene formato e inviato un nuovo pacchetto, e i valori delle variabili vengono aggiornati. Altrimenti, la parte mittente restituisce i dati al livello superiore, il che indica implicitamente che la finestra è piena. Di solito, il livello superiore tenta di ripetere l'invio dei dati dopo un certo tempo. In un'applicazione reale, il mittente probabilmente avrebbe sia memorizzato i dati in buffer (invece di inviarli immediatamente), o avrebbe un meccanismo di sincronizzazione (come un semaforo o un flag), che consentirebbe al livello superiore di chiamare la funzione di invio dati solo quando la finestra non è piena.
  • ricezione della conferma. Nel protocollo, per il pacchetto con numero di sequenza N viene emessa una ricevuta generale, che indica che tutti i pacchetti con numeri di sequenza precedenti a N sono stati ricevuti con successo.
  • scadenza del timeout. Per determinare i fatti di perdita e ritardi nei pacchetti e delle ricevute, il protocollo utilizza un timer. Se il timeout scade, la parte mittente reinvia tutti i pacchetti inviati non confermati.

ripetizione selettiva

Quando la dimensione della finestra e il prodotto della larghezza di banda e della latenza di propagazione sono elevati, un gran numero di pacchetti può essere in coda. In tal caso, l'errore di un singolo pacchetto può causare la ritrasmissione di un gran numero di pacchetti, la maggior parte dei quali non erano necessari.

Esempio

Migliori teoriche pratiche raccolte in un'implementazione pratica TCP. E se qualcuno sa come fare meglio — welcome.

Server

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "strings"
)

func main() {
    // creiamo un socket sulla porta 
    ln, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatalln(err)
    }
    // attesa della chiamata
    conn, _ := ln.Accept()

    for {
        // lettura dei dati
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // output del messaggio su stdout
        fmt.Print("Messaggio ricevuto:", string(msg))
        // conversione della stringa in maiuscolo
        newMsg := strings.ToUpper(msg)
        // invio dei dati
        conn.Write([]byte(newMsg + "n"))
    }
}

Cliente

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "os"
)

func main() {
    // connessione
    conn, err := net.Dial("tcp", "127.0.0.1:8081")
    if err != nil {
        log.Fatalln(err)
    }

    for {
        // lettura dei dati da stdin
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Testo da inviare: ")
        // riga per riga
        text, err := reader.ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // invio
        fmt.Fprintf(conn, text+"n")
        // ricezione
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // stampa della risposta ricevuta
        fmt.Print("Msg dal Server: " + msg)
    }
}

Risultato

Meccanismi che garantiscono una trasmissione affidabile dei dati e il loro utilizzo

Meccanismo
Applicazione, commento

Checksum
Utilizzato per la rilevazione di errori bit nella trasmissione del pacchetto

Timer
Conteggio dell'intervallo di attesa e indicazione della sua scadenza. Ciò significa che è altamente probabile che il pacchetto o la sua ricevuta siano stati persi durante la trasmissione. Se il pacchetto arriva con ritardo ma non viene perso (scadenza prematura dell'intervallo di attesa), oppure si verifica la perdita della ricevuta, la ritrasmissione porta a un duplicato del pacchetto sul lato ricevente

Numero di sequenza
Utilizzato per la numerazione sequenziale dei pacchetti di dati trasmessi dal mittente al destinatario. Le interruzioni nei numeri di sequenza dei pacchetti ricevuti consentono al destinatario di rilevare la perdita di pacchetti. Numeri di sequenza identici indicano che i pacchetti si duplicano l'uno con l'altro.

Conferma
Generato dalla parte ricevente e indica alla parte che trasmette che il pacchetto o il gruppo di pacchetti corrispondente è stato ricevuto con successo. Di solito, la conferma contiene i numeri di sequenza dei pacchetti ricevuti con successo. A seconda del protocollo, si distinguono conferme individuali e di gruppo.

Conferma negativa
Utilizzata dal destinatario per informare il mittente che il pacchetto è stato ricevuto in modo errato. La conferma negativa include normalmente il numero di sequenza del pacchetto che non è stato ricevuto correttamente.

Finestra, canalizzazione
Limitano l'intervallo dei numeri di sequenza che possono essere utilizzati per la trasmissione dei pacchetti. La trasmissione di gruppo e il handshake consentono di aumentare significativamente la larghezza di banda dei protocolli rispetto alla modalità di attesa delle conferme. Come vedremo, la dimensione della finestra può essere calcolata in base alle capacità di ricezione e buffering del lato ricevente, così come al livello di carico della rete.

Altri esempi di utilizzo di Go per il networking

In repository.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster