Реалізація Reliable Udp протоколу для .Net

Інтернет давно змінився. Один з основних протоколів Інтернету – UDP використовується додаткам не лише для доставки дейтаграм та широкомовних розсилок, але й для забезпечення "peer-to-peer" з'єднань між вузлами мережі. Зважаючи на свій простий пристрій, у даного протоколу з'явилося безліч не запланованих раніше способів застосування, правда, недоліки протоколу, такі як відсутність гарантованої доставки, нікуди при цьому не зникли. Ця стаття описує реалізацію протоколу гарантованої доставки поверх UDP.
Зміст:Вступ
Вимоги до протоколу
Заголовок Reliable UDP
Загальні засади роботи протоколу
Тайм-аути та таймери протоколу
Діаграма станів передачі Reliable UDP
Глибше в код. Блок керування передачею
Глибше в код. Стану

Глибше в код. Створення та встановлення з'єднань
Глибше в код. Закриття з'єднання за тайм-аутом
Глибше в код. Відновлення передачі даних
API Reliable UDP
Висновок
Корисні посилання та статті

Вступ

Початкова архітектура Інтернету мала на увазі однорідний адресний простір, в якому кожен вузол мав глобальну та унікальну IP адресу, і міг безпосередньо спілкуватися з іншими вузлами. Зараз Інтернет, за фактом, має іншу архітектуру – одну область глобальних IP-адрес та безліч областей з приватними адресами, прихованим за NAT пристроями.У такій архітектурі тільки пристрої, що знаходяться в глобальному адресному просторі, можуть з легкістю взаємодіяти з ким-небудь у мережі, оскільки мають унікальну, глобальну маршрутизовану IP адресу. Вузол, що знаходиться в приватній мережі, може з'єднуватися з іншими вузлами цієї мережі, а також з'єднуватися з іншими, добре відомими вузлами в глобальному адресному просторі. Така взаємодія досягається багато в чому завдяки механізму перетворення мережевих адрес. NAT пристрої, наприклад Wi-Fi маршрутизатори, створюють спеціальні записи в таблицях трансляцій для вихідних з'єднань і модифікують IP адреси та номери портів у пакетах. Це дозволяє встановити з приватної мережі вихідне з'єднання з вузлами у глобальному адресному просторі. Але в той же час NAT пристрої зазвичай блокують весь вхідний трафік, якщо не встановлені окремі правила для вхідних з'єднань.

Така архітектура Інтернету досить правильна для клієнт-серверної взаємодії, коли клієнти можуть перебувати у приватних мережах, а сервери мають глобальну адресу. Але вона створює труднощі для прямого з'єднання двох вузлів між різними приватними мережами. Пряме з'єднання двох вузлів є важливим для «peer-to-peer» програм, таких як передача голосу (Skype), отримання віддаленого доступу до комп'ютера (TeamViewer), або онлайн ігри.

Один з найбільш ефективних методів для встановлення peer-to-peer з'єднання між пристроями, що знаходяться в різних приватних мережах, називається hole punching. Ця техніка найчастіше використовується з програмами на основі UDP протоколу.

Але якщо ваша програма потребує гарантованої доставки даних, наприклад, передає файли між комп'ютерами, то при використанні UDP з'явиться безліч труднощів, пов'язаних з тим, що UDP не є протоколом гарантованої доставки і не забезпечує доставку пакетів по порядку, на відміну від протоколу TCP.

У такому випадку, для забезпечення гарантованої доставки пакетів, потрібно реалізувати протокол прикладного рівня, що забезпечує необхідну функціональність та працює поверх UDP.

Відразу хочу помітити, що існує техніка TCP hole punching для встановлення TCP з'єднань між вузлами в різних приватних мережах, але через відсутність підтримки її багатьма NAT пристроями вона зазвичай не розглядається як основний спосіб з'єднання таких вузлів.

Далі в цій статті я розглядатиму лише реалізацію протоколу гарантованої доставки. Реалізація техніки UDP hole punching буде описана у наступних статтях.

