Implementacja protokołu Reliable Udp dla .Net

Internet zmienił się dawno temu. Jeden z głównych protokołów Internetu - UDP jest wykorzystywany przez aplikacje nie tylko do dostarczania datagramów i rozgłaszania, ale także do zapewniania połączeń "peer-to-peer" pomiędzy węzłami sieci. Ze względu na swoją prostą konstrukcję protokół ten ma wiele nieplanowanych wcześniej zastosowań, jednak mankamenty protokołu, takie jak brak gwarantowanej dostawy, nigdzie nie zniknęły. W tym artykule opisano implementację protokołu gwarantowanej dostawy przez UDP.
Spis treści:Wejście
Wymagania protokołu
Niezawodny nagłówek UDP
Ogólne zasady protokołu
Limity czasu i liczniki protokołów
Diagram stanu niezawodnej transmisji UDP
Głębiej w kodzie. jednostka sterująca skrzynią biegów
Głębiej w kodzie. stany

Głębiej w kodzie. Tworzenie i nawiązywanie połączeń
Głębiej w kodzie. Zamykanie połączenia po przekroczeniu limitu czasu
Głębiej w kodzie. Przywracanie transferu danych
Niezawodny interfejs API UDP
wniosek
Przydatne linki i artykuły

Wejście

Oryginalna architektura Internetu zakładała jednorodną przestrzeń adresową, w której każdy węzeł miał globalny i unikalny adres IP i mógł komunikować się bezpośrednio z innymi węzłami. Teraz Internet ma w rzeczywistości inną architekturę - jeden obszar globalnych adresów IP i wiele obszarów z adresami prywatnymi ukrytymi za urządzeniami NAT.W tej architekturze tylko urządzenia w globalnej przestrzeni adresowej mogą łatwo komunikować się z kimkolwiek w sieci, ponieważ mają unikalny, globalnie rutowalny adres IP. Węzeł w sieci prywatnej może łączyć się z innymi węzłami w tej samej sieci, a także z innymi dobrze znanymi węzłami w globalnej przestrzeni adresowej. Ta interakcja jest osiągana głównie dzięki mechanizmowi translacji adresów sieciowych. Urządzenia NAT, takie jak routery Wi-Fi, tworzą specjalne wpisy w tablicy translacji dla połączeń wychodzących oraz modyfikują adresy IP i numery portów w pakietach. Pozwala to na połączenia wychodzące z sieci prywatnej do hostów w globalnej przestrzeni adresowej. Ale jednocześnie urządzenia NAT zwykle blokują cały ruch przychodzący, chyba że zostaną ustawione osobne reguły dla połączeń przychodzących.

Taka architektura Internetu jest wystarczająco poprawna do komunikacji klient-serwer, gdzie klienci mogą znajdować się w sieciach prywatnych, a serwery mają adres globalny. Stwarza to jednak trudności w bezpośrednim połączeniu dwóch węzłów pomiędzy różny sieci prywatne. Bezpośrednie połączenie między dwoma węzłami 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 nawiązywania połączenia peer-to-peer między urządzeniami w różnych sieciach prywatnych jest dziurkowanie. Ta technika jest najczęściej stosowana w aplikacjach opartych na protokole UDP.

Ale jeśli Twoja aplikacja wymaga gwarantowanego dostarczenia danych, na przykład przesyłasz pliki między komputerami, to użycie UDP będzie sprawiało wiele trudności, ponieważ UDP nie jest protokołem gwarantowanej dostawy i nie zapewnia dostarczania pakietów w kolejności, w przeciwieństwie do TCP protokół.

W takim przypadku, aby zapewnić gwarantowaną dostawę pakietów, wymagane jest zaimplementowanie protokołu warstwy aplikacji, który zapewnia niezbędną funkcjonalność i działa poprzez UDP.

Od razu chcę zaznaczyć, że istnieje technika TCP hole punching do nawiązywania połączeń TCP między węzłami w różnych sieciach prywatnych, ale ze względu na brak wsparcia dla niej przez wiele urządzeń NAT, zwykle nie jest uważana za główny sposób łączenia takie węzły.

