
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