Вимоги до протоколу

  1. Надійна доставка пакетів, реалізована через механізм позитивного зворотного зв'язку (так званий positive acknowledgment)
  2. Необхідність ефективної передачі великих даних, тобто. протокол повинен уникати зайвих ретрансляцій пакетів
  3. Повинна бути можливість скасування механізму підтвердження доставки (можливість функціонувати як «чистий» протокол UDP)
  4. Можливість реалізації командного режиму, з підтвердженням кожного повідомлення
  5. Базовою одиницею передачі даних за протоколом має бути повідомлення

Ці вимоги багато в чому збігаються з вимогами до Reliable Data Protocol, описаними в rfc 908 и rfc 1151, і я ґрунтувався на цих стандартах при розробці даного протоколу.

Для розуміння даних вимог, давайте розглянемо часові діаграми передачі між двома вузлами мережі за протоколами TCP і UDP. Нехай в обох випадках у нас буде втрачено один пакет.
Передача неінтерактивних даних через TCP:Реалізація Reliable Udp протоколу для .Net

Як видно з діаграми, у разі втрати пакетів TCP виявить втрачений пакет і повідомить про це відправнику, запитавши номер втраченого сегмента.
Передача даних за протоколом UDP:Реалізація Reliable Udp протоколу для .Net

UDP не робить жодних кроків щодо виявлення втрат. Контроль помилок передачі в протоколі UDP повністю покладається на додаток.

Виявлення помилок у протоколі TCP досягається завдяки установці з'єднання з кінцевим вузлом, збереженню стану цього з'єднання, вказівці номера відправлених байт у кожному заголовку пакета, і повідомлення про отримання за допомогою номера підтвердження «acknowledge number».

Додатково, для підвищення продуктивності (тобто відправки більше одного сегмента без отримання підтвердження) TCP протокол використовує так зване вікно передачі - число байт даних, які відправник сегмента очікує прийняти.

Більш детально з протоколом TCP можна ознайомитися в rfc 793, з UDP в rfc 768де вони, власне кажучи, і визначені.

З вищеописаного, зрозуміло, що для створення надійного протоколу доставки повідомлень поверх UDP (надалі будемо називати Надійний UDP), потрібно реалізувати схожі з TCP механізми передачі. А саме:

  • зберігати стан з'єднання
  • використовувати нумерацію сегментів
  • використовувати спеціальні пакети підтвердження
  • використовувати спрощений механізм вікна, для збільшення пропускної спроможності протоколу

Додатково потрібно:

  • сигналізувати про початок повідомлення, виділення ресурсів під з'єднання
  • сигналізувати про закінчення повідомлення, передачі отриманого повідомлення вищому додатку і вивільнення ресурсів протоколу
  • дозволити протоколу для конкретних з'єднань відключати механізм підтвердження доставки, щоб функціонувати як «чистий» UDP

Заголовок Reliable UDP

Згадаємо, що UDP дейтаграма інкапсулюється в IP дейтаграму. Пакет Reliable UDP відповідно "загортається" в UDP дейтаграму.
Інкапсуляція заголовка Reliable UDP:Реалізація Reliable Udp протоколу для .Net

Структура заголовка Reliable UDP досить проста:

Реалізація Reliable Udp протоколу для .Net

  • Flags – керуючі прапори пакету
  • MessageType – тип повідомлення, що використовується вищими програмами, для передплати певних повідомлень
  • TransmissionId — номер передачі, разом із адресою та портом одержувача унікально визначає з'єднання
  • PacketNumber – номер пакету
  • Options – додаткові опції протоколу. У разі першого пакета використовується для вказівки розміру повідомлення

Прапори бувають такі:

  • FirstPacket – перший пакет повідомлення
  • NoAsk — повідомлення не вимагає увімкнення механізму підтвердження
  • LastPacket – останній пакет повідомлення
  • RequestForPacket — пакет підтвердження або запит на втрачений пакет

Загальні засади роботи протоколу

