Internet od dawna się zmienił. Jednym z podstawowych protokołów Internetu jest UDP, który służy aplikacjom nie tylko do dostarczania datagramów i rozsyłania multicastowego, ale także do zapewnienia połączeń typu „peer-to-peer” między węzłami sieci. Ze względu na swoją prostą strukturę, ten protokół zyskał wiele nieplanowanych wcześniej zastosowań, jednak wady protokołu, takie jak brak gwarancji dostarczenia, nie zniknęły. W tym artykule opisano realizację protokołu z gwarantowanym dostarczeniem na bazie UDP.
Zawartość:
Wprowadzenie
Początkowa architektura Internetu zakładała jednorodne przestrzenie adresowe, w której każdy węzeł miał globalny i unikalny adres IP i mógł bezpośrednio komunikować się z innymi węzłami. Obecnie Internet w rzeczywistości ma inną architekturę – jeden obszar globalnych adresów IP i wiele obszarów z adresami prywatnymi, ukrytymi za urządzeniami NAT.W takiej architekturze tylko urządzenia znajdujące się w globalnej przestrzeni adresowej mogą swobodnie komunikować się z innymi w sieci, ponieważ mają unikalny, globalny adres IP umożliwiający routowanie. Węzeł znajdujący się w prywatnej sieci może łączyć się z innymi węzłami w tej samej sieci, a także z dobrze znanymi węzłami w globalnej przestrzeni adresowej. Taka interakcja jest w dużej mierze możliwa dzięki mechanizmowi konwersji adresów sieciowych. Urządzenia NAT, na przykład routery Wi-Fi, tworzą specjalne wpisy w tabelach translacji dla połączeń wychodzących i modyfikują adresy IP oraz numery portów w pakietach. Pozwala to na ustanawianie połączeń wychodzących z prywatnej sieci z węzłami w globalnej przestrzeni adresowej. Jednak jednocześnie urządzenia NAT zazwyczaj blokują cały ruch przychodzący, chyba że są ustawione osobne zasady dla połączeń przychodzących.
Taka architektura Internetu jest wystarczająco poprawna dla interakcji klient-serwer, gdy klienci mogą znajdować się w prywatnych sieciach, a serwery mają globalny adres. Tworzy to jednak trudności dla bezpośredniego połączenia dwóch węzłów pomiędzy różnymi prywatnymi sieciami. Bezpośrednie połączenie dwóch węzłów jest ważne dla aplikacji 'peer-to-peer', takich jak transmisja głosu (Skype), uzyskiwanie zdalnego dostępu do komputera (TeamViewer) czy gry online.
Jedną z najskuteczniejszych metod ustanawiania połączenia peer-to-peer między urządzeniami znajdującymi się w różnych prywatnych sieciach jest 'hole punching'. Ta technika jest najczęściej używana w aplikacjach opartych na protokole UDP.
Jednak jeśli Twoja aplikacja wymaga gwarantowanej dostawy danych, na przykład przy przesyłaniu plików między komputerami, korzystanie z UDP wiąże się z wieloma trudnościami, ponieważ UDP nie jest protokołem gwarantującym dostawę i nie zapewnia dostarczania pakietów w kolejnności, w przeciwieństwie do protokołu TCP.
W takim przypadku, aby zapewnić gwarantowaną dostawę pakietów, konieczne jest wdrożenie protokołu warstwy aplikacji, który zapewni wymaganą funkcjonalność i będzie działał na wierzchu UDP.
Na początku chciałbym zauważyć, że istnieje technika TCP hole punching, służąca do nawiązywania połączeń TCP między węzłami w różnych prywatnych sieciach, jednak z powodu braku wsparcia ze strony wielu urządzeń NAT zwykle nie jest ona rozważana jako główny sposób łączenia takich węzłów.
W dalszej części tego artykułu będę rozważał tylko implementację protokołu gwarantującego dostawę. Implementacja techniki UDP hole punching zostanie opisana w kolejnych artykułach.
Wymagania dotyczące protokołu
- Niezawodna dostawa pakietów jest realizowana za pomocą mechanizmu pozytywnego potwierdzenia (tzw. positive acknowledgment).
- Konieczność efektywnego przesyłania dużych danych, tj. protokół powinien unikać zbędnej retransmisji pakietów.
- Powinna istnieć możliwość dezaktywacji mechanizmu potwierdzania dostawy (możliwość działania jako 'czysty' protokół UDP).
- Możliwość wdrożenia trybu poleceń z potwierdzeniem każdego komunikatu.
- Podstawową jednostką przesyłania danych w protokole powinien być komunikat.
Te wymagania w dużej mierze pokrywają się z wymaganiami do Reliable Data Protocol, opisanymi w i , i oparłem się na tych standardach podczas tworzenia tego protokołu.
Aby zrozumieć te wymagania, przyjrzyjmy się czasowym wykresom przesyłania danych między dwoma węzłami sieci według protokołów TCP i UDP. Załóżmy, że w obu przypadkach utracony zostanie jeden pakiet.
Przesyłanie danych nieinteraktywnych przez TCP:
Jak widać na wykresie, w przypadku utraty pakietów, TCP wykryje utracony pakiet i powiadomi o tym nadawcę, żądając numeru utraconego segmentu.
Przesyłanie danych przez protokół UDP:
UDP nie podejmuje żadnych działań w celu wykrywania strat. Kontrola błędów transmisji w protokole UDP całkowicie spoczywa na aplikacji.
Wykrywanie błędów w protokole TCP osiągane jest dzięki nawiązywaniu połączenia z końcowym węzłem, utrzymywaniu stanu tego połączenia, wskazywaniu numeru wysłanych bajtów w każdym nagłówku pakietu oraz powiadomieniom o odbiorze za pomocą numeru potwierdzenia 'acknowledge number'.
Dodatkowo, aby zwiększyć wydajność (tj. wysłać więcej niż jeden segment bez otrzymywania potwierdzenia), protokół TCP korzysta z tzw. okna przesyłania — liczby bajtów danych, które nadawca segmentu oczekuje na przyjęcie.
Szczegółowe informacje na temat protokołu TCP można znaleźć w , z UDP w , gdzie są one, właściwie mówiąc, zdefiniowane.
Z powyższego opisu wynika, że aby stworzyć niezawodny protokół dostarczania wiadomości na bazie UDP (dalej nazywany Reliable UDP), należy zaimplementować mechanizmy przesyłania danych podobne do TCP. A mianowicie:
- utrzymywać stan połączenia
- używać numeracji segmentów
- używać specjalnych pakietów potwierdzających
- używać uproszczonego mechanizmu okna w celu zwiększenia przepustowości protokołu
Dodatkowo, należy:
- sygnalizować rozpoczęcie wiadomości, aby przydzielić zasoby pod połączenie
- sygnalizować zakończenie wiadomości, aby przekazać otrzymaną wiadomość wyższemu aplikacji i zwolnić zasoby protokołu
- pozwolić protokołowi dla określonych połączeń wyłączyć mechanizm potwierdzeń dostawy, aby działać jako „czysty” UDP
Nagłówek Reliable UDP
Przypomnijmy, że datagram UDP jest enkapsulowany w datagramie IP. Pakiet Reliable UDP odpowiednio jest „opakowywany” w datagram UDP.
Enkapsulacja nagłówka Reliable UDP:
Struktura nagłówka Reliable UDP jest dość prosta:

