
Für diejenigen, die sich mit Netzwerken und Protokollen auseinandersetzen möchten.
Zusammenfassung
In diesem Artikel werden die Grundlagen der zuverlässigen Datenübertragung erläutert, mit Beispielen auf , einschließlich UDP und TCP. Inspiriert von , , und dem Buch "Computer Networks: A Top-Down Approach", da nur über Tanenbaum und Olifer gesprochen wird.
Transportprotokoll
Stellt eine logische Verbindung zwischen Anwendungsprozessen her, die auf unterschiedlichen Hosts ausgeführt werden. Aus Sicht der Anwendungen sieht eine logische Verbindung wie ein Kanal aus, der die Prozesse direkt verbindet.

werden von Endsystemen unterstützt, nicht von Netzwerkrouten (mit Ausnahme von — ). Auf der Senderseite wandelt die Transportschicht die Anwendungsdaten, die sie vom übertragenden Anwendungsprozess erhält, in Transportschichtpakete um, die Segmente genannt werden.

Dies geschieht durch die Aufteilung (wenn nötig) der Anwendungsnachrichten in Fragmente und das Hinzufügen eines Headers der Transportschicht zu jedem dieser Fragmente.

Anschließend überträgt die Transportschicht das Segment an die Netzwerkschicht des Senders, wo das Segment in ein Netzpaket (Datagramm) eingekapselt und versendet wird. Auf der Empfängerseite extrahiert die Netzwerkschicht das Segment der Transportschicht aus dem Datagramm und leitet es an die Transportschicht weiter. Daraufhin verarbeitet die Transportschicht das erhaltene Segment, damit die Daten der Empfangsanwendung zur Verfügung stehen.

Prinzipien der zuverlässigen Datenübertragung
Zuverlässige Datenübertragung über einen absolut zuverlässigen Kanal
Der einfachste Fall. Die sendende Seite nimmt einfach die Daten von der oberen Ebene entgegen, erstellt ein Paket, das diese 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)
}
\/\/ Erstellen eines Sockets mit Port
serverConn, err := net.ListenUDP("udp", serverAddr)
if err != nil {
log.Fatal(err)
}
\/\/ Verzögertes Schließen der Verbindung
defer serverConn.Close()
\/\/ Erstellen eines Puffers für Daten
buf := make([]byte, 1024)
\/\/ Warten auf Verbindung
for {
\/\/ Lesen der Anfrage
n, addr, err := serverConn.ReadFromUDP(buf)
\/\/ Übertragen der Daten an die OBERSTE Ebene: 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ässiger Kanal ist
}
}Kunde
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)
}
// Lokale 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 {
// Eingabe von Daten auf OBERE EBENE
fmt.Print("Bitte geben Sie einen Satz ein > ")
var msg string
_, err := fmt.Scanf("%s", &msg)
if err != nil {
log.Fatal(err)
}
// Es wird ein Byte-Stream übermittelt, kein String
buf := []byte(msg)
// Schreiben (Übertragung) 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 potenziellen Fehlern
Der nächste Schritt besteht darin, davon auszugehen, dass alle übertragenen Pakete in der Reihenfolge empfangen wurden, in der sie gesendet wurden, aber die Bits in ihnen möglicherweise beschädigt sind, da der Kanal manchmal Daten verzerrt überträgt.

In diesem Fall kommen folgende Mechanismen zum Einsatz:
- Fehlererkennung;
- Feedback;
- Wiederübertragung.
Übertragungsprotokolle, die über Mechanismen zur mehrmaligen Übertragung verfügen, werden als Protokolle mit automatischem Wiederholungsanfrage (Automatic Repeat reQuest, ARQ) bezeichnet.
Zusätzlich sollte auch die Möglichkeit von Fehlern in den Empfangsbestätigungen berücksichtigt werden, wenn die empfangende Seite keine Informationen über das Ergebnis der Übertragung des letzten Pakets erhält.
Die Lösung dieses Problems, die auch in TCP verwendet wird, besteht darin, dem Datenpaket ein neues Feld hinzuzufügen, das die Seriennummer des Pakets enthält.