W pozostałej części tego artykułu skupię się tylko na implementacji protokołu gwarantowanej dostawy. Implementacja techniki dziurkowania UDP zostanie opisana w kolejnych artykułach.

Wymagania protokołu

  1. Niezawodne dostarczanie pakietów realizowane poprzez mechanizm pozytywnego sprzężenia zwrotnego (tzw. pozytywne potwierdzenie)
  2. Potrzeba sprawnego przesyłania dużych zbiorów danych, tj. protokół musi unikać niepotrzebnego przekazywania pakietów
  3. Powinna istnieć możliwość anulowania mechanizmu potwierdzenia doręczenia (możliwość funkcjonowania jako „czysty” protokół UDP)
  4. Możliwość zaimplementowania trybu poleceń, z potwierdzeniem każdego komunikatu
  5. Podstawową jednostką przesyłania danych przez protokół musi być komunikat

Wymagania te w dużej mierze pokrywają się z wymaganiami protokołu Reliable Data Protocol opisanymi w RFC 908 и RFC 1151i polegałem na tych standardach podczas opracowywania tego protokołu.

Aby zrozumieć te wymagania, przyjrzyjmy się czasowi przesyłania danych między dwoma węzłami sieci za pomocą protokołów TCP i UDP. Niech w obu przypadkach stracimy jeden pakiet.
Transfer nieinteraktywnych danych przez TCP:Implementacja protokołu Reliable Udp dla .Net

Jak widać na diagramie, w przypadku utraty pakietu, TCP wykryje utracony pakiet i zgłosi to nadawcy, pytając o numer utraconego segmentu.
Transmisja danych przez protokół UDP:Implementacja protokołu Reliable Udp dla .Net

UDP nie podejmuje żadnych kroków w celu wykrycia utraty. Za kontrolę błędów transmisji w protokole UDP odpowiada wyłącznie aplikacja.

Wykrywanie błędów w protokole TCP odbywa się poprzez ustanowienie połączenia z węzłem końcowym, zapamiętanie stanu tego połączenia, wskazanie liczby przesłanych bajtów w każdym nagłówku pakietu oraz powiadomienie o odbiorach za pomocą numeru potwierdzenia.

Dodatkowo w celu poprawy wydajności (czyli wysyłania więcej niż jednego segmentu bez otrzymania potwierdzenia) protokół TCP wykorzystuje tzw. okno transmisji - liczbę bajtów danych, które nadawca segmentu spodziewa się otrzymać.

Aby uzyskać więcej informacji na temat protokołu TCP, zobacz RFC 793, z UDP do RFC 768gdzie w rzeczywistości są one zdefiniowane.

Z powyższego wynika, że ​​w celu stworzenia niezawodnego protokołu dostarczania wiadomości przez UDP (zwanego dalej Niezawodny protokół UDP), wymagane jest zaimplementowanie mechanizmów przesyłania danych podobnych do TCP. Mianowicie:

  • zapisz stan połączenia
  • użyj numeracji segmentów
  • użyj specjalnych pakietów potwierdzeń
  • użyj uproszczonego mechanizmu okienkowania, aby zwiększyć przepustowość protokołu

Dodatkowo potrzebujesz:

  • sygnalizować początek komunikatu, przydzielać zasoby dla połączenia
  • zasygnalizować koniec komunikatu, przekazać odebrany komunikat do aplikacji nadrzędnej i zwolnić zasoby protokołu
  • zezwolić protokołowi specyficznemu dla połączenia na wyłączenie mechanizmu potwierdzania dostarczenia, aby działał jako „czysty” UDP

Niezawodny nagłówek UDP

Przypomnij sobie, że datagram UDP jest zawarty w datagramie IP. Niezawodny pakiet UDP jest odpowiednio „pakowany” w datagram UDP.
Niezawodna enkapsulacja nagłówka UDP:Implementacja protokołu Reliable Udp dla .Net

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

