
A coloro che vogliono comprendere le reti e i protocolli, è dedicato.
In breve
L'articolo tratta le basi della trasmissione affidabile dei dati, presentando esempi su , inclusi UDP e TCP. Ispirato a , , 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.

sono supportati dai sistemi finali, ma non dai router di rete (eccetto — ). 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.

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.

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.

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.

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.

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 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.

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.

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 pratiche raccolte in un'implementazione pratica . E se qualcuno sa come fare meglio — .
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 .
Fonte: habr.com