Zuverlässige Datenübertragung über einen unzuverlässigen Kanal, der Verzerrungen und Paketverluste zulässt.
Leider treten in Netzwerken neben Verzerrungen auch Paketverluste auf.
Um dieses Problem zu lösen, werden folgende Mechanismen benötigt:
- Erkennung des Paketverlusts;
- erneute Zustellung der verlorenen Pakete an die empfangende Seite.
Zusätzlich zu Paketverlusten muss auch die Möglichkeit des Verlusts einer Bestätigung oder, falls nichts verloren geht, der verspäteten Zustellung berücksichtigt werden. In allen Fällen erfolgt das Gleiche: die erneute Übertragung des Pakets. Ein Timer wird zur Kontrolle der Zeit in diesem Mechanismus verwendet, der das Ende des Wartintervals bestimmt. So im Paket. Der TCPKeepAlive-Parameter ist 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
)Der sendenden Seite muss bei jeder Paketübertragung (sowohl bei der ersten als auch bei der Wiederholung) den Timer starten, Interrupts vom Timer verarbeiten und ihn anhalten.
Nun sind wir mit den SchlüsselkConcepten der zuverlässigen Datenübertragungsprotokolle vertraut:
- Prüfziffern;
- Paketsequenznummern;
- Timer;
- positive und negative Bestätigungen.
Aber das ist noch nicht alles!
Das Protokoll für zuverlässige Datenübertragung mit Pipeline-Verarbeitung.
In der bereits betrachteten Variante ist das Protokoll für die zuverlässige Zustellung sehr ineffizient. Es beginnt, die Übertragung über den Kommunikationskanal zu „verlangsamen“, wenn die RTT steigt. Um die Effizienz zu steigern und eine bessere Auslastung der Bandbreite zu erreichen, wird die Pipeline-Technologie eingesetzt.

Die Anwendung der Pipeline-Technologie führt zu:
- einer Erhöhung des Bereichs der Sequenznummern, da alle gesendeten Pakete (außer den Wiederübertragungen) eindeutig identifizierbar sein müssen;
- der Notwendigkeit, die Puffer sowohl auf der Sender- als auch auf der Empfangsseite zu vergrößern.
Der Bereich der Sequenznummern und die Anforderungen an die Pufferspeichergröße hängen von den Maßnahmen ab, die das Protokoll als Reaktion auf Verzerrungen, Paketverlust und Verzögerungen ergreift. Bei der Pipeline-Technologie gibt es zwei Methoden zur Fehlerkorrektur:
- Rückkehr um N Pakete;
- selektive Wiederholung.
Die Rückkehr um N Pakete ist ein Sliding-Window-Protokoll.

Der Sender muss drei Arten von Ereignissen verwalten:
- Aufruf durch ein höheres Protokoll. Wenn die Funktion zum Senden von Daten auf einer höheren Ebene aufgerufen wird, überprüft die sendende Seite zunächst den Füllstand des Fensters (d.h. die Anzahl der gesendeten Nachrichten, die auf eine Bestätigung warten). Ist das Fenster nicht voll, wird ein neues Paket gebildet und übertragen, und die Variablenwerte werden aktualisiert. Andernfalls gibt die sendende Seite die Daten an die oberere Ebene zurück, was implizit bedeutet, dass das Fenster voll ist. In der Regel unternimmt die obere Ebene nach einiger Zeit einen neuen Übertragungsversuch. In einer realen Anwendung hätte der Sender wahrscheinlich die Daten zwischengespeichert (anstatt sie sofort zu senden) oder einen Synchronmechanismus (wie ein Semaphore oder Flag) implementiert, welcher es der höheren Ebene ermöglichen würde, die Funktion zum Senden von Daten nur bei einem nicht vollen Fenster aufzurufen.
- Bestätigungsempfang. Im Protokoll erhält das Paket mit der Sequenznummer N eine allgemeine Quittung, die angibt, dass alle Pakete mit Sequenznummern, die vor N kommen, erfolgreich empfangen wurden.
- Die Wartezeit ist abgelaufen. Um Paketverluste und Verzögerungen sowie Bestätigungen zu bestimmen, verwendet das Protokoll einen Timer. Wenn die Wartezeit abläuft, sendet die sendende Seite alle unbestätigten Pakete erneut.
Selective Repeat
Wenn die Fenstergröße und das Produkt aus Bandbreite und Ausbreitungsverzögerung groß sind, kann sich eine erhebliche Anzahl von Paketen in der Pipeline befinden. In diesem Fall kann der Fehler eines einzelnen Pakets dazu führen, dass eine große Anzahl von Paketen erneut übertragen wird, von denen die meisten nicht erforderlich waren.
Beispiel
Die besten Praktiken sind in einer praktischen Umsetzung zusammengefasst . Und falls jemand weiß, wie es besser geht — .
Server
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
// Erstellen eines Sockets mit Port
ln, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatalln(err)
}
// Warten auf einen Aufruf
conn, _ := ln.Accept()
for {
// Daten lesen
msg, err := bufio.NewReader(conn).ReadString('n')
if err != nil {
log.Fatalln(err)
}
// Nachricht in stdout ausgeben
fmt.Print("Nachricht empfangen:", string(msg))
// String in Großbuchstaben umwandeln
newMsg := strings.ToUpper(msg)
// Daten senden
conn.Write([]byte(newMsg + "n"))
}
}Kunde
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
)
func main() {
// Verbindung herstellen
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 zu senden: ")
// zeilenweise
text, err := reader.ReadString('n')
if err != nil {
log.Fatalln(err)
}
// senden
fmt.Fprintf(conn, text+"n")
// empfangen
msg, err := bufio.NewReader(conn).ReadString('n')
if err != nil {
log.Fatalln(err)
}
// Ausgabe der empfangenen Antwort
fmt.Print("Nachricht vom Server: " + msg)
}
}Fazit
Mechanismen zur Gewährleistung einer zuverlässigen Datenübertragung und deren Nutzung
Mechanismus
Anwendung, Kommentar
Prüfziffer
Wird zur Erkennung von Bitfehlern im übertragenen Paket verwendet
Timer
Zeitintervall und Indikator für dessen Ablauf. Letzteres deutet darauf hin, dass mit hoher Wahrscheinlichkeit das Paket oder dessen Bestätigung während der Übertragung verloren gegangen sind. Wenn das Paket mit Verzögerung, aber nicht verloren ankommt (vorzeitiges Ablaufen des Zeitintervalls), oder wenn die Bestätigung verloren geht, führt eine erneute Übertragung zu einer Verdopplung des Pakets auf der Empfängerseite.
Sequenznummer
Wird zur sequenziellen Nummerierung von Datenpaketen verwendet, die vom Sender an den Empfänger übertragen werden. Lücken in den sequentiellen Nummern der empfangenen Pakete ermöglichen es dem Empfänger, den Verlust eines Pakets zu erkennen. Identische sequentielle Nummern der Pakete bedeuten, dass die Pakete sich gegenseitig duplizieren.
Bestätigung
Wird von der empfangenden Seite erzeugt 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 sequentiellen Nummern der erfolgreich empfangenen Pakete. Abhängig vom Protokoll wird zwischen Einzel- und Gruppenbestätigungen unterschieden.
Negative Bestätigung
Wird vom Empfänger verwendet, um dem Sender mitzuteilen, dass das Paket fehlerhaft empfangen wurde. Die negative Bestätigung enthält in der Regel die sequentielle Nummer des Pakets, das nicht korrekt empfangen wurde.
Fenster, Pipeline
Einschränkungen des Bereichs der Sequenznummern, die für die Paketübertragung genutzt werden können. Gruppierung und Handshake ermöglichen eine signifikante Erhöhung der Protokolldurchsatzfähigkeit im Vergleich zum Bestätigungsmodus. Wie wir sehen werden, kann die Fenstergröße basierend auf den Empfangs- und Pufferkapazitäten der Empfangsseite sowie dem Netzwerkbelastungsgrad berechnet werden.
Weitere Beispiele für die Verwendung von Go zur Netzwerkarbeit.
In .
Quelle: habr.com