Implementacja protokołu Reliable Udp dla .Net

  • Flagi - flagi kontrolne pakietów
  • MessageType — typ wiadomości używany przez aplikacje nadrzędne do subskrybowania określonych wiadomości
  • TransmissionId - numer transmisji wraz z adresem i portem odbiorcy jednoznacznie identyfikuje połączenie
  • PacketNumber - numer pakietu
  • Opcje - 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 potwierdzania
  • LastPacket - ostatni pakiet wiadomości
  • RequestForPacket - pakiet potwierdzający lub prośba o zagubiony pakiet

Ogólne zasady protokołu

Ponieważ Reliable UDP koncentruje się na gwarantowanej transmisji komunikatów między dwoma węzłami, musi być w stanie nawiązać połączenie z drugą stroną. W celu nawiązania połączenia nadawca wysyła pakiet z flagą FirstPacket, której odpowiedź będzie oznaczać nawiązanie połączenia. Wszystkie pakiety odpowiedzi lub, innymi słowy, pakiety potwierdzenia, zawsze ustawiają wartość pola PacketNumber na wartość o jeden większą niż największa wartość PacketNumber pomyślnie odebranych pakietów. Pole Opcje dla pierwszego wysłanego pakietu określa rozmiar wiadomości.

Podobny mechanizm służy do zakończenia połączenia. Flaga LastPacket jest ustawiana na ostatnim pakiecie wiadomości. W pakiecie odpowiedzi wskazany jest numer ostatniego pakietu + 1, co dla strony odbierającej oznacza pomyślne dostarczenie wiadomości.
Schemat nawiązania i zakończenia połączenia:Implementacja protokołu Reliable Udp dla .Net

Po nawiązaniu połączenia rozpoczyna się przesyłanie danych. Dane przesyłane są w blokach pakietów. Każdy blok, z wyjątkiem ostatniego, zawiera ustaloną liczbę pakietów. Jest równy rozmiarowi okna odbioru/nadawania. Ostatni blok danych może zawierać mniej pakietów. Po wysłaniu każdego bloku strona wysyłająca czeka na potwierdzenie dostarczenia lub prośbę o ponowne dostarczenie utraconych pakietów, pozostawiając otwarte okno odbioru/transmisji w celu odebrania odpowiedzi. Po otrzymaniu potwierdzenia dostarczenia bloku następuje przesunięcie okna odbioru/nadawania i wysłanie kolejnego bloku danych.

Strona odbierająca odbiera pakiety. Każdy pakiet jest sprawdzany pod kątem tego, czy mieści się w oknie transmisji. Pakiety i duplikaty, które nie wpadają do okna, są odfiltrowywane. Ponieważ Jeżeli rozmiar okna jest stały i taki sam dla odbiorcy i nadawcy, to w przypadku doręczenia bloku pakietów bez strat, okno jest przesuwane w celu odebrania pakietów kolejnego bloku danych i następuje potwierdzenie doręczenia wysłano. Jeśli okno nie zapełni się w czasie ustawionym przez licznik czasu pracy, wówczas uruchomione zostanie sprawdzenie, które paczki nie zostały dostarczone i zostaną wysłane prośby o ponowną dostawę.
Schemat retransmisji:Implementacja protokołu Reliable Udp dla .Net

Limity czasu i liczniki protokołów

Istnieje kilka powodów, dla których nie można nawiązać połączenia. Na przykład, jeśli odbiorca jest offline. W takim przypadku podczas próby nawiązania połączenia połączenie zostanie zamknięte po przekroczeniu limitu czasu. Niezawodna implementacja protokołu UDP używa dwóch liczników czasu do ustawiania limitów czasu. Pierwszy, zegar roboczy, służy do oczekiwania na odpowiedź od zdalnego hosta. Jeśli zadziała po stronie nadawcy, ostatni wysłany pakiet zostanie wysłany ponownie. Jeśli licznik czasu u odbiorcy wygaśnie, przeprowadzane jest sprawdzanie utraconych pakietów i wysyłane są żądania ponownego dostarczenia.