- Flags – flagi kontrolne pakietu
- MessageType – typ wiadomości, wykorzystywany przez wyższe aplikacje do subskrybcji na określone wiadomości
- TransmissionId — numer transmisji, wraz z adresem i portem odbiorcy, unikalnie określa połączenie
- PacketNumber – numer pakietu
- Options – dodatkowe opcje protokołu. W przypadku pierwszego pakietu służy do wskazania rozmiaru wiadomości
Flagi są następujące:
- FirstPacket — pierwszy pakiet wiadomości
- NoAsk — wiadomość nie wymaga włączenia mechanizmu potwierdzenia
- LastPacket — ostatni pakiet wiadomości
- RequestForPacket — pakiet potwierdzający lub żądanie utraconego pakietu
Ogólne zasady działania protokołu
Ponieważ Reliable UDP jest ukierunkowany na gwarantowane dostarczanie wiadomości między dwoma węzłami, musi umieć nawiązać połączenie z drugą stroną. Aby nawiązać połączenie, strona nadająca wysyła pakiet z flagą FirstPacket, na który odpowiedź będzie oznaczała nawiązanie połączenia. Wszystkie pakiety odpowiedzi, lub inaczej, pakiety potwierdzające, zawsze mają wartość pola PacketNumber o jeden większą niż największa wartość PacketNumber u pomyślnie odebranych pakietów. W polu Options dla pierwszego wysłanego pakietu zapisywany jest rozmiar wiadomości.
Do zakończenia połączenia używany jest podobny mechanizm. W ostatniej paczce wiadomości ustawiany jest znacznik LastPacket. W odpowiedniej paczce podawany jest numer ostatniej paczki + 1, co dla strony odbierającej oznacza pomyślną dostawę wiadomości.
Diagram ustanawiania i zakończenia połączenia:
Gdy połączenie zostanie nawiązane, rozpoczyna się przesyłanie danych. Dane przesyłane są w blokach paczek. Każdy blok, z wyjątkiem ostatniego, zawiera ustaloną liczbę paczek. Równa jest ona rozmiarowi okna odbioru/nadawania. Ostatni blok danych może zawierać mniejszą liczbę paczek. Po wysłaniu każdego bloku strona nadająca oczekuje na potwierdzenie dostawy lub żądanie ponownej wysyłki zagubionych paczek, pozostawiając otwarte okno odbioru/nadawania na odbieranie odpowiedzi. Po otrzymaniu potwierdzenia dostawy bloku, okno odbioru/nadawania przesuwa się, a następnie wysyłany jest kolejny blok danych.
Strona odbierająca przyjmuje paczki. Każda paczka jest sprawdzana pod kątem trafienia w okno przesyłania. Paczki, które nie mieszczą się w oknie oraz duplikaty są odrzucane. Ponieważ rozmiar okna jest ściśle ustalony i taki sam dla odbiorcy i nadawcy, w przypadku dostarczenia bloku paczek bez strat okno przesuwa się w celu przyjęcia paczek następnego bloku danych i wysyłane jest potwierdzenie dostawy. Jeśli okno nie zostanie wypełnione w ustalonym czasie roboczym, zostanie uruchomiona kontrola, jakie paczki nie zostały dostarczone i zostaną wysłane żądania ponownej dostawy.
Diagram ponownej transmisji:
Czasy oczekiwania i timery protokołu
Istnieje kilka powodów, dla których połączenie może nie zostać nawiązane. Na przykład, jeśli strona odbierająca jest offline. W takim przypadku, przy próbie nawiązania połączenia, połączenie zostanie zamknięte z powodu przekroczenia czasu. W implementacji Reliable UDP wykorzystuje się dwa timery do ustalenia limitów czasowych. Pierwszy, timer roboczy, służy do oczekiwania na odpowiedź od zdalnego hosta. Jeśli zadziała po stronie nadawcy, następuje ponowne wysłanie ostatnio wysłanej paczki. Jeśli jednak timer zadziała po stronie odbiorcy, następuje kontrola utraconych paczek i wysyłane są żądania ponownej dostawy.
Drugi timer jest niezbędny do zamknięcia połączenia w przypadku braku komunikacji między węzłami. Dla strony wysyłającej uruchamia się od razu po aktywacji timera roboczego i czeka na odpowiedź od zdalnego węzła. W przypadku braku odpowiedzi w ustalonym okresie, połączenie zostaje zakończone, a zasoby zwolnione. Dla strony odbierającej, timer zamknięcia połączenia uruchamia się po podwójnym wyzwoleniu timera roboczego. Jest to niezbędne jako zabezpieczenie przed utratą pakietu potwierdzającego. Po wyzwoleniu timera połączenie również zostaje zakończone, a zasoby zwolnione.
Diagram stanów przesyłu Reliable UDP
Zasady działania protokołu zrealizowane są w automacie skończonym, którego każde stany odpowiada za określoną logikę przetwarzania pakietów.
Diagram stanów Reliable UDP:

