Grundlagen der zuverlässigen Datenübertragung

Grundlagen der zuverlässigen Datenübertragung

An diejenigen, die verstehen wollen , 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 Gogegeben, darunter UDP und TCP. Inspiriert von eins, zwei, drei 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.

Grundlagen der zuverlässigen Datenübertragung

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

Grundlagen der zuverlässigen Datenübertragung

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

Grundlagen der zuverlässigen Datenübertragung

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.

Grundlagen der zuverlässigen Datenübertragung

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.

Grundlagen der zuverlässigen Datenübertragung

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.

Grundlagen der zuverlässigen Datenübertragung

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

Grundlagen der zuverlässigen Datenübertragung

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

Grundlagen der zuverlässigen Datenübertragung

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 theoretischen Praktiken sind in einer praktischen Umsetzung gesammelt TCP. Und wenn jemand weiß, wie es besser geht — Willkommen.

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

Quelle: habr.com

60GB SSD 8Gb DDR4