Implementacja protokołu Reliable Udp dla .Net

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
Wymagania dotyczące protokołu
Nagłówek Reliable UDP
Ogólne zasady działania protokołu
Czasy oczekiwania i timery protokołu
Diagram stanów przesyłu Reliable UDP
Głębiej w kodzie. Blok zarządzania przesyłem
Głębiej w kodzie. Stany

Głębiej w kodzie. Tworzenie i ustanawianie połączeń
Głębiej w kodzie. Zamykanie połączenia z powodu wygaśnięcia czasu
Głębiej w kodzie. Przywracanie przesyłu danych
API Reliable UDP
Podsumowanie
Przydatne linki i artykuły

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

  1. Niezawodna dostawa pakietów jest realizowana za pomocą mechanizmu pozytywnego potwierdzenia (tzw. positive acknowledgment).
  2. Konieczność efektywnego przesyłania dużych danych, tj. protokół powinien unikać zbędnej retransmisji pakietów.
  3. Powinna istnieć możliwość dezaktywacji mechanizmu potwierdzania dostawy (możliwość działania jako 'czysty' protokół UDP).
  4. Możliwość wdrożenia trybu poleceń z potwierdzeniem każdego komunikatu.
  5. 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 rfc 908 i rfc 1151, 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:Implementacja protokołu Reliable Udp dla .Net

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:Implementacja protokołu Reliable Udp dla .Net

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 rfc 793, z UDP w rfc 768, 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:Implementacja protokołu Reliable Udp dla .Net

Struktura nagłówka Reliable UDP jest dość prosta:

Implementacja protokołu Reliable Udp dla .Net

  • 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:Implementacja protokołu Reliable Udp dla .Net

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:Implementacja protokołu Reliable Udp dla .Net

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:

Implementacja protokołu Reliable Udp dla .Net

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:

Implementacja protokołu Reliable Udp dla .Net

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:Implementacja protokołu Reliable Udp dla .Net

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:Implementacja protokołu Reliable Udp dla .Net

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:Implementacja protokołu Reliable Udp dla .Net

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:
Projekt Reliable UDP

Przydatne linki i artykuły

  1. Specyfikacja protokołu TCP: w języku angielskim i po polsku
  2. Specyfikacja protokołu UDP: w języku angielskim i po polsku
  3. Dyskusja na temat protokołu RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 i rfc 1151
  5. Prosta implementacja potwierdzenia dostawy na bazie UDP: Przejmij pełną kontrolę nad swoją siecią dzięki .NET i UDP
  6. Artykuł opisujący mechanizmy przezwyciężania NAT: Komunikacja Peer-to-Peer przez Translatory Adresów Sieciowych
  7. Implementacja asynchronicznego modelu programowania: Implementacja modelu programowania asynchronicznego CLR i Jak zaimplementować wzorzec projektowy IAsyncResult
  8. Przenoszenie asynchronicznego modelu programowania do asynchronicznego wzorca opartego na zadaniach (APM do TAP):
    TPL i tradycyjne programowanie asynchroniczne w .NET
    Interop z innymi asynchronicznymi wzorcami i typami

Aktualizacja: Dziękuję mayorovp i sidristij 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

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