Drugi timer jest potrzebny do zamknięcia połączenia w przypadku braku komunikacji między węzłami. Po stronie nadawcy rozpoczyna się natychmiast po wygaśnięciu czasu pracy i oczekuje na odpowiedź ze zdalnego węzła. Brak odpowiedzi w określonym czasie powoduje przerwanie połączenia i zwolnienie zasobów. Po stronie odbierającej licznik czasu zamknięcia połączenia jest uruchamiany po dwukrotnym upłynięciu czasu pracy. Jest to konieczne, aby zabezpieczyć się przed utratą pakietu potwierdzającego. Po wygaśnięciu licznika czasu połączenie jest również przerywane, a zasoby są zwalniane.

Diagram stanu niezawodnej transmisji UDP

Zasady protokołu są zaimplementowane w skończonej maszynie stanów, której każdy stan odpowiada za określoną logikę przetwarzania pakietów.
Niezawodny diagram stanu UDP:

Implementacja protokołu Reliable Udp dla .Net

Zamknięte - nie jest tak naprawdę stanem, jest punktem początkowym i końcowym automatu. Dla stanu Zamknięte odbierany jest blok sterowania transmisją, który realizując asynchroniczny serwer UDP przekazuje pakiety do odpowiednich połączeń i rozpoczyna przetwarzanie stanu.

Wysyłanie pierwszego pakietu – początkowy stan połączenia wychodzącego w momencie wysłania wiadomości.

W tym stanie wysyłany jest pierwszy pakiet dla normalnych wiadomości. W przypadku wiadomości bez potwierdzenia wysłania jest to jedyny stan, w którym wysyłana jest cała wiadomość.

Cykl wysyłania – stan podstawowy do transmisji pakietów komunikatów.

Przejście do niego ze stanu Wysyłanie pierwszego pakietu przeprowadzane po wysłaniu pierwszego pakietu wiadomości. W tym stanie napływają wszystkie potwierdzenia i prośby o retransmisje. Wyjście z niego jest możliwe w dwóch przypadkach - w przypadku pomyślnego dostarczenia wiadomości lub przekroczenia limitu czasu.

Odebrano pierwszy pakiet – stan początkowy odbiorcy wiadomości.

Sprawdza poprawność rozpoczęcia transmisji, tworzy potrzebne struktury i wysyła potwierdzenie odbioru pierwszego pakietu.

W przypadku wiadomości, która składa się z pojedynczego pakietu i została wysłana bez użycia dowodu doręczenia, jest to jedyny stan. Po przetworzeniu takiej wiadomości połączenie jest zamykane.

Złożenie – podstawowy stan odbierania pakietów komunikatów.

Zapisuje pakiety do pamięci tymczasowej, sprawdza utratę pakietów, wysyła potwierdzenia dostarczenia bloku pakietów i całej wiadomości oraz wysyła żądania ponownego dostarczenia utraconych pakietów. W przypadku pomyślnego odebrania całej wiadomości połączenie przechodzi w stan Zakończony, w przeciwnym razie limit czasu zostanie zakończony.

Zakończony – zamknięcie połączenia w przypadku pomyślnego odebrania całej wiadomości.

Stan ten jest niezbędny do złożenia wiadomości oraz w przypadku zagubienia potwierdzenia doręczenia wiadomości w drodze do nadawcy. Ten stan jest opuszczany po przekroczeniu limitu czasu, ale połączenie jest uważane za pomyślnie zamknięte.

Głębiej w kodzie. jednostka sterująca skrzynią biegów

Jednym z kluczowych elementów Reliable UDP jest blok sterowania transmisją. Zadaniem tego bloku jest przechowywanie bieżących połączeń i elementów pomocniczych, dystrybucja pakietów przychodzących do odpowiednich połączeń, udostępnianie interfejsu do wysyłania pakietów do połączenia oraz implementacja API protokołu. Blok sterowania transmisją odbiera pakiety z warstwy UDP i przekazuje je do maszyny stanowej w celu przetworzenia. Aby odbierać pakiety, implementuje asynchroniczny serwer UDP.
Niektórzy członkowie klasy ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Implementacja asynchronicznego serwera UDP:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  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);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Dla każdego transferu wiadomości tworzona jest struktura zawierająca informacje o połączeniu. Taka konstrukcja nazywa się rekord połączenia.
Niektórzy członkowie klasy ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