Оскільки Reliable UDP орієнтований на гарантовану передачу повідомлення між двома вузлами, він має вміти встановлювати з'єднання з іншою стороною. Для встановлення з'єднання сторона-відправник посилає пакет із прапором FirstPacket, відповідь на який означатиме встановлення з'єднання. Всі пакети у відповідь, або, по-іншому, пакети підтвердження, завжди виставляють значення поля PacketNumber на одиницю більше, ніж найбільше значення PacketNumber у пакетів, що успішно прийшли. У полі Options для першого надісланого пакета записується розмір повідомлення.

Для завершення з'єднання використовується подібний механізм. В останньому пакеті повідомлення встановлюється прапорець LastPacket. У пакеті у відповідь вказується номер останнього пакета + 1, що для приймальної сторони означає успішну доставку повідомлення.
Діаграма встановлення та завершення з'єднання:Реалізація Reliable Udp протоколу для .Net

Коли з'єднання встановлено, починається надсилання даних. Дані передаються блоками пакетів. Кожен блок, крім останнього, містить фіксовану кількість пакетів. Воно дорівнює розміру вікна прийому/передачі. Останній блок даних може мати менше пакетів. Після відправлення кожного блоку, сторона-відправник очікує на підтвердження про доставку, або запиту на повторну доставку втрачених пакетів, залишаючи відкритим вікно прийому/передачі для отримання відповідей. Після отримання підтвердження про доставку блоку, вікно прийому/передачі зсувається і відправляється наступний блок даних.

Сторона-одержувач приймає пакети. Кожен пакет перевіряється на влучення у вікно передачі. Пакети, що не потрапляють у вікно, і дублікати відсіваються. Т.к. розмір вікна сторого фіксований і однаковий у одержувача і відправника, то у разі доставки блоку пакетів без втрат, вікно зсувається для прийому пакетів наступного блоку даних і відправляється підтвердження про доставку. Якщо вікно не заповниться за встановлений робочим таймером період, то буде запущено перевірку на те, які пакети не були доставлені та надіслані запити на повторну доставку.
Діаграма повторної передачі:Реалізація Reliable Udp протоколу для .Net

Тайм-аути та таймери протоколу

Існує кілька причин, через які не може бути встановлено з'єднання. Наприклад, якщо сторона, що приймає, поза мережею. У такому випадку, при спробі встановити з'єднання, з'єднання буде закрито за тайм-аутом. У реалізації Reliable UDP використовуються два таймери для встановлення тайм-аутів. Перший, робочий таймер, служить для очікування відповіді віддаленого хоста. Якщо він спрацьовує на стороні відправника, то виконується повторне відправлення останнього відправленого пакета. Якщо ж таймер спрацьовує в одержувача, то перевіряється на втрачені пакети і надсилаються запити на повторну доставку.

Другий таймер – необхідний закриття з'єднання у разі відсутності зв'язку між вузлами. Для сторони-відправника він запускається відразу після спрацьовування робочого таймера і чекає відповіді від віддаленого вузла. У разі відсутності відповіді за встановлений період – з'єднання завершується та ресурси звільняються. Для сторони-одержувача таймер закриття з'єднання запускається після подвійного спрацьовування робочого таймера. Це необхідно для страховки від втрати пакета підтвердження. При спрацьовуванні таймера також завершується з'єднання і вивільняються ресурси.

Діаграма станів передачі Reliable UDP

Принципи роботи протоколу реалізовані кінцевому автоматі, кожен стан якого відповідає певну логіку обробки пакетів.
Діаграма станів Reliable UDP:

Реалізація Reliable Udp протоколу для .Net

Закрито - Насправді не є станом, це стартова і кінцева точка для автомата. За стан Закрито приймається блок управління передачею, який, реалізуючи асинхронний UDP сервер, перенаправляє пакети відповідні з'єднання і запускає обробку станів.

FirstPacketSending – початковий стан, у якому знаходиться вихідне з'єднання під час надсилання повідомлення.

У цьому стані надсилається перший пакет для звичайних повідомлень. Для повідомлень без підтвердження надсилання, це єдиний стан – у ньому відбувається надсилання всього повідомлення.

SendingCycle – основний стан передачі пакетів повідомлення.

Перехід у нього зі стану FirstPacketSending здійснюється після надсилання першого пакета повідомлення. Саме в цей стан приходять усі підтвердження та запити на повторні передачі. Вихід із нього можливий у двох випадках – у разі успішної доставки повідомлення або за тайм-аутом.

FirstPacketReceived - Початковий стан для одержувача повідомлення.

У ньому перевіряється коректність початку передачі, створюються необхідні структури, і надсилається підтвердження прийому першого пакета.

Для повідомлення, що складається з єдиного пакета та надісланого без використання підтвердження доставки, це єдиний стан. Після обробки повідомлення з'єднання закривається.

Сборка – основний стан прийому пакетів повідомлення.

У ньому проводиться запис пакетів у тимчасове сховище, перевірка відсутність втрат пакетів, відправка підтверджень про доставку блоку пакетів і повідомлення повністю, і надсилання запитів на повторну доставку втрачених пакетів. У разі успішного отримання всього повідомлення – з'єднання переходить у стан Зроблено, інакше виконується вихід за тайм-аутом.

Зроблено – закриття з'єднання у разі успішного отримання повідомлення.

Цей стан необхідний для складання повідомлення та випадку, коли підтвердження про доставку повідомлення було втрачено на шляху до відправника. Вихід із цього стану проводиться за тайм-аутом, але з'єднання вважається успішно закритим.

Глибше в код. Блок керування передачею

Один із ключових елементів Reliable UDP – блок керування передачею. Завдання даного блоку - зберігання поточних з'єднань і допоміжних елементів, розподіл пакетів, що прийшли по відповідним з'єднанням, надання інтерфейсу для відправки пакетів з'єднанню і реалізація API протоколу. Блок управління передачею приймає пакети від UDP рівня та перенаправляє їх на обробку кінцевого автомата. Для прийому пакетів у ньому реалізовано асинхронний UDP сервер.
Деякі члени класу 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;    	
  //...
}

Реалізація асинхронного 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);
}

Для кожної передачі повідомлення створюється структура, що містить інформацію про з'єднанні. Така структура називається connection record.
Деякі члени класу 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;
  //...
}

Глибше в код. Стану

Стану реалізують кінцевий автомат протоколу Reliable UDP, де відбувається основна обробка пакетів. Абстрактний клас ReliableUdpState надає інтерфейс для стану:

Реалізація Reliable Udp протоколу для .Net

Всю логіку роботи протоколу реалізують представлені вище класи, разом із допоміжним класом, що надає статичні методи, такі як, наприклад, побудови заголовка ReliableUdp із connection record.

Далі будуть розглянуті подробиці реалізації методів інтерфейсу, що визначають основні алгоритми роботи протоколу.

Метод DisposeByTimeout

Метод DisposeByTimeout відповідає за вивільнення ресурсів з'єднання після закінчення тайм-ауту та для сигналізації про успішну/неуспішну доставку повідомлення.
ReliableUdpState.DisposeByTimeout:

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

Він перевизначений лише у стані Зроблено.
Completed.DisposeByTimeout:

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

Метод ProcessPackets

Метод ProcessPackets відповідає за додаткову обробку пакета чи пакетів. Викликається безпосередньо або через таймер очікування пакетів.

В стані Сборка метод перевизначено та відповідає за перевірку втрачених пакетів та перехід у стан Зроблено, у разі отримання останнього пакету та проходження успішної перевірки
Assembling.ProcessPackets:

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);
  }
}

В стані SendingCycle цей метод викликається лише за таймером, і відповідає за повторне відправлення останнього повідомлення, а також за включення таймера закриття з'єднання.
SendingCycle.ProcessPackets:

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

В стані Зроблено метод зупиняє робочий таймер та передає повідомлення передплатникам.
Completed.ProcessPackets:

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

Метод ReceivePacket

В стані FirstPacketReceived основне завдання методу - визначити чи дійсно перший пакет повідомлення прийшов на інтерфейс, а також зібрати повідомлення, що складається з єдиного пакета.
FirstPacketReceived.ReceivePacket:

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);
  }
}

В стані SendingCycle цей метод перевизначено для прийому підтверджень про доставку та запитів повторної передачі.
SendingCycle.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));
}

В стані Сборка у методі ReceivePacket відбувається основна робота зі збирання повідомлення з пакетів, що надходять.
Assembling.ReceivePacket:

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);
  }
}

В стані Зроблено єдине завдання методу - надіслати повторне підтвердження про успішну доставку повідомлення.
Completed.ReceivePacket:

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

Метод SendPacket

В стані FirstPacketSending цей метод здійснює відправку першого пакета даних, або, якщо повідомлення не вимагає підтвердження доставки, - все повідомлення.
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);
}

В стані SendingCycle у цьому методі відбувається надсилання блоку пакетів.
SendingCycle.SendPacket:

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 );
  }
}

Глибше в код. Створення та встановлення з'єднань

Тепер, коли ми познайомилися з основними станами та методами, що використовуються для обробки станів, можна розібрати трохи докладніше кілька прикладів протоколу.
Діаграма передачі даних у нормальних умовах:Реалізація Reliable Udp протоколу для .Net

Розглянемо докладно створення connection record для з'єднання та відправлення першого пакета. Ініціатором передачі завжди виступає програма, що викликає API-метод надсилання повідомлення. Далі задіюється метод StartTransmission блоку управління передачею, що запускає передачу даних нового повідомлення.
Створення вихідного з'єднання:

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]);
}

Надсилання першого пакета (стан 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);
}

Після відправлення першого пакета відправник переходить у стан SendingCycle – очікувати на підтвердження про доставку пакета.
Сторона-одержувач, за допомогою методу EndReceive, приймає відправлений пакет, створює нову connection record і передає даний пакет, з попередньо розпареним заголовком, в обробку методу ReceivePacket стану FirstPacketReceived
Створення з'єднання на стороні, що приймає:

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);
}

Прийом першого пакета та відправка підтвердження (стан 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);
  }
}

Глибше в код. Закриття з'єднання за тайм-аутом

Відпрацювання тайм-аутів є важливою частиною Reliable UDP. Розглянемо приклад, у якому на проміжному вузлі стався збій і доставка даних в обох напрямках стала неможливою.
Діаграма закриття з'єднання по тайму-ауту:Реалізація Reliable Udp протоколу для .Net

Як видно з діаграми, робочий таймер відправника включається відразу після відправки блоку пакетів. Це відбувається в методі SendPacket стану SendingCycle.
Увімкнення робочого таймера (стан SendingCycle):

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

Періоди таймера задаються під час створення з'єднання. За умовчанням ShortTimerPeriod дорівнює 5 секунд. У прикладі він встановлений за 1,5 секунди.

У вхідного з'єднання таймер запускається після отримання останнього пакета даних, що дійшов, це відбувається в методі ReceivePacket стану Сборка
Увімкнення робочого таймера (стан Assembling):

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);
  // ...
}

У з'єднанні за час очікування робочого таймера не прийшло більше пакетів. Таймер спрацював і викликав метод ProcessPackets, в якому було виявлено втрачені пакети та вперше відправлено запити на повторну доставку.
Надсилання запитів на повторну доставку (стан Assembling):

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);
  }
}

Змінна TimerSecondTry встановилася в правда. Ця змінна відповідає за повторний перезапуск робочого таймера.

З боку відправника теж спрацьовує робочий таймер і повторно надсилається останній відправлений пакет.
Увімкнення таймера закриття з'єднання (стан SendingCycle):

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

Після чого у вихідному з'єднанні запускається таймер закриття з'єднання.
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);
}

Період очікування таймера закриття з'єднання дорівнює 30 секунд за замовчуванням.

Через нетривалий час, повторно спрацьовує робочий таймер на стороні одержувача, знову провадиться відправка запитів, після чого запускається таймер закриття з'єднання у вхідного з'єднання

Після спрацювання таймерів закриття всі ресурси обох connection record звільняються. Відправник повідомляє про невдалу доставку вищому додатку (див. API Reliable UDP).
Визволення ресурсів connection record'a:

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);
  }
}

Глибше в код. Відновлення передачі даних