Zamknięte – w rzeczywistości nie jest stanem, jest to punkt startowy i końcowy dla automatu. Stanem Zamknięte jest blok zarządzający transmisją, który, implementując asynchroniczny serwer UDP, przekierowuje pakiety do odpowiednich połączeń i uruchamia przetwarzanie stanów.
PierwszeWysyłaniePakietu – stan początkowy, w którym znajduje się wychodzące połączenie podczas wysyłania wiadomości.
W tym stanie wysyłany jest pierwszy pakiet dla zwykłych wiadomości. Dla wiadomości bez potwierdzenia odbioru, jest to jedyny stan – w nim następuje wysyłka całej wiadomości.
CyklWysyłania – główny stan do przesyłania pakietów wiadomości.
Przechodzenie do niego ze stanu PierwszeWysyłaniePakietu następuje po wysłaniu pierwszego pakietu wiadomości. Właśnie w tym stanie przyjmowane są wszystkie potwierdzenia i prośby o ponowne przesyłanie. Wyjście z niego możliwe jest w dwóch przypadkach – w przypadku pomyślnej dostawy wiadomości lub po przekroczeniu czasu.
PierwszyPakietOdebrany – stan początkowy dla odbiorcy wiadomości.
W nim sprawdzana jest poprawność rozpoczęcia przesyłania, tworzone są niezbędne struktury, i wysyłane jest potwierdzenie o odebraniu pierwszego pakietu.
Dla wiadomości składającej się z jednego pakietu i wysłanej bez potwierdzenia dostawy – jest to jedyny stan. Po przetworzeniu takiej wiadomości połączenie zostaje zamknięte.
Składanie – główny stan do odbierania pakietów wiadomości.
Rejestracja pakietów odbywa się w pamięci podręcznej, sprawdzanie utraty pakietów, wysyłanie potwierdzeń dostarczenia bloków pakietów i komunikatów całkowitych oraz wysyłanie żądań ponownego dostarczenia utraconych pakietów. W przypadku pomyślnego otrzymania całego komunikatu połączenie przechodzi w stan Completed, w przeciwnym razie następuje timeout.
Completed – zamknięcie połączenia w przypadku pomyślnego otrzymania całego komunikatu.
Ten stan jest niezbędny do złożenia komunikatu oraz w przypadku, gdy potwierdzenie dostarczenia komunikatu zostało zgubione w drodze do nadawcy. Wyjście z tego stanu następuje po upływie czasu, lecz połączenie uznaje się za pomyślnie zamknięte.
Głębiej w kodzie. Blok zarządzania przesyłem
Jednym z kluczowych elementów Reliable UDP jest blok zarządzania połączeniem. Zadaniem tego bloku jest przechowywanie bieżących połączeń oraz elementów pomocniczych, rozdzielanie przychodzących pakietów do odpowiednich połączeń, zapewnienie interfejsu do wysyłania pakietów do połączenia oraz realizowanie API protokołu. Blok zarządzania połączeniem odbiera pakiety z poziomu UDP i przekierowuje je do przetwarzania w automacie stanowym. Do odbierania pakietów zaimplementowano w nim asynchroniczny serwer UDP.
Niektóre człony klasy ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// tablica bajtów dla danego klucza. Używana do składania przychodzących komunikatów
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
// tablica bajtów dla danego klucza. Używana do wysyłania wychodzących komunikatów.
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
// rekord połączenia dla danego klucza.
private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
// lista subskrybentów komunikatów.
private readonly List<ReliableUdpSubscribeObject> m_subscribers;
// lokalny gniazdko
private Socket m_socketIn;
// port dla przychodzących komunikatów
private int m_port;
// lokalny adres IP
private IPAddress m_ipAddress;
// lokalny punkt końcowy
public IPEndPoint LocalEndpoint { get; private set; }
// kolekcja wstępnie zainicjalizowanych
// stanów automatu stanowego
public StatesCollection States { get; private set; }
// generator liczb losowych. Używany do tworzenia TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementacja asynchronicznego serwera UDP:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// tworzymy nowy bufor dla każdego socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// przekazujemy bufor jako parametr dla asynchronicznej metody
this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}
private void EndReceive(IAsyncResult ar)
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
//pakiet odebrany, gotowi do odbierania następnego
Receive();
// ponieważ najprostszym sposobem na rozwiązanie problemu z buforem jest uzyskanie do niego odniesienia
// z IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// otrzymujemy nagłówek pakietu
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// nadeszła nieprawidłowa paczka - odrzucamy ją
return;
}
// konstrukcja klucza do określenia rekordu połączenia dla pakietu
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// uzyskujemy istniejący rekord połączenia lub tworzymy nowy
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// uruchamiamy przetwarzanie pakietu w automacie stanowym
record.State.ReceivePacket(record, header, bytes);
}
Dla każdej transmisji wiadomości tworzona jest struktura zawierająca informacje o połączeniu. Taka struktura nazywana jest connection record.
Niektóre człony klasy ReliableUdpConnectionRecord:
wewnętrzna klasa ReliableUdpConnectionRecord : IDisposable
{
// tablica bajtów z wiadomością
public byte[] IncomingStream { get; set; }
// odnośnik do stanu automatów skończonych
public ReliableUdpState State { get; set; }
// para, jednoznacznie określająca rekord połączenia
// w bloku kontroli przesyłania
public Tuple Key { get; private set;}
// dolna granica okna odbiorczego
public int WindowLowerBound;
// rozmiar okna przesyłania
public readonly int WindowSize;
// numer pakietu do wysłania
public int SndNext;
// liczba pakietów do wysłania
public int NumberOfPackets;
// numer przesyłania (to jest druga część Tuple)
// dla każdej wiadomości własny
public readonly Int32 TransmissionId;
// zdalny punkt końcowy IP – faktyczny odbiorca wiadomości
public readonly IPEndPoint RemoteClient;
// rozmiar pakietu, aby uniknąć fragmentacji na poziomie IP
// nie powinien przekraczać MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
public readonly int BufferSize;
// blok kontroli przesyłania
public readonly ReliableUdpConnectionControlBlock Tcb;
// inkapsuluje wyniki operacji asynchronicznej dla BeginSendMessage/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
// nie wysyłać pakietów potwierdzających
public bool IsNoAnswerNeeded;
// ostatni poprawnie odebrany pakiet (zawsze ustawiany na najwyższy numer)
public int RcvCurrent;
// tablica z numerami utraconych pakietów
public int[] LostPackets { get; private set; }
// czy ostatni pakiet został odebrany. Używane jako bool.
public int IsLastPacketReceived = 0;
//...
}
Głębiej w kodzie. Stany
Stany implementują automat skończony protokołu Reliable UDP, w którym odbywa się główna obsługa pakietów. Abstrakcyjna klasa ReliableUdpState zapewnia interfejs dla stanu:

Cała logika działania protokołu jest realizowana przez powyższe klasy, a także przez pomocniczą klasę, dostarczającą metody statyczne, takie jak na przykład budowa nagłówka ReliableUdp z rekordu połączenia.
Następnie szczegółowo omówione zostaną implementacje metod interfejsu, które określają główne algorytmy działania protokołu.
Metoda DisposeByTimeout
Metoda DisposeByTimeout odpowiada za zwolnienie zasobów połączenia po upływie czasu oraz za sygnalizowanie udanej/nieudanej dostawy wiadomości.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Został on nadpisany tylko w stanie Completed.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// inform about the successful receipt of the message
SetAsCompleted(connectionRecord);
}
Metoda ProcessPackets
Metoda ProcessPackets odpowiada za dodatkowe przetwarzanie pakietu lub pakietów. Jest wywoływana bezpośrednio lub poprzez timer oczekiwania na pakiety.
W stanie Składanie metoda została nadpisana i odpowiada za sprawdzanie utraconych pakietów oraz przejście do stanu Completed, po otrzymaniu ostatniego pakietu i pomyślnym wykonaniu testu
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// są utracone pakiety, wysyłamy zapytania o nie
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// ustawiamy timer po raz drugi, aby spróbować ponownie wysłać
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// jeśli po dwóch próbach wyzwolenia WaitForPacketTimer
// nie udało się uzyskać pakietów - uruchamiamy timer zamknięcia połączenia
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// pomyślne potwierdzenie
{
// wysyłamy potwierdzenie odbioru bloku danych
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// zamiast natychmiastowej realizacji zasobów
// uruchamiamy timer, na wypadek, gdyby
// ostatni ack nie dotarł do nadawcy i ten zażąda go ponownie.
// przy wyzwoleniu timera - realizujemy zasoby
// w stanie Completed metoda timera została nadpisana
StartCloseWaitTimer(connectionRecord);
}
// to przypadek, w którym ack na blok pakietów został utracony
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// uruchamiamy timer zamknięcia połączenia
StartCloseWaitTimer(connectionRecord);
}
}
W stanie CyklWysyłania ta metoda jest wywoływana tylko przez timer i odpowiada za ponowne wysłanie ostatniej wiadomości oraz uruchomienie timera zamknięcia połączenia.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// wysyłamy ponownie ostatni pakiet
// (w przypadku przywrócenia połączenia, węzeł odbierający ponownie wyśle żądania, które do niego nie dotarły)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// włączamy timer CloseWait – aby czekać na przywrócenie połączenia lub jego zakończenie
StartCloseWaitTimer(connectionRecord);
}
W stanie Completed metoda zatrzymuje działający timer i przekazuje wiadomość subskrybentom.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// zbieramy wiadomość i przekazujemy ją subskrybentom
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Metoda ReceivePacket
W stanie PierwszyPakietOdebrany głównym celem metody jest określenie, czy rzeczywiście pierwszy pakiet wiadomości dotarł do interfejsu, a także zebranie wiadomości składającej się z jednego pakietu.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// odrzucamy pakiet
return;
// kombinacja dwóch flag - FirstPacket i LastPacket - mówi, że mamy jedną wiadomość
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// wysyłamy potwierdzenie pakietu
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// z założenia wszystkie numery pakietów zaczynają się od 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// obliczamy liczbę pakietów, które powinny przyjść
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
// zapisujemy numer ostatnio odebranego pakietu (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// po przesunięciu okna odbiorczego o 1
connectionRecord.WindowLowerBound++;
// przełączamy stan
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// jeśli nie wymaga mechanizmu potwierdzenia
// uruchamiamy timer, który zwolni wszystkie struktury
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
else
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
W stanie CyklWysyłania Ta metoda została nadpisana, aby odbierać potwierdzenia dostawy i żądania ponownego przesłania.
SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// obliczanie górnej granicy okna
// bierze się granicę okna + 1, aby uzyskać potwierdzenia dostarczenia
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// sprawdzenie, czy wpada w okno
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// sprawdzenie ostatniego pakietu:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// przekazanie zakończone
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// to jest odpowiedź na pierwszy pakiet z potwierdzeniem
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// bez przesunięcia okna
SendPacket(connectionRecord);
}
// otrzymano potwierdzenie odbioru bloku danych
else if (header.PacketNumber == windowHighestBound)
{
// przesuwamy okno odbioru/wysyłania
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// zeruje tablicę kontroli przesyłu
connectionRecord.WindowControlArray.Nullify();
// wysyłamy blok pakietów
SendPacket(connectionRecord);
}
// to jest prośba o retransmisję – wysyłamy wymagany pakiet
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
W stanie Składanie W metodzie ReceivePacket odbywa się główna praca związana z składaniem wiadomości z przychodzących pakietów.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// przetwarzanie pakietów z wyłączonym mechanizmem potwierdzania dostawy
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// resetujemy timer
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// zapisujemy dane
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// jeśli otrzymaliśmy pakiet z ostatnim flagą - kończymy
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// obliczamy górną granicę okna
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// odrzucamy pakiety, które nie mieszczą się w oknie
if (header.PacketNumber (windowHighestBound))
return;
// odrzucamy duplikaty
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// zapisujemy dane
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// zwiększamy licznik pakietów
connectionRecord.PacketCounter++;
// zapisujemy w tablicy zarządzania oknem bieżący numer pakietu
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// ustawiamy największy odebrany pakiet
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// restartujemy timery
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// jeśli nadszedł ostatni pakiet
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// jeśli otrzymaliśmy wszystkie pakiety okna, resetujemy licznik
// i wysyłamy pakiet potwierdzający
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resetujemy licznik.
connectionRecord.PacketCounter = 0;
// przesuwamy okno transmisji
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// resetujemy tablicę zarządzania transmisją
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// jeśli ostatni pakiet już istnieje
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// sprawdzamy pakiety
ProcessPackets(connectionRecord);
}
}
W stanie Completed Jedynym zadaniem metody jest wysłanie powtórnego potwierdzenia o pomyślnym dostarczeniu wiadomości.
Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ponowne wysłanie ostatniej paczki z powodu,
// że ostatnie ack nie dotarło do nadawcy
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Metoda SendPacket
W stanie PierwszeWysyłaniePakietu ta metoda przeprowadza wysyłkę pierwszej paczki danych lub, jeśli wiadomość nie wymaga potwierdzenia dostarczenia — całej wiadomości.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// jeśli potwierdzenie nie jest wymagane - wysyłamy wszystkie paczki
// i zwalniamy zasoby
if (connectionRecord.IsNoAnswerNeeded)
{
// Tutaj odbywa się wysyłka As Is
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// tworzymy nagłówek paczki i wysyłamy go
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// zwiększamy licznik
connectionRecord.SndNext++;
// przesuwamy okno
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Uruchamiamy timer
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
W stanie CyklWysyłania w tej metodzie odbywa się wysyłka bloku paczek.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// wysyłamy blok paczek
for (connectionRecord.PacketCounter = 0;
connectionRecord.PacketCounter < connectionRecord.WindowSize &&
connectionRecord.SndNext < connectionRecord.NumberOfPackets;
connectionRecord.PacketCounter++)
{
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
connectionRecord.SndNext++;
}
// w przypadku dużego okna transmisji, uruchamiamy timer na nowo po wysyłce
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
Głębiej w kodzie. Tworzenie i ustanawianie połączeń
Teraz, gdy zapoznaliśmy się z podstawowymi stanami i metodami używanymi do obsługi stanów, możemy bliżej przyjrzeć się kilku przykładom działania protokołu.
Diagram przesyłania danych w normalnych warunkach:
Przyjrzyjmy się dokładniej tworzeniu connection record Aby nawiązać połączenie i wysłać pierwszy pakiet, inicjatorem transferu zawsze jest aplikacja wywołująca metodę API do wysyłania wiadomości. Następnie używana jest metoda StartTransmission bloku zarządzania transferem, która rozpoczyna transfer danych dla nowej wiadomości.
Tworzenie połączenia wychodzącego:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "Musisz użyć konstruktora z parametrami lub uruchomić nasłuch przed wysłaniem wiadomości" );
}
// uruchamiamy przetwarzanie przychodzących pakietów
StartListener(LocalEndpoint);
}
// tworzymy klucz dla słownika na podstawie EndPoint i ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// generujemy losowy numer transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// tworzymy nowy rekord dla połączenia i sprawdzamy,
// czy taki numer już istnieje w naszych słownikach
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// jeśli istnieje – to ponownie generujemy losowy numer
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// jeśli ponownie się nie udało – generujemy wyjątek
throw new ArgumentException("Para TransmissionId & EndPoint już istnieje w słowniku");
}
// uruchomiliśmy stan do przetwarzania
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Wysyłanie pierwszego pakietu (stan FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// tworzymy nagłówek pakietu i wysyłamy go
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// zwiększamy licznik
connectionRecord.SndNext++;
// przesuwamy okno
connectionRecord.WindowLowerBound++;
// przechodzimy w stan SendingCycle
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Uruchamiamy timer
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Po wysłaniu pierwszego pakietu nadawca przechodzi w stan CyklWysyłania – oczekiwanie na potwierdzenie dostarczenia pakietu.
Strona odbierająca, za pomocą metody EndReceive, odbiera wysłany pakiet, tworzy nowy connection record i przekazuje ten pakiet, z wcześniej sparsowanym nagłówkiem, do przetworzenia przez metodę ReceivePacket stanu PierwszyPakietOdebrany
Tworzenie połączenia po stronie odbierającej:
private void EndReceive(IAsyncResult ar)
{
\/\/ ...
\/\/ pakiet odebrany
\/\/ analizujemy nagłówek pakietu
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
\/\/ nadesłany pakiet jest niepoprawny - odrzucamy go
return;
}
\/\/ konstruujemy klucz do określenia rekordu połączenia dla pakietu
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
\/\/ pobieramy istniejący rekord połączenia lub tworzymy nowy
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
\/\/ uruchamiamy pakiet w przetwarzaniu w automacie stanów
record.State.ReceivePacket(record, header, bytes);
}
Odbiór pierwszego pakietu i wysłanie potwierdzenia (stan FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
\/\/ odrzucamy pakiet
return;
\/\/ ...
\/\/ z założenia wszystkie numery pakietów zaczynają się od 0;
if (header.PacketNumber != 0)
return;
\/\/ inicjujemy tablicę do przechowywania części wiadomości
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
\/\/ zapisujemy dane pakietu w tablicy
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
\/\/ obliczamy liczbę pakietów, które powinny nadejść
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
\/\/ zapisujemy numer ostatnio odebranego pakietu (0)
connectionRecord.RcvCurrent = header.PacketNumber;
\/\/ przesuwamy okno odbioru o 1
connectionRecord.WindowLowerBound++;
\/\/ przełączamy stan
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (\/\/ jeśli mechanizm potwierdzenia nie jest wymagany)
\/\/ ...
else
{
\/\/ wysyłamy potwierdzenie
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Głębiej w kodzie. Zamykanie połączenia z powodu wygaśnięcia czasu
Obsługa opóźnień to ważna część Reliable UDP. Rozważmy przykład, w którym na węźle pośrednim wystąpiła awaria i dostarczenie danych w obu kierunkach stało się niemożliwe.
Diagram zamykania połączenia z powodu opóźnienia:
Jak widać na diagramie, aktywny timer u nadawcy uruchamia się natychmiast po wysłaniu bloku pakietów. Dzieje się to w metodzie SendPacket stanu CyklWysyłania.
Aktywacja aktywnego timera (stan SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ wysyłamy blok pakietów
\/\/ ...
\/\/ ponownie uruchamiamy timer po wysłaniu
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
Okresy timera są ustawiane podczas nawiązywania połączenia. Domyślnie ShortTimerPeriod wynosi 5 sekund. W przykładzie jest ustawiony na 1,5 sekundy.
W przypadku połączenia przychodzącego timer uruchamia się po otrzymaniu ostatniego dotartego pakietu danych, co ma miejsce w metodzie ReceivePacket stanu Składanie
Uruchomienie roboczego timera (stan Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// restartujemy timery
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
W przypadku połączenia przychodzącego w czasie oczekiwania na roboczy timer nie dotarło więcej pakietów. Timer zadziałał i wywołał metodę ProcessPackets, w której wykryto utracone pakiety oraz po raz pierwszy wysłano żądania ponownej dostawy.
Wysyłanie żądań ponownej dostawy (stan Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*sprawdzanie na utracone pakiety */)
{
// wysyłamy żądania ponownej dostawy
// ustawiamy timer po raz drugi, na próbę ponownego przesłania
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// jeśli po dwóch próbach uruchomienia WaitForPacketTimer
// nie udało się otrzymać pakietów - uruchamiamy timer zakończenia połączenia
StartCloseWaitTimer(connectionRecord);
}
else if (/*dotarł ostatni pakiet i pomyślna weryfikacja */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// jeśli ack na blok pakietów został utracony
else
{
if (!connectionRecord.TimerSecondTry)
{
// ponownie wysyłamy ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// uruchamiamy timer zakończenia połączenia
StartCloseWaitTimer(connectionRecord);
}
}
Zmienna TimerSecondTry została ustawiona na true. Ta zmienna odpowiada za ponowne uruchomienie roboczego timera.
Z perspektywy nadawcy również uruchamia się roboczy timer i ponownie wysyłany jest ostatni wysłany pakiet.
Uruchomienie timera zamknięcia połączenia (stan SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// ponownie wysyłamy ostatni pakiet
// ...
// uruchamiamy timer CloseWait – w celu oczekiwania na przywrócenie połączenia lub jego zakończenie
StartCloseWaitTimer(connectionRecord);
}
Po tym w połączeniu wychodzącym uruchamiany jest timer zamknięcia połączenia.
ReliableUdpState.StartCloseWaitTimer:
protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
else
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}
Domyślny czas oczekiwania timera zamknięcia połączenia wynosi 30 sekund.
Po krótkim czasie ponownie aktywowany jest timer roboczy po stronie odbiorcy, co skutkuje wysyłaniem zapytań, po czym uruchamiany jest timer zamknięcia połączenia w przychodzącym połączeniu.
Po wywołaniu timerów zamknięcia wszystkie zasoby obu zapisów połączeń są zwalniane. Nadawca informuje o nieudanej dostawie wyższe aplikacje (zobacz API Reliable UDP).
Zwalnianie zasobów zapisu połączenia:
public void Dispose()
{
try
{
System.Threading.Monitor.Enter(this.LockerReceive);
}
finally
{
Interlocked.Increment(ref this.IsDone);
if (WaitForPacketsTimer != null)
{
WaitForPacketsTimer.Dispose();
}
if (CloseWaitTimer != null)
{
CloseWaitTimer.Dispose();
}
byte[] stream;
Tcb.IncomingStreams.TryRemove(Key, out stream);
stream = null;
Tcb.OutcomingStreams.TryRemove(Key, out stream);
stream = null;
System.Threading.Monitor.Exit(this.LockerReceive);
}
}
Głębiej w kodzie. Przywracanie przesyłu danych
Diagram odbudowy transmisji danych przy utracie pakietu:
Jak już wspomniano w przypadku zamknięcia połączenia z powodu timeoutu, po upływie timera roboczego odbiorca sprawdzi utratę pakietów. W przypadku wykrycia utraty pakietów, zostanie sporządzona lista numerów pakietów, które nie dotarły do odbiorcy. Numery te są przenoszone do tablicy LostPackets konkretnego połączenia i rozpoczęta zostanie wysyłka zapytań o ich ponowną dostawę.
Wysyłka zapytań o ponowną dostawę pakietów (stan Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
//...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// są utracone pakiety, wysyłamy zapytania o nie
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// ...
}
}
Nadawca przyjmie zapytanie o ponowną dostawę i wyśle brakujące pakiety. Warto zauważyć, że w tym momencie timer zamknięcia połączenia już działa u nadawcy i po otrzymaniu zapytania zostanie zresetowany.
Ponowna wysyłka utraconych pakietów (stan SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
\/\/ resetowanie timera zamknięcia połączenia
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
\/\/ to jest żądanie ponownej transmisji – wysyłamy wymagany pakiet
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Ponownie wysłany pakiet (packet#3 na diagramie) jest odbierany przez przychodzące połączenie. Sprawdzane jest wypełnienie okna odbiorczego, a standardowa transmisja danych zostaje przywrócona.
Sprawdzanie, czy mieści się w oknie odbiorczym (stan Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
\/\/ zwiększamy licznik pakietów
connectionRecord.PacketCounter++;
\/\/ zapisujemy w tablicy kontroli okna aktualny numer pakietu
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
\/\/ ustawiamy najwyższy przychodzący pakiet
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
\/\/ restartujemy timery
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
\/\/ jeśli otrzymaliśmy wszystkie pakiety okna, resetujemy licznik
\/\/ i wysyłamy pakiet potwierdzenia
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
\/\/ resetujemy licznik.
connectionRecord.PacketCounter = 0;
\/\/ przesuwamy okno transmisji
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
\/\/ zerowanie tablicy kontroli transmisji
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
\/\/ ...
}
API Reliable UDP
Aby interagować z protokołem transmisji danych, dostępna jest otwarta klasa Reliable Udp, która jest opakowaniem dla bloku sterowania transmisją. Oto najważniejsze człony klasy:
public sealed class ReliableUdp : IDisposable
{
// pobiera lokalny punkt końcowy
public IPEndPoint LocalEndpoint
// tworzy instancję ReliableUdp i uruchamia
// nasłuchiwanie przychodzących pakietów na podanym adresie IP
// i porcie. Wartość 0 dla portu oznacza użycie
// dynamicznie przydzielonego portu
public ReliableUdp(IPAddress localAddress, int port = 0)
// subskrypcja na otrzymywanie przychodzących wiadomości
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// wypisanie z otrzymywania wiadomości
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// asynchronicznie wysyła wiadomość
// Uwaga: zgodność z XP i Server 2003 nie jest tracona, ponieważ używany jest .NET Framework 4.0
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// rozpocznij asynchroniczne wysyłanie wiadomości
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// uzyskaj wynik asynchronicznego wysyłania
public bool EndSendMessage(IAsyncResult asyncResult)
// zwolnij zasoby
public void Dispose()
}
Odbieranie wiadomości odbywa się poprzez subskrypcję. Sygnatura delegata dla metody zwrotnej:
public delegate void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );Wiadomość:
public class ReliableUdpMessage
{
// typ wiadomości, prosta enumeracja
public ReliableUdpMessageTypes Type { get; private set; }
// dane wiadomości
public byte[] Body { get; private set; }
// jeśli ustawione na true – mechanizm potwierdzenia dostawy będzie wyłączony
// dla przesyłania konkretnej wiadomości
public bool NoAsk { get; private set; }
}
Aby subskrybować konkretne typy wiadomości i/lub konkretnego nadawcę, używa się dwóch opcjonalnych parametrów: ReliableUdpMessageTypes messageType i IPEndPoint ipEndPoint.
Typy wiadomości:
public enum ReliableUdpMessageTypes : short
{
// Dowolny
Any = 0,
// Żądanie do serwera STUN
StunRequest = 1,
// Odpowiedź z serwera STUN
StunResponse = 2,
// Przesyłanie pliku
FileTransfer =3,
// ...
}
Wysyłanie wiadomości odbywa się asynchronicznie, do tego w protokole zrealizowano asynchroniczny model programowania:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Wynik wysyłania wiadomości będzie true – jeśli wiadomość pomyślnie dotarła do odbiorcy, a false – jeśli połączenie zostało zamknięte z powodu upływu czasu:
public bool EndSendMessage(IAsyncResult asyncResult)
Podsumowanie
Wiele rzeczy nie zostało opisanych w ramach tego artykułu. Mechanizmy uzgadniania strumieni, obsługa wyjątków i błędów, implementacja asynchronicznych metod wysyłania wiadomości. Jednak rdzeń protokołu, opis logiki przetwarzania pakietów, ustanawianie połączenia i obsługa czasów oczekiwania powinny stać się dla Ciebie jasne.
Demonstracyjna wersja protokołu niezawodnej dostawy jest wystarczająco stabilna i elastyczna oraz spełnia wcześniej określone wymagania. Chcę jednak dodać, że opisana implementacja może zostać udoskonalona. Na przykład, aby zwiększyć przepustowość i dynamicznie zmieniać czasy liczników, można dodać takie mechanizmy jak sliding window i RTT, a także przydatna będzie implementacja mechanizmu określania MTU między węzłami połączenia (ale tylko w przypadku wysyłania dużych wiadomości).
Dziękuję za uwagę, czekam na Twoje komentarze i uwagi.
P.S. Dla tych, którzy interesują się szczegółami lub po prostu chcą przetestować protokół, link do projektu na GitHubie:
Przydatne linki i artykuły
- Specyfikacja protokołu TCP: i
- Specyfikacja protokołu UDP: i
- Dyskusja na temat protokołu RUDP:
- Reliable Data Protocol: i
- Prosta implementacja potwierdzenia dostawy na bazie UDP:
- Artykuł opisujący mechanizmy przezwyciężania NAT:
- Implementacja asynchronicznego modelu programowania: i
- Przenoszenie asynchronicznego modelu programowania do asynchronicznego wzorca opartego na zadaniach (APM do TAP):
Aktualizacja: Dziękuję i za pomysł dodania zadania do interfejsu. Kompatybilność biblioteki ze starymi systemami operacyjnymi nie jest naruszona, ponieważ framework 4 wspiera zarówno XP, jak i serwer 2003.
Źródło: habr.com