Głębiej w kodzie. stany

Stany implementują maszynę stanów protokołu Reliable UDP, w której odbywa się główne przetwarzanie pakietów. Klasa abstrakcyjna ReliableUdpState zapewnia interfejs dla stanu:

Implementacja protokołu Reliable Udp dla .Net

Cała logika protokołu jest realizowana przez zaprezentowane powyżej klasy wraz z klasą pomocniczą udostępniającą metody statyczne, takie jak np. konstruowanie nagłówka ReliableUdp z rekordu połączenia.

Następnie szczegółowo rozważymy implementację metod interfejsu, które określają podstawowe algorytmy protokołu.

Metoda DisposeByTimeout

Metoda DisposeByTimeout odpowiada za zwolnienie zasobów połączenia po przekroczeniu limitu czasu oraz za sygnalizację pomyślnego/nieudanego dostarczenia komunikatu.
NiezawodnyUdpState.DisposeByTimeout:

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;      
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

Jest zastępowany tylko w stanie Zakończony.
Ukończono.DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

Metoda ProcessPackets

Metoda ProcessPackets odpowiada za dodatkowe przetwarzanie paczki lub paczek. Wywoływane bezpośrednio lub za pośrednictwem zegara oczekiwania na pakiet.

Zdolny Złożenie metoda jest nadpisywana i odpowiada za sprawdzanie utraconych pakietów i przejście do stanu Zakończony, w przypadku odebrania ostatniego pakietu i pomyślnego sprawdzenia
Montaż.Pakiety procesowe:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Zdolny Cykl wysyłania ta metoda jest wywoływana tylko na czasomierzu i jest odpowiedzialna za ponowne wysłanie ostatniej wiadomości, a także włączenie licznika czasu zamknięcia połączenia.
WysyłanieCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Zdolny Zakończony metoda zatrzymuje uruchomiony licznik czasu i wysyła wiadomość do subskrybentów.
Ukończono.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Metoda odbierania pakietów

Zdolny Odebrano pierwszy pakiet głównym zadaniem metody jest ustalenie, czy pierwszy pakiet wiadomości rzeczywiście dotarł do interfejsu, a także zebranie wiadomości składającej się z pojedynczego pakietu.
Odebrany pierwszy pakiet. Odbiór pakietu:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  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))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  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);
  }
}

Zdolny Cykl wysyłania ta metoda jest zastępowana w celu akceptowania potwierdzeń dostarczenia i żądań retransmisji.
WysyłanieCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Zdolny Złożenie w metodzie ReceivePacket odbywa się główna praca polegająca na składaniu wiadomości z pakietów przychodzących.
Montaż. Odbiór pakietu:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

Zdolny Zakończony jedynym zadaniem metody jest wysłanie ponownego potwierdzenia pomyślnego dostarczenia wiadomości.
Ukończono. Odbierz pakiet:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Metoda wysyłania pakietów

Zdolny Wysyłanie pierwszego pakietu w tej metodzie wysyłany jest pierwszy pakiet danych lub, jeśli wiadomość nie wymaga potwierdzenia doręczenia, cała wiadomość.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Zdolny Cykl wysyłania w tej metodzie wysyłany jest blok pakietów.
Cykl wysyłania.Wyślij pakiet:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  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++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Głębiej w kodzie. Tworzenie i nawiązywanie połączeń

Teraz, gdy poznaliśmy już podstawowe stany i metody używane do obsługi stanów, przyjrzyjmy się bliżej kilku przykładom działania protokołu.
Schemat transmisji danych w normalnych warunkach:Implementacja protokołu Reliable Udp dla .Net

Rozważ szczegółowo stworzenie rekord połączenia aby połączyć się i wysłać pierwszy pakiet. Transfer jest zawsze inicjowany przez aplikację, która wywołuje API wysyłania wiadomości. Następnie wywoływana jest metoda StartTransmission bloku sterującego transmisją, która rozpoczyna transmisję 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( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  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;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Po wysłaniu pierwszego pakietu nadawca przechodzi w stan Cykl wysyłania – czekaj na potwierdzenie doręczenia paczki.
Strona odbierająca za pomocą metody EndReceive odbiera wysłany pakiet, tworzy nowy rekord połączenia i przekazuje ten pakiet ze wstępnie przeanalizowanym nagłówkiem do metody ReceivePacket stanu w celu przetworzenia Odebrano pierwszy pakiet
Tworzenie połączenia po stronie odbierającej:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  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))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Głębiej w kodzie. Zamykanie połączenia po przekroczeniu limitu czasu

Obsługa limitu czasu jest ważną częścią niezawodnego protokołu UDP. Rozważmy przykład, w którym węzeł pośredni uległ awarii i dostarczanie danych w obu kierunkach stało się niemożliwe.
Diagram zamykania połączenia po przekroczeniu limitu czasu:Implementacja protokołu Reliable Udp dla .Net

Jak widać na diagramie, odliczanie czasu pracy nadawcy rozpoczyna się natychmiast po wysłaniu bloku pakietów. Dzieje się tak w metodzie SendPacket stanu Cykl wysyłania.
Włączenie licznika czasu pracy (stan SendCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

Okresy timera są ustawiane podczas tworzenia połączenia. Domyślny parametr ShortTimerPeriod to 5 sekund. W przykładzie jest ustawiony na 1,5 sekundy.

Dla połączenia przychodzącego licznik czasu uruchamia się po odebraniu ostatniego pakietu danych przychodzących, dzieje się to w metodzie ReceivePacket stanu Złożenie
Włączenie timera pracy (stan montażu):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Żadne więcej pakietów nie dotarło do połączenia przychodzącego podczas oczekiwania na działający licznik czasu. Licznik zadziałał i wywołał metodę ProcessPackets, w której znaleziono utracone pakiety i po raz pierwszy wysłano żądania ponownego dostarczenia.
Wysyłanie próśb o ponowną dostawę (stan składania):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Zmienna TimerSecondTry jest ustawiona na prawdziwy. Ta zmienna odpowiada za ponowne uruchomienie działającego timera.

Po stronie nadawcy uruchamiany jest również licznik czasu pracy i ponownie wysyłany jest ostatnio wysłany pakiet.
Włączanie licznika czasu zamknięcia połączenia (stan SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Następnie w połączeniu wychodzącym rozpoczyna się odliczanie czasu zamknięcia połączenia.
NiezawodnyUdpState.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);
}

Limit czasu zamknięcia połączenia wynosi domyślnie 30 sekund.

Po krótkim czasie licznik czasu pracy po stronie odbiorcy ponownie się odpala, ponownie wysyłane są żądania, po czym uruchamia się licznik czasu zamknięcia połączenia dla połączenia przychodzącego

Gdy liczniki czasu zamknięcia zostaną uruchomione, wszystkie zasoby obu rekordów połączeń zostaną zwolnione. Nadawca zgłasza niepowodzenie dostarczenia do nadrzędnej aplikacji (zobacz Niezawodny interfejs API UDP).
Zwalnianie zasobów rekordu 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 transferu danych

Schemat odzyskiwania transmisji danych w przypadku utraty pakietu:Implementacja protokołu Reliable Udp dla .Net

Jak już omówiono przy zamykaniu połączenia po przekroczeniu limitu czasu, gdy upłynie czas pracy, odbiornik sprawdzi, czy nie ma utraconych pakietów. W przypadku utraty pakietów zostanie sporządzona lista ilości pakietów, które nie dotarły do ​​odbiorcy. Numery te są wprowadzane do tablicy LostPackets określonego połączenia i wysyłane są żądania ponownego dostarczenia.
Wysyłanie próśb o ponowne dostarczenie paczek (stan montażu):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Nadawca zaakceptuje żądanie ponownego dostarczenia i wyśle ​​brakujące pakiety. Warto zauważyć, że w tym momencie nadawca uruchomił już licznik czasu zamknięcia połączenia i po otrzymaniu żądania jest on resetowany.
Ponowne wysyłanie utraconych pakietów (stan SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Ponownie wysłany pakiet (pakiet nr 3 na schemacie) jest odbierany przez połączenie przychodzące. Sprawdzane jest, czy okno odbioru jest pełne i normalna transmisja danych została przywrócona.
Sprawdzanie trafień w oknie odbioru (stan składania):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

Niezawodny interfejs API UDP

Do interakcji z protokołem przesyłania danych służy otwarta klasa Reliable Udp, która jest opakowaniem bloku sterującego transferem. Oto najważniejsi członkowie klasy:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

Wiadomości są odbierane w ramach subskrypcji. Podpis delegata dla metody wywołania zwrotnego:

public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );

Wiadomość:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

Aby zasubskrybować określony typ wiadomości i/lub określonego nadawcę, używane są dwa opcjonalne parametry: ReliableUdpMessageTypes messageType i IPEndPoint ipEndPoint.

Typy wiadomości:

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

Wiadomość jest wysyłana asynchronicznie; w tym celu protokół implementuje model programowania asynchronicznego:

public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)

Wynikiem wysłania wiadomości będzie prawda - jeśli wiadomość dotarła pomyślnie do odbiorcy i fałsz - jeśli połączenie zostało zamknięte przez timeout:

public bool EndSendMessage(IAsyncResult asyncResult)

wniosek

Wiele nie zostało opisanych w tym artykule. Mechanizmy dopasowywania wątków, obsługa wyjątków i błędów, implementacja metod asynchronicznego wysyłania komunikatów. Ale rdzeń protokołu, opis logiki przetwarzania pakietów, nawiązywania połączenia i obsługi limitów czasu, powinien być dla ciebie jasny.

Zademonstrowana wersja niezawodnego protokołu dostarczania jest wystarczająco solidna i elastyczna, aby spełnić wcześniej zdefiniowane wymagania. Ale chcę dodać, że opisaną implementację można poprawić. Dla przykładu, aby zwiększyć przepustowość i dynamicznie zmieniać okresy timera, można dodać do protokołu mechanizmy takie jak przesuwane okno i RTT, przyda się też zaimplementowanie 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 komentarze i komentarze.

PS Dla zainteresowanych szczegółami lub po prostu chcących przetestować protokół link do projektu na GitHube:
Niezawodny projekt UDP

Przydatne linki i artykuły

  1. Specyfikacja protokołu TCP: w języku angielskim и на русском
  2. Specyfikacja protokołu UDP: w języku angielskim и на русском
  3. Omówienie protokołu RUDP: wersja robocza-ietf-sigtran-niezawodna-udp-00
  4. Niezawodny protokół danych: RFC 908 и RFC 1151
  5. Prosta implementacja potwierdzenia doręczenia przez UDP: Przejmij całkowitą kontrolę nad swoją siecią dzięki .NET i UDP
  6. Artykuł opisujący mechanizmy translacji NAT: Komunikacja peer-to-peer przez translatory adresów sieciowych
  7. Implementacja modelu programowania asynchronicznego: Implementacja modelu programowania asynchronicznego CLR и Jak zaimplementować wzorzec projektowy IAsyncResult
  8. Portowanie modelu programowania asynchronicznego do wzorca asynchronicznego opartego na zadaniach (APM w TAP):
    TPL i tradycyjne programowanie asynchroniczne .NET
    Współpraca z innymi wzorcami i typami asynchronicznymi

Aktualizacja: Dziękuję burmistrzowp и sidristij za pomysł dodania zadania do interfejsu. Kompatybilność biblioteki ze starymi systemami operacyjnymi nie jest naruszona, ponieważ Czwarty framework obsługuje zarówno serwer XP, jak i 4.

Źródło: www.habr.com

Dodaj komentarz