
Dla tych, którzy pragną zrozumieć sieci i protokoły.
Krótko
W artykule omawiane są podstawy niezawodnej transmisji danych, realizowane są przykłady na , w tym UDP i TCP. Na podstawie , , 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.

są wspierane przez systemy końcowe, ale nie przez routery sieciowe (z wyjątkiem — ). 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.

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.

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.

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.

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.

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

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

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 praktyki zebrane w praktycznej realizacji . A jeśli ktoś wie, jak lepiej — .
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 .
Źródło: habr.com