Діаграма відновлення передачі даних при втраті пакета:Реалізація Reliable Udp протоколу для .Net

Як вже обговорювалося у закритті з'єднання за тайм-аутом, після закінчення робочого таймера у одержувача відбудеться перевірка на втрачені пакети. У разі наявності втрат пакетів буде складено список номерів пакетів, які не дійшли до одержувача. Дані номери заносяться до масиву LostPackets конкретного з'єднання та виконується відправка запитів на повторну доставку.
Надсилання запитів на повторну доставку пакетів (стан Assembling):

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);
      }
    }
    // ...
  }
}

Відправник прийме запит на повторну доставку і надішле пакети, що бракують. Варто зауважити, що в цей момент у відправника вже запущено таймер закриття з'єднання і при отриманні запиту він скидається.
Повторне відправлення втрачених пакетів (стан 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));
}

Повторно надісланий пакет (packet#3 на діаграмі) приймається вхідним з'єднанням. Виконується перевірка заповнення вікна прийому і звичайна передача даних відновлюється.
Перевірка на влучення у вікно прийому (стан Assembling):

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);
  }
  // ...
}

API Reliable UDP

Для взаємодії з протоколом передачі є відкритий клас Reliable Udp, що є обгорткою над блоком управління передачею. Ось найважливіші члени класу:

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()    
}

Отримання повідомлення здійснюється за передплатою. Сигнатура делегату для методу зворотного виклику:

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

Повідомлення:

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

Для підписки на конкретний тип повідомлень та/або на конкретного відправника використовуються два необов'язкові параметри: ReliableUdpMessageTypes messageType та IPEndPoint ipEndPoint.

Типи повідомлень:

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

Відправлення повідомлення здійснюється асинхронним, для цього в протоколі реалізовано асинхронну модель програмування:

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

Результат надсилання повідомлення буде true – якщо повідомлення успішно дійшло до одержувача та false – якщо з'єднання було закрито за тайм-аутом:

public bool EndSendMessage(IAsyncResult asyncResult)

Висновок

Багато чого не було описано у рамках цієї статті. Механізми узгодження потоків, обробка винятків та помилок, реалізація асинхронних методів надсилання повідомлення. Але ядро ​​протоколу, опис логіки обробки пакетів, встановлення з'єднання та відпрацювання тайм-аутів повинні прояснитися для Вас.

Продемонстрована версія протоколу надійної доставки є досить стійкою і гнучкою, і відповідає визначеним раніше вимогам. Але я хочу додати, що описана реалізація може бути вдосконалена. Наприклад, для збільшення пропускної спроможності та динамічної зміни періодів таймерів до протоколу можна додати такі механізми як sliding window та RTT, також буде корисним реалізація механізму визначення MTU між вузлами з'єднання (але тільки у разі надсилання великих повідомлень).

Дякую за увагу, чекаю на Ваші коментарі та зауваження.

PS Для тих, хто цікавиться подробицями або просто хоче протестувати протокол, посилання на проект на GitHube:
Проект Reliable UDP

Корисні посилання та статті

  1. Специфікація протоколу TCP: англійською и російською
  2. Специфікація протоколу UDP: англійською и російською
  3. Обговорення RUDP протоколу: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 и rfc 1151
  5. Проста реалізація підтвердження доставки по UDP: Take Total Control Of Your Networking With .NET And UDP
  6. Стаття, що описує механізми подолання NAT'ів: Peer-to-Peer Communication Across Network Address Translators
  7. Реалізація асинхронної моделі програмування: Implementing the CLR Asynchronous Programming Model и How to implementte IAsyncResult design pattern
  8. Перенесення асинхронної моделі програмування в асинхронний шаблон, заснований на завданнях (APM TAP):
    TPL і традиційне асинхронне програмування .NET
    Interop with Other Asynchronous Patterns and Types

Update: Дякую mayorovp и сідрістій за ідею додавання task'а до інтерфейсу Сумісність бібліотеки із старими ОС не порушується, т.к. Четвертий фреймворк підтримує і XP і 4 Server.

Джерело: habr.com

Додати коментар або відгук