Podstawy niezawodnej transmisji danych

Podstawy niezawodnej transmisji danych

Dla tych, którzy dąży pragną zrozumieć sieci i protokoły.

Krótko

W artykule omawiane są podstawy niezawodnej transmisji danych, realizowane są przykłady na Go, w tym UDP i TCP. Na podstawie raz, dwa, trzy i książki "Sieci komputerowe. Podejście od dołu", podczas gdy wszyscy dyskutują tylko o Tanenbaumie i Oliferach.

Protokół warstwy transportowej

zapewnia logiczne połączenie między procesami aplikacyjnymi działającymi na różnych hostach. Logiczne połączenie z punktu widzenia aplikacji wygląda jak kanał bezpośrednio łączący procesy.

Podstawy niezawodnej transmisji danych

Protokoły warstwy transportowej są wspierane przez systemy końcowe, ale nie przez routery sieciowe (z wyjątkiem — DPI). Po stronie nadawcy warstwa transportowa przekształca dane warstwy aplikacji, które otrzymuje od wysyłającego procesu aplikacyjnego, w pakiety warstwy transportowej, zwane segmentami.

Podstawy niezawodnej transmisji danych

Dzieje się to poprzez podział (w razie potrzeby) wiadomości warstwy aplikacji na fragmenty oraz dodanie do każdego z nich nagłówka warstwy transportowej.

Podstawy niezawodnej transmisji danych

Następnie warstwa transportowa przesyła segment do warstwy sieciowej nadawcy, gdzie segment jest inkapsulowany w pakiecie warstwy sieciowej (datagramie) i wysyłany. Po stronie odbierającej warstwa sieciowa wydobywa segment warstwy transportowej z datagramu i przesyła go w górę do warstwy transportowej. Następnie warstwa transportowa przetwarza otrzymany segment w taki sposób, aby jego dane stały się dostępne dla aplikacji odbierającej.

Podstawy niezawodnej transmisji danych

Zasady niezawodnej transmisji danych

Niezawodna transmisja danych przez całkowicie niezawodny kanał

Najprostszy przypadek. Strona nadawcza po prostu odbiera dane z wyższej warstwy, tworzy pakiet je zawierający i wysyła go przez kanał.

Serwer

package main

import (
    "log"
    "net"
)

func main() {
    // Adres IP serwera i port
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }

    // Tworzymy gniazdo z portem
    serverConn, err := net.ListenUDP("udp", serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // Opóźnione zamknięcie połączenia
    defer serverConn.Close()

    // Tworzymy bufor dla danych
    buf := make([]byte, 1024)

    // Czekamy na połączenie
    for {
        // Odczytujemy zapytanie
        n, addr, err := serverConn.ReadFromUDP(buf)
        // Przekazujemy dane do WARSTWY WYŻSZEJ: w naszym przypadku stdout
        println(string(buf[0:n]), " form ", addr.IP.String())
        if err != nil {
            log.Fatal(err)
        }
        // Brak odpowiedzi, ponieważ to UDP + niezawodny kanał
    }
}

Klient

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    // Adres IP serwera i port
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }
    // Lokalny adres IP i port
    localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
    if err != nil {
        log.Fatal(err)
    }
    // Ustanowienie połączenia
    conn, err := net.DialUDP("udp", localAddr, serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // Opóźnione zamknięcie połączenia
    defer conn.Close()

    for {
        // Odbieranie danych z WARSTWY WYŻSZEJ
        fmt.Print("Wprowadź zdanie tekstowe > ")
        var msg string
        _, err := fmt.Scanf("%s", &msg)
        if err != nil {
            log.Fatal(err)
        }
        // Przesyłane są bajty, a nie łańcuch
        buf := []byte(msg)
        // Zapis (przesłanie) do połączenia
        _, err = conn.Write(buf)
        if err != nil {
            log.Fatal(err)
        }
        // Chwilka przerwy
        time.Sleep(time.Second * 1)
    }
}

Niezawodne przesyłanie danych w kanałach z możliwymi błędami

Następnym etapem jest założenie, że wszystkie przesyłane pakiety zostały odebrane w tej samej kolejności, w jakiej zostały wysłane, ale bity w nich mogą być uszkodzone, ponieważ kanał czasami przesyła dane z zniekształceniami.

Podstawy niezawodnej transmisji danych

W takim przypadku stosuje się mechanizmy:

  • wykrywania błędów;
  • informacji zwrotnej;
  • powtórnego przesyłania.

Protokoły niezawodnego przesyłania danych, które zawierają podobne mechanizmy wielu powtórzeń przesyłania, nazywane są protokołami z automatycznym żądaniem powtórzenia (Automatic Repeat reQuest, ARQ).
Dodatkowo warto przewidzieć możliwość błędów również w potwierdzeniach, gdy strona odbierająca nie otrzyma żadnych informacji o wynikach przesyłu ostatniego pakietu.
Rozwiązaniem tego problemu, wykorzystywanym również w TCP, jest dodanie do pakietu danych nowego pola zawierającego numer sekwencyjny pakietu.

Podstawy niezawodnej transmisji danych

Niezawodne przesyłanie danych przez niezawodny kanał, dopuszczający zniekształcenia i utratę pakietów

Jednocześnie z zniekształceniami, niestety, w sieci występuje utrata pakietów.
I do rozwiązania tego problemu wymagane są mechanizmy:

  • określenia straty pakietów;
  • powtórnego dostarczenia zagubionych pakietów stronie odbierającej.

Dodatkowo, poza utratą pakietu, należy przewidzieć możliwość utraty potwierdzenia lub, jeśli nic nie zostało utracone, jego dostarczenia z istotnym opóźnieniem. W każdym przypadku podejmuje się tę samą akcję: ponowną transmisję pakietu. Do kontrolowania czasu w tym mechanizmie stosowany jest stoper, który pozwala określić zakończenie interwału oczekiwania. Tak więc w pakiecie net parametr TCPKeepAlive jest domyślnie ustawiony na 15 sekund:

// defaultTCPKeepAlive is a default constant value for TCPKeepAlive times
// See golang.org/issue/31510
const (
    defaultTCPKeepAlive = 15 * time.Second
)

Strona nadająca musi uruchamiać stoper za każdym razem podczas przesyłania pakietu (zarówno przy pierwszym, jak i przy ponownej transmisji), obsługiwać przerwania z timera i zatrzymać go.

Zatem zapoznaliśmy się z kluczowymi pojęciami protokołów niezawodnej transmisji danych:

  • sumami kontrolnymi;
  • numerami porządkowymi pakietów;
  • timerami;
  • pozytywnymi i negatywnymi potwierdzeniami.

Ale to jeszcze nie wszystko!

Protokół niezawodnej transmisji danych z potokowaniem

W rozważanym przez nas wariancie protokół niezawodnej dostawy jest bardzo nieefektywny. Zaczyna "spowalniać" transmisję, zapewnianą przez kanał komunikacyjny, wraz ze wzrostem RTT. Aby zwiększyć jego efektywność oraz lepsze wykorzystanie przepustowości kanału komunikacyjnego stosuje się potokowanie.

Podstawy niezawodnej transmisji danych

Zastosowanie potokowania prowadzi do:

  • zwiększenia zakresu numerów porządkowych, ponieważ wszystkie wysyłane pakiety (z wyjątkiem ponownych transmisji) muszą być jednoznacznie identyfikowalne;
  • konieczności zwiększenia buforów po stronie nadawczej i odbiorczej.

Zakres numerów porządkowych i wymagania dotyczące rozmiarów buforów zależą od działań podejmowanych przez protokół w odpowiedzi na zakłócenia, utratę i opóźnienie pakietu. W przypadku potokowania istnieją dwie metody korekcji błędów:

  • powrót o N pakietów;
  • wybórkowe powtórzenie.

Powrót o N pakietów — protokół okna przesuwnego

Podstawy niezawodnej transmisji danych

Nadawca musi śledzić trzy typy wydarzeń:

  • wywołanie protokołem wyższego poziomu. Kiedy funkcja wysyłania danych jest wywoływana „z góry”, strona wysyłająca najpierw sprawdza stopień zapełnienia okna (to jest obecność N wysłanych wiadomości oczekujących na potwierdzenie odbioru). Jeśli okno nie jest zapełnione, nowy pakiet jest formowany i przesyłany, a wartości zmiennych aktualizowane. W przeciwnym razie strona wysyłająca zwraca dane do poziomu wyższego, co stanowi niejawne wskazanie, że okno jest zapełnione. Zwykle poziom wyższy podejmuje próbę ponownego przesłania danych po pewnym czasie. W rzeczywistej aplikacji nadawca najprawdopodobniej albo buforowałby dane (zamiast natychmiastowej wysyłki), albo miałby mechanizm synchronizacji (na przykład semafor lub flaga), który pozwalałby poziomowi wyższemu wywołać funkcję wysyłania danych tylko gdy okno jest niezapełnione.
  • otrzymanie potwierdzenia. W protokole dla pakietu z numerem porządkowym N wydawana jest ogólna potwierdzenie wskazująca, że wszystkie pakiety z numerami porządkowymi poprzedzającymi N zostały pomyślnie przyjęte.
  • czas oczekiwania. W celu ustalenia faktów utraty i opóźnień pakietów i potwierdzeń protokół wykorzystuje stoper. Jeśli czas oczekiwania upływa, strona wysyłająca ponownie przesyła wszystkie wysłane niepotwierdzone pakiety.

Powtarzanie selektywne

Gdy rozmiar okna i iloczyn przepustowości oraz opóźnienia propagacji są duże, w potoku może znajdować się wiele pakietów. W takim przypadku błąd pojedynczego pakietu może spowodować ponowne wysłanie dużej liczby pakietów, z których większość nie była wymagana.

Przykład

Najlepsze teoretyczne praktyki zebrane w praktycznej realizacji TCP. A jeśli ktoś wie, jak lepiej — witamy.

Serwer

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "strings"
)

func main() {
    // tworzymy gniazdo z portem 
    ln, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatalln(err)
    }
    // oczekiwanie na wywołanie
    conn, _ := ln.Accept()

    for {
        // odczyt danych
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // wyświetlanie wiadomości w stdout
        fmt.Print("Otrzymano wiadomość:", string(msg))
        // zmiana wiadomości na wielkie litery
        newMsg := strings.ToUpper(msg)
        // wysyłanie danych
        conn.Write([]byte(newMsg + "n"))
    }
}

Klient

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "os"
)

func main() {
    // nawiązanie połączenia
    conn, err := net.Dial("tcp", "127.0.0.1:8081")
    if err != nil {
        log.Fatalln(err)
    }

    for {
        // odczyt danych z stdin
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Tekst do wysłania: ")
        // linia po linii
        text, err := reader.ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // wysyłanie
        fmt.Fprintf(conn, text+"n")
        // odbieranie
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // wyświetlenie otrzymanej odpowiedzi
        fmt.Print("Wiadomość z serwera: " + msg)
    }
}

Wnioski

Mechanizmy zapewniające niezawodne przesyłanie danych i ich użycie

Mechanizm
Zastosowanie, komentarz

Suma kontrolna
Używana do wykrywania błędów bitowych w przesyłanym pakiecie

Timer
Odliczanie czasu oczekiwania i wskazanie jego wygaśnięcia. Ostatnie oznacza, że istnieje duże prawdopodobieństwo, że pakiet lub jego potwierdzenie zostały utracone podczas przesyłania. W przypadku, gdy pakiet jest dostarczany z opóźnieniem, ale nie jest tracony (przedwczesne wygaśnięcie czasu oczekiwania) lub dochodzi do utraty potwierdzenia, ponowne przesłanie prowadzi do duplikacji pakietu po stronie odbiorcy

Numer porządkowy
Używana do numerowania porządkowego pakietów danych przesyłanych od nadawcy do odbiorcy. Luka w numerach porządkowych otrzymanych pakietów pozwala odbiorcy wykryć utratę pakietu. Identical numbers of packets mean that packets duplicate each other

Potwierdzenie
Generowane przez stronę odbierającą i informujące stronę nadawczą, że odpowiedni pakiet lub grupa pakietów zostały pomyślnie odebrane. Zazwyczaj potwierdzenie zawiera numery porządkowe pomyślnie odebranych pakietów. W zależności od protokołu wyróżnia się potwierdzenia indywidualne i grupowe

Potwierdzenie negatywne
Używane przez odbiorcę do informowania nadawcy, że pakiet został odebrany niepoprawnie. Potwierdzenie negatywne zazwyczaj zawiera numer porządkowy pakietu, który nie został prawidłowo odebrany

Okno, potokowanie
Ograniczają zakres numerów porządkowych, które mogą być używane do przesyłania pakietów. Przesyłanie grupowe i handshake znacznie zwiększają przepustowość protokołów w porównaniu do trybu oczekiwania na potwierdzenia. Jak zobaczymy, rozmiar okna może być obliczany na podstawie możliwości odbioru i buforowania po stronie odbiorcy, a także poziomu obciążenia sieci.

Inne przykłady użycia Go do pracy z siecią.

W repozytorium.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster