
A coloro che vogliono capire le reti e i protocolli, è dedicato.
In sintesi
L'articolo esamina le basi della trasmissione affidabile dei dati, fornendo esempi su , inclusi UDP e TCP. Con riferimento a , , e al libro "Reti di computer. Approccio discendente", dato che tutti discutono solo di Tanenbaum e Olifer.
Protocollo di livello di trasporto
Garantisce una connessione logica tra i processi applicativi che vengono eseguiti su host diversi. La 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 — ). Dalla parte del mittente, il livello di trasporto trasforma i dati del livello applicativo ricevuti dal processo applicativo mittente in pacchetti di livello di trasporto, chiamati segmenti.

Questo viene fatto suddividendo (quando necessario) i messaggi del livello applicativo in frammenti e aggiungendo a ciascuno di essi un'intestazione del 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. Dalla parte del 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 in modo che i suoi dati siano disponibili per l'applicazione ricevente.

Principi di trasmissione affidabile dei dati
Trasmissione affidabile dei dati su un canale completamente affidabile
Il caso più semplice. La parte mittente semplicemente riceve i dati dal livello superiore, crea un pacchetto che li contiene e lo invia nel canale.
Server
pacchetto principale
importa (
"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 sulla 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 una connessione
for {
\/\/ leggiamo la richiesta
n, addr, err := serverConn.ReadFromUDP(buf)
\/\/ passiamo i dati al Livello Superiore: nel nostro caso stdout
println(string(buf[0:n]), " form ", addr.IP.String())
if err != nil {
log.Fatal(err)
}
\/\/ nessuna risposta, poiché è UDP + canale affidabile
}
}Cliente
pacchetto principale
importa (
"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 una connessione
conn, err := net.DialUDP("udp", localAddr, serverAddr)
if err != nil {
log.Fatal(err)
}
\/\/ chiusura ritardata della connessione
defer conn.Close()
for {
\/\/ ricezione di dati dal Livello Superiore
fmt.Print("Inserisci una frase in minuscolo > ")
var msg string
_, err := fmt.Scanf("%s", &msg)
if err != nil {
log.Fatal(err)
}
\/\/ viene trasmesso un flusso di byte, non una stringa
buf := []byte(msg)
\/\/ scrittura (trasmissione) nella connessione
_, err = conn.Write(buf)
if err != nil {
log.Fatal(err)
}
\/\/ un secondo
time.Sleep(time.Second * 1)
}
}Trasmissione sicura dei dati attraverso un canale con possibili errori
Il passo successivo è supposare che tutti i pacchetti trasmessi vengano 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 tal caso vengono applicati meccanismi:
- rilevazione degli errori;
- feedback;
- ritrasmissione.
I protocolli per la trasmissione sicura dei dati, che dispongono di tali meccanismi di ritrasmissione multipla, vengono chiamati protocolli con richiesta automatica di ritrasmissione (Automatic Repeat reQuest, ARQ).
Inoltre, è opportuno prevedere la possibilità di errori anche nelle ricevute, quando la parte ricevente non riceve alcuna informazione sui risultati della trasmissione dell'ultimo pacchetto.
La soluzione a questo compito, utilizzata anche nel TCP, consiste nell'aggiungere a un pacchetto dati un nuovo campo contenente il numero di sequenza del pacchetto.

Trasferimento affidabile dei dati su un canale inaffidabile, soggetto a distorsioni e perdita di pacchetti
Sfortunatamente, insieme alle distorsioni, nella rete è presente anche la perdita di pacchetti.
E per risolvere questo problema sono necessari meccanismi:
- determina l'accaduto della perdita di pacchetti;
- rispedizione dei pacchetti persi al lato ricevente.
Inoltre, oltre alla perdita del pacchetto, è necessario prevedere la possibilità di perdita di una ricevuta o, se nulla è andato perso, la sua consegna con un notevole ritardo. In tutti i casi si procede nello stesso modo: la ritrasmissione del pacchetto. Per controllare il tempo in questo meccanismo si utilizza un timer di conteggio, che permette di determinare la scadenza 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 lato mittente deve avviare il timer ogni volta che trasmette un pacchetto (sia alla prima che alla ritrasmissione), gestire le interruzioni del timer e fermarlo.
Dunque, 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 pipeline
Nella variante che abbiamo già esaminato, il protocollo di consegna affidabile è molto inefficiente. Inizia a «rallentare» la trasmissione, garantita dal canale di comunicazione, con l'aumento di RTT. Per migliorare la sua efficienza, e una migliore utilizzazione della larghezza di banda del canale di comunicazione, si applica la pipeline.

L'applicazione della pipeline porta a:
- un aumento dell'intervallo dei numeri di sequenza, poiché tutti i pacchetti inviati (ad eccezione delle ritrasmissioni) devono essere identificabili in modo univoco;
- la necessità di aumentare i buffer sui lati mittente e ricevente.
L'intervallo dei numeri di sequenza e i requisiti sulle dimensioni dei buffer dipendono dalle azioni intraprese dal protocollo in risposta a distorsioni, perdite e ritardi di pacchetti. Nel caso della pipeline ci sono due metodi di correzione degli errori:
- riportare indietro N pacchetti;
- ripetizione selettiva.
Riportare indietro N pacchetti — protocollo di finestra scorrevole

Il mittente deve mantenere tre tipi di eventi:
- chiamata tramite protocollo di livello superiore. Quando una funzione di invio dati viene chiamata "dall'alto", la parte trasmittente controlla prima il riempimento della finestra (cioè il numero di N messaggi inviati in attesa di ricevuta). Se la finestra non è piena, un nuovo pacchetto viene formato e inviato, e i valori delle variabili vengono aggiornati. In caso contrario, la parte trasmittente restituisce i dati al livello superiore, indicando implicitamente che la finestra è piena. Di solito, il livello superiore tenta di ritrasmettere i dati dopo un certo tempo. In una applicazione reale, il mittente probabilmente avrebbe memorizzato i dati nella cache (invece di inviarli immediatamente), o avrebbe un meccanismo di sincronizzazione (come un semaforo o un flag) che permetterebbe 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 la perdita di pacchetti e ritardi di ricevute, il protocollo utilizza un timer. Se il timeout scade, la parte trasmittente ritrasmette tutti i pacchetti inviati che non sono stati confermati.
Ripetizione selettiva
Quando la dimensione della finestra e il prodotto della larghezza di banda per la latenza di propagazione sono elevati, un gran numero di pacchetti può essere in pipeline. In tal caso, la perdita 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 una implementazione pratica . E se qualcuno sa come fare meglio — .
Server
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
// creiamo un socket con la 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
pacchetto principale
importa (
"bufio"
"fmt"
"log"
"net"
"os"
)
funzione main() {
\\/ creazione della connessione
conn, err := net.Dial("tcp", "127.0.0.1:8081")
se err != nil {
log.Fatalln(err)
}
per {
\\/ lettura dei dati da stdin
lettore := bufio.NewReader(os.Stdin)
fmt.Print("Testo da inviare: ")
\\/ riga per riga
text, err := lettore.ReadString('n')
se err != nil {
log.Fatalln(err)
}
\\/ invio
fmt.Fprintf(conn, text+"n")
\\/ ricezione
msg, err := bufio.NewReader(conn).ReadString('n')
se err != nil {
log.Fatalln(err)
}
\\/ output della risposta ricevuta
fmt.Print("Msg dal Server: " + msg)
}
}Conclusione
Meccanismi che garantiscono una trasmissione affidabile dei dati e il loro utilizzo
Il meccanismo
Applicazione, commento
Checksum
Utilizzato per rilevare errori di bit nel pacchetto trasmesso
Timer
Misurazione del timeout e indicazione della sua scadenza. L'ultima significa che, con alta probabilità, il pacchetto o la sua ricevuta sono stati persi durante la trasmissione. Nel caso in cui il pacchetto venga consegnato con ritardo ma non venga perso (scadenza prematura del timeout), oppure si verifichi la perdita della ricevuta, la ritrasmissione porta alla duplicazione del pacchetto sul lato ricevente
Numero di sequenza
Utilizzato per la numerazione sequenziale dei pacchetti dati trasmessi dal mittente al destinatario. Interruzioni nei numeri di sequenza dei pacchetti ricevuti consentono al destinatario di rilevare la perdita di un pacchetto. Numeri di sequenza identici nei pacchetti indicano che i pacchetti si duplicano a vicenda
Conferma
Generato dal lato ricevente e indica al lato trasmittente che il pacchetto corrispondente o gruppo di pacchetti sono stati ricevuti correttamente. 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
Utilizzato dal ricevente per informare il mittente che il pacchetto è stato ricevuto in modo errato. La conferma negativa di solito include il numero di sequenza del pacchetto che non è stato ricevuto correttamente
Finestra, pipeline
Limitano l'intervallo dei numeri di sequenza che possono essere utilizzati per la trasmissione dei pacchetti. La trasmissione di gruppo e l'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 di buffering della parte ricevente, nonché al livello di congestione della rete.
Altri esempi di utilizzo di Go per lavorare con la rete
In .
Fonte: habr.com
