Do tych którzy
Krótko
W artykule omówiono podstawy niezawodnej transmisji danych, zaimplementowano przykłady nt
Protokół warstwy transportowej
Zapewnia logiczne połączenie pomiędzy procesami aplikacji działającymi na różnych hostach. Z punktu widzenia aplikacji połączenie logiczne wygląda jak kanał bezpośrednio łączący procesy.
Odbywa się to poprzez podzielenie (jeśli to konieczne) komunikatów warstwy aplikacji na fragmenty i dodanie do każdego z nich nagłówka warstwy transportowej.
Następnie warstwa transportowa przekazuje segment do warstwy sieci nadawcy, gdzie segment jest hermetyzowany w pakiecie warstwy sieciowej (datagram) i wysyłany. Po stronie odbiorczej warstwa sieciowa wyodrębnia segment warstwy transportowej z datagramu i przekazuje go do warstwy transportowej. Następnie warstwa transportowa przetwarza odebrany segment tak, aby jego dane stały się dostępne dla aplikacji odbierającej.
Zasady niezawodnej transmisji danych
Niezawodna transmisja danych całkowicie bezpiecznym kanałem
Najprostszy przypadek. Strona wysyłająca po prostu odbiera dane z wyższej warstwy, tworzy zawierający je pakiet i wysyła je do kanału.
Server
package main
import (
"log"
"net"
)
func main() {
// IP-адрес сервера и порт
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
if err != nil {
log.Fatal(err)
}
// создаем сокет с портом
serverConn, err := net.ListenUDP("udp", serverAddr)
if err != nil {
log.Fatal(err)
}
// отложенное закрытие соединения
defer serverConn.Close()
// создаем буфер для данных
buf := make([]byte, 1024)
// ждем соединение
for {
// читаем запрос
n, addr, err := serverConn.ReadFromUDP(buf)
// передаем данные в ВЕРХНИЙ уровень: в нашем случае stdout
println(string(buf[0:n]), " form ", addr.IP.String())
if err != nil {
log.Fatal(err)
}
// ответа нет, т.к. это UDP + надежный канал
}
}
Klient
package main
import (
"fmt"
"log"
"net"
"time"
)
func main() {
// IP-адрес сервера и порт
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
if err != nil {
log.Fatal(err)
}
// локальный IP-адрес и порт
localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
// установка соединения
conn, err := net.DialUDP("udp", localAddr, serverAddr)
if err != nil {
log.Fatal(err)
}
// отложенное закрытие соединения
defer conn.Close()
for {
// получение данных от ВЕРХНЕГО уровня
fmt.Print("Введите строчное предложение > ")
var msg string
_, err := fmt.Scanf("%s", &msg)
if err != nil {
log.Fatal(err)
}
// передается поток байт, а не строка
buf := []byte(msg)
// запись (передача) в соединение
_, err = conn.Write(buf)
if err != nil {
log.Fatal(err)
}
// 1 секундочку
time.Sleep(time.Second * 1)
}
}
Niezawodna transmisja danych kanałem z możliwymi błędami
Następnym krokiem jest założenie, że wszystkie przesyłane pakiety są odbierane w kolejności, w jakiej zostały wysłane, jednak zawarte w nich bity mogą być uszkodzone ze względu na fakt, że kanał czasami przesyła dane ze zniekształceniami.
W takim przypadku stosowane są następujące mechanizmy:
- wykrywanie błędów;
- informacja zwrotna;
- retransmisja.
Niezawodne protokoły przesyłania danych, które mają podobne mechanizmy wielokrotnego powtarzania transmisji, nazywane są protokołami Automatic Repeat ReQuest (ARQ).
Dodatkowo warto uwzględnić możliwość wystąpienia błędów w paragonach, gdy odbiorca nie otrzyma żadnej informacji o wynikach przekazania ostatniego pakietu.
Rozwiązanie tego problemu, stosowane również w protokole TCP, polega na dodaniu do pakietu danych nowego pola zawierającego numer sekwencyjny pakietu.
Niezawodna transmisja danych przez zawodny kanał narażony na zniekształcenia i utratę pakietów
Niestety wraz z zakłóceniami dochodzi do utraty pakietów w sieci.
Aby rozwiązać ten problem, wymagane są mechanizmy:
- ustalenie faktu utraty pakietów;
- ponowne dostarczenie utraconych pakietów do strony odbierającej.
Dodatkowo oprócz utraty przesyłki należy uwzględnić możliwość zagubienia paragonu lub, jeśli nic nie zginęło, jej doręczenia ze znacznym opóźnieniem. We wszystkich przypadkach robi się to samo: pakiet jest retransmitowany. Aby kontrolować czas, mechanizm ten wykorzystuje licznik czasu, który pozwala określić koniec okresu oczekiwania. Tak w pakiecie
// defaultTCPKeepAlive is a default constant value for TCPKeepAlive times
// See golang.org/issue/31510
const (
defaultTCPKeepAlive = 15 * time.Second
)
Strona wysyłająca musi uruchamiać licznik czasu za każdym razem, gdy pakiet jest przesyłany (zarówno za pierwszym, jak i za drugim razem), obsługiwać przerwania ze strony licznika czasu i go zatrzymywać.
Zapoznaliśmy się więc z kluczowymi koncepcjami niezawodnych protokołów przesyłania danych:
- sumy kontrolne;
- numery porządkowe pakietów;
- timery;
- wpływy pozytywne i negatywne.
Ale to nie wszystko!
Niezawodny protokół przesyłania danych z potokowaniem
W wariancie, który już rozważaliśmy, niezawodny protokół dostawy jest bardzo nieefektywny. Zaczyna „spowalniać” transmisję zapewnianą przez kanał komunikacyjny wraz ze wzrostem RTT. Aby zwiększyć jego efektywność i lepiej wykorzystać przepustowość kanału komunikacyjnego, stosuje się potokowanie.
Zastosowanie rurociągów prowadzi do:
- zwiększenie zakresu numerów sekwencyjnych, ponieważ wszystkie wysyłane pakiety (z wyjątkiem retransmisji) muszą być jednoznacznie identyfikowane;
- konieczność zwiększenia buforów po stronie nadawczej i odbiorczej.
Zakres numerów sekwencyjnych i wymagania dotyczące rozmiaru bufora zależą od działań podejmowanych przez protokół w odpowiedzi na uszkodzenie, utratę i opóźnienie pakietu. W przypadku potokowania istnieją dwie metody poprawiania błędów:
- zwróć N pakietów;
- selektywne powtarzanie.
Wracanie N pakietów - protokół przesuwanego okna
Nadawca musi obsługiwać trzy typy zdarzeń:
- połączenie przez protokół wyższego poziomu. Gdy funkcja wysyłania danych zostanie wywołana „z góry”, strona wysyłająca w pierwszej kolejności sprawdza stopień wypełnienia okna (czyli obecność N wysłanych komunikatów oczekujących na otrzymanie potwierdzeń). Jeśli okno jest puste, generowany i przesyłany jest nowy pakiet, a wartości zmiennych są aktualizowane. W przeciwnym razie strona wysyłająca zwraca dane do wyższej warstwy, co jest dorozumianą wskazówką, że okno jest pełne. Zwykle po pewnym czasie górna warstwa będzie próbowała ponownie przesłać dane. W prawdziwej aplikacji nadawca prawdopodobnie albo buforowałby dane (zamiast natychmiast je wysyłać), albo posiadał mechanizm synchronizacji (taki jak semafor lub flaga), który pozwalałby górnej warstwie wywoływać funkcję wysyłania tylko wtedy, gdy okno jest puste .
- otrzymania potwierdzenia. W protokole dla pakietu o numerze sekwencyjnym N wydawane jest ogólne potwierdzenie wskazujące, że wszystkie pakiety o numerach sekwencyjnych poprzedzających N zostały pomyślnie odebrane.
- upłynął okres oczekiwania. Aby określić fakty dotyczące strat i opóźnień pakietów i potwierdzeń, protokół wykorzystuje timer. Jeśli upłynie limit czasu, strona wysyłająca ponownie wysyła wszystkie wysłane niepotwierdzone pakiety.
Wybiórcze powtórzenie
Gdy rozmiar okna i produkt opóźnienia propagacji przepustowości są duże, w potoku może znajdować się duża liczba pakietów. W takim przypadku błąd pojedynczego pakietu może spowodować retransmisję dużej liczby pakietów, z których większość nie była wymagana.
Przykład
Najlepsze
Server
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
// создаем сокет с портом
ln, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatalln(err)
}
// ожидание вызова
conn, _ := ln.Accept()
for {
// считывание данных
msg, err := bufio.NewReader(conn).ReadString('n')
if err != nil {
log.Fatalln(err)
}
// вывод сообщения в stdout
fmt.Print("Message Received:", string(msg))
// перевод строки в верхний регистр
newMsg := strings.ToUpper(msg)
// отправка данных
conn.Write([]byte(newMsg + "n"))
}
}
Klient
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
)
func main() {
// установка соединения
conn, err := net.Dial("tcp", "127.0.0.1:8081")
if err != nil {
log.Fatalln(err)
}
for {
// считывание данных с stdin
reader := bufio.NewReader(os.Stdin)
fmt.Print("Text to send: ")
// построчно
text, err := reader.ReadString('n')
if err != nil {
log.Fatalln(err)
}
// отправка
fmt.Fprintf(conn, text+"n")
// прием
msg, err := bufio.NewReader(conn).ReadString('n')
if err != nil {
log.Fatalln(err)
}
// вывод полученного ответа
fmt.Print("Msg from Server: " + msg)
}
}
Wniosek
Mechanizmy zapewniające niezawodny transfer i wykorzystanie danych
mechanizm
Aplikacja, komentarz
Sprawdź sumę
Służy do wykrywania błędów bitowych w przesyłanym pakiecie
Regulator czasowy
Odlicza limit czasu i wskazuje, kiedy upłynął. To drugie oznacza, że z dużym prawdopodobieństwem pakiet lub jego odbiór zostanie utracony podczas transmisji. Jeżeli pakiet został dostarczony z opóźnieniem, ale nie został utracony (przedwczesny upłynięcie limitu czasu) lub utracono potwierdzenie, retransmisja prowadzi do zduplikowania pakietu po stronie odbierającej
Numer seryjny
Służy do sekwencyjnego numerowania pakietów danych przesyłanych od nadawcy do odbiorcy. Luki w numerach sekwencji odebranych pakietów umożliwiają odbiornikowi wykrycie utraty pakietów. Te same numery sekwencji pakietów oznaczają, że pakiety są duplikatami siebie
Potwierdzenie
Generowane przez stronę odbiorczą i wskazujące stronie wysyłającej, że odpowiedni pakiet lub grupa pakietów została pomyślnie odebrana. Zazwyczaj potwierdzenie zawiera numery kolejne pomyślnie odebranych pakietów. W zależności od protokołu rozróżnia się potwierdzenia indywidualne i grupowe
Negatywne potwierdzenie
Używane przez odbiorcę w celu poinformowania nadawcy, że pakiet został odebrany nieprawidłowo. Negatywne potwierdzenie zwykle zawiera numer kolejny pakietu, który nie został poprawnie odebrany
Okno, przenośnik
Ogranicz zakres numerów sekwencyjnych, których można używać do przesyłania pakietów. Multicast i uzgadnianie mogą znacząco zwiększyć przepustowość protokołu w porównaniu z oczekiwaniem na potwierdzenia. Jak zobaczymy, rozmiar okna można obliczyć na podstawie możliwości odbioru i buforowania strony odbiorczej, a także poziomu obciążenia sieci
Więcej przykładów wykorzystania Go do networkingu
В
Źródło: www.habr.com