
An diejenigen, die , die sich mit Netzwerken und Protokollen beschäftigen.
Kurz
In diesem Artikel werden die Grundlagen der zuverlässigen Datenübertragung behandelt und es werden Beispiele auf gegeben, darunter UDP und TCP. Inspiriert von , , und dem Buch "Computer-Netzwerke. Der absteigende Ansatz", da nur über Tanenbaum und Oliéfer diskutiert wird.
Transportprotokoll
Gewährleistet eine logische Verbindung zwischen Anwendungsprozessen, die auf verschiedenen Hosts ausgeführt werden. Die logische Verbindung sieht aus Sicht der Anwendungen wie ein Kanal aus, der die Prozesse direkt verbindet.

werden von Endsystemen unterstützt, jedoch nicht von Netzwerk-Routern (außer — ). Auf der Senderseite wandelt die Transportschicht die vom anwendenden Prozess erhaltenen Daten in Transport-Pakete um, die Segmente genannt werden.

Dies geschieht durch das Zerlegen (falls notwendig) von Anwendungsnachrichten in Fragmente und das Hinzufügen eines Transportprotokoll-Headers zu jedem davon.

Anschließend überträgt die Transportschicht das Segment an die Netzwerkschicht des Senders, wo das Segment in ein Netzwerk-Paket (Datagramm) eingekapselt und versendet wird. Auf der Empfängerseite extrahiert die Netzwerkschicht das Transportsegment aus dem Datagramm und übergibt es an die Transportschicht. Diese verarbeitet das empfangene Segment so, dass die Daten der Anwendung des Empfängers zugänglich sind.

Prinzipien der zuverlässigen Datenübertragung
Zuverlässige Datenübertragung über einen völlig zuverlässigen Kanal
Der einfachste Fall. Die sendende Seite nimmt einfach die Daten von der höheren Ebene entgegen, erstellt ein Paket, das sie enthält, und sendet es über den Kanal.
Server
package main
import (
"log"
"net"
)
func main() {
// IP-Adresse des Servers und Port
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
if err != nil {
log.Fatal(err)
}
// Socket mit Port erstellen
serverConn, err := net.ListenUDP("udp", serverAddr)
if err != nil {
log.Fatal(err)
}
// verzögertes Schließen der Verbindung
defer serverConn.Close()
// Puffer für Daten erstellen
buf := make([]byte, 1024)
// Verbindung abwarten
for {
// Anfrage lesen
n, addr, err := serverConn.ReadFromUDP(buf)
// Daten an die OBERSTE Ebene weitergeben: in unserem Fall stdout
println(string(buf[0:n]), " form ", addr.IP.String())
if err != nil {
log.Fatal(err)
}
// keine Antwort, da dies UDP + zuverlässige Verbindung ist
}
}den Kunden zurückzuführen sind, nicht verfügbar ist.
package main
import (
"fmt"
"log"
"net"
"time"
)
func main() {
// IP-Adresse des Servers und Port
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
if err != nil {
log.Fatal(err)
}
// lokaler IP-Adresse und Port
localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
// Verbindung herstellen
conn, err := net.DialUDP("udp", localAddr, serverAddr)
if err != nil {
log.Fatal(err)
}
// verzögertes Schließen der Verbindung
defer conn.Close()
for {
// Daten von der OBERSTEN Ebene erhalten
fmt.Print("Geben Sie einen Satz ein > ")
var msg string
_, err := fmt.Scanf("%s", &msg)
if err != nil {
log.Fatal(err)
}
// es wird ein Byte-Stream, nicht ein String übergeben
buf := []byte(msg)
// schreiben (übertragen) in die Verbindung
_, err = conn.Write(buf)
if err != nil {
log.Fatal(err)
}
// 1 Sekunde warten
time.Sleep(time.Second * 1)
}
}Zuverlässige Datenübertragung über Kanäle mit möglichen Fehlern
Der nächste Schritt besteht darin, zu vermuten, dass alle übertragenen Pakete in der Reihenfolge empfangen werden, in der sie gesendet wurden, aber die Bits in ihnen beschädigt sein können, da der Kanal manchmal Daten mit Verzerrungen überträgt.

In diesem Fall kommen folgende Mechanismen zur Anwendung:
- Fehlererkennung;
- Rückmeldung;
- Wiederübertragung.
Protokolle für die zuverlässige Datenübertragung, die über derartige Mechanismen zur mehrfachen Übertragung verfügen, werden als Protokolle mit automatischer Wiederholungsanforderung (Automatic Repeat reQuest, ARQ) bezeichnet.
Darüber hinaus sollte man die Möglichkeit von Fehlern auch in den Bestätigungen berücksichtigen, wenn die empfangende Seite keine Informationen über das Ergebnis der Übertragung des letzten Pakets erhält.
Die Lösung dieses Problems, die unter anderem in TCP verwendet wird, besteht darin, ein neues Feld mit der Paketnummer in das Datenpaket hinzuzufügen.

Zuverlässige Datenübertragung über einen unzuverlässigen Kanal, der Verzerrungen und Paketverluste zulässt.
Leider gibt es neben Verzerrungen auch Paketverluste im Netzwerk.
Für die Lösung dieses Problems sind Mechanismen erforderlich:
- Erkennung von Paketverlusten;
- Wiederübertragung verlorener Pakete an die empfangende Seite.
Zusätzlich zur Paketverlust ist es notwendig, die Möglichkeit eines Verlusts von Quittungen oder, falls nichts verloren gegangen ist, deren Zustellung mit erheblichen Verzögerungen zu berücksichtigen. In allen Fällen erfolgt das Gleiche: die erneute Übertragung des Pakets. Zur Kontrolle der Zeit wird in diesem Mechanismus ein Timer verwendet, der das Ende des Warteintervalls bestimmt. So ist im Paket der TCPKeepAlive-Parameter standardmäßig auf 15 Sekunden eingestellt:
// defaultTCPKeepAlive is a default constant value for TCPKeepAlive times
// See golang.org/issue/31510
const (
defaultTCPKeepAlive = 15 * time.Second
)Die sendende Seite muss jedes Mal einen Timer starten, wenn ein Paket übertragen wird (sowohl beim ersten als auch beim erneuten Senden), Unterbrechungen vom Timer verarbeiten und ihn anhalten.
Nun haben wir uns mit den Schlüsselbegriffen der Protokolle für zuverlässige Datenübertragung vertraut gemacht:
- Prüfziffern;
- Paketnummern;
- Timer;
- positive und negative Quittungen.
Aber das ist noch nicht alles!
Protokoll für zuverlässige Datenübertragung mit Pipeline-Übertragung
In der bereits betrachteten Variante ist das Protokoll für zuverlässige Übertragung sehr ineffizient. Es beginnt, die Übertragung, die durch den Kommunikationskanal gewährleistet wird, bei steigenden RTTs zu „verlangsamen“. Um seine Effizienz zu steigern und die Bandbreite des Kommunikationskanals besser zu nutzen, wird die Pipeline-Übertragung verwendet.

Die Anwendung der Pipeline-Übertragung führt zu:
- einer Erhöhung des Bereichs der Paketnummern, da alle gesendeten Pakete (außer Wiederholungsübertragungen) eindeutig identifizierbar sein müssen;
- einem Bedarf an größeren Puffern auf der sendenden und empfangenden Seite.
Der Bereich der Paketnummern und die Anforderungen an die Puffergrößen hängen von den Maßnahmen ab, die das Protokoll als Reaktion auf Verzerrungen, Verluste und Verzögerungen von Paketen ergreift. Im Falle von Pipeline-Übertragungen gibt es zwei Fehlerkorrekturmöglichkeiten:
- Rückkehr um N Pakete;
- selektive Wiederholung.
Rückkehr um N Pakete — das Sliding-Window-Protokoll

Der Sender muss drei Arten von Ereignissen verwalten:
- Aufruf durch ein höheres Protokoll. Wenn die Funktion zum Senden von Daten vom „oberen“ Level aufgerufen wird, überprüft die sendende Seite zunächst den Füllgrad des Fensters (d.h. ob N gesendete Nachrichten auf die Bestätigungen warten). Ist das Fenster nicht gefüllt, wird ein neues Paket gebildet und gesendet, während die Werte der Variablen aktualisiert werden. Andernfalls übergibt die sendende Seite die Daten an das obere Level zurück, was eine implizite Anweisung darstellt, dass das Fenster gefüllt ist. Üblicherweise unternimmt das obere Level nach einer gewissen Zeit einen erneuten Versuch, die Daten zu übertragen. In einer realen Anwendung würde der Sender wahrscheinlich entweder die Daten puffern (anstatt sie sofort zu senden) oder über einen Synchronmechanismus (z.B. ein Semaphore oder ein Flag) verfügen, der es dem übergeordneten Level ermöglicht, die Funktion zum Senden von Daten nur dann aufzurufen, wenn das Fenster nicht gefüllt ist.
- Bestätigung erhalten. Im Protokoll wird für das Paket mit der Sequenznummer N eine allgemeine Quittung ausgestellt, die anzeigt, dass alle Pakete mit Sequenznummern vor N erfolgreich empfangen wurden.
- Ablauf des Warteintervalls. Um Verluste und Verzögerungen von Paketen und Quittungen zu bestimmen, verwendet das Protokoll einen Timer. Wenn das Warteintervall abläuft, sendet die sendende Seite alle gesendeten, unbestätigten Pakete erneut.
Selektives Wiederholen
Wenn die Fenstergröße und das Produkt aus Bandbreite und Ausbreitungsverzögerung hoch sind, können sich viele Pakete in der Pipeline befinden. In einem solchen Fall könnte der Fehler eines einzelnen Pakets die erneute Übertragung vieler Pakete auslösen, von denen die meisten nicht benötigt wurden.
Beispiel
Die besten Praktiken sind in einer praktischen Umsetzung gesammelt . Und wenn jemand weiß, wie es besser geht — .
Server
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
// Erstelle einen Socket mit dem Port
ln, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatalln(err)
}
// Aufruf abwarten
conn, _ := ln.Accept()
for {
// Daten lesen
msg, err := bufio.NewReader(conn).ReadString('n')
if err != nil {
log.Fatalln(err)
}
// Nachricht auf stdout ausgeben
fmt.Print("Nachricht empfangen:", string(msg))
// Zeile in Großbuchstaben umwandeln
newMsg := strings.ToUpper(msg)
// Daten senden
conn.Write([]byte(newMsg + "n"))
}
}den Kunden zurückzuführen sind, nicht verfügbar ist.
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
)
func main() {
\/\/ Verbindung einrichten
conn, err := net.Dial("tcp", "127.0.0.1:8081")
if err != nil {
log.Fatalln(err)
}
for {
\/\/ Daten von stdin lesen
reader := bufio.NewReader(os.Stdin)
fmt.Print("Text zum Senden: ")
\/\/ Zeilenweise
text, err := reader.ReadString('n')
if err != nil {
log.Fatalln(err)
}
\/\/ Senden
fmt.Fprintf(conn, text+"n")
\/\/ Empfang
msg, err := bufio.NewReader(conn).ReadString('n')
if err != nil {
log.Fatalln(err)
}
\/\/ Ausgabe der empfangenen Antwort
fmt.Print("Nachricht vom Server: " + msg)
}
}Ausgabe
Mechanismen, die eine zuverlässige Datenübertragung und deren Nutzung gewährleisten
Mechanismus
Anwendung, Kommentar
Prüfziffer
Wird verwendet, um Bitfehler im übertragenen Paket zu erkennen
Timer
Abbruchzeitraum und Hinweis auf dessen Ablauf. Letzteres bedeutet, dass mit hoher Wahrscheinlichkeit das Paket oder dessen Quittung beim Versand verloren gegangen sind. Falls das Paket mit Verzögerung, jedoch nicht verloren (vorzeitiger Ablauf des Abbruchzeitraums) zugestellt wird oder die Quittung verloren geht, führt die Wiederübertragung zu einer Duplizierung des Pakets auf der Empfangsseite.
Folgenummer
Wird zur sequentiellen Nummerierung von Datenpaketen verwendet, die vom Sender an den Empfänger übertragen werden. Lücken in den Reihenfolgenummern der empfangenen Pakete ermöglichen es dem Empfänger, den Verlust eines Pakets zu erkennen. Gleiche Folgenummern von Paketen bedeuten, dass die Pakete sich duplizieren.
Bestätigung
Wird von der empfangenden Seite generiert und zeigt der sendenden Seite an, dass das entsprechende Paket oder eine Gruppe von Paketen erfolgreich empfangen wurde. In der Regel enthält die Bestätigung die Folgenummern erfolgreich empfangener Pakete. Je nach Protokoll unterscheidet man zwischen Einzel- und Gruppenbestätigungen.
Negative Bestätigung
Wird vom Empfänger verwendet, um dem Sender mitzuteilen, dass das Paket nicht korrekt empfangen wurde. Eine negative Bestätigung umfasst normalerweise die Folgenummer des Pakets, das nicht korrekt empfangen wurde.
Fenster, Pipeline
Sie beschränken den Bereich der verwendbaren Sequenznummern für die Übertragung von Paketen. Die Gruppensendung und das Handshaking ermöglichen eine signifikante Erhöhung der Protokolldurchsatzrate im Vergleich zum Bestätigungsmodus. Wie wir sehen werden, kann die Größe des Fensters basierend auf den Empfangs- und Pufferfähigkeiten der empfangenden Seite sowie dem Niveau der Netzwerkauslastung berechnet werden.
Weitere Beispiele für die Verwendung von Go im Netzwerkbereich.
Im .
Quelle: habr.com
