Реалізація 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:

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

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

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

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

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

З сказаного, зрозуміло, що для створення надійного протоколу доставки повідомлень поверх UDP (надалі будемо називати Reliable UDP), потрібно реалізувати схожі з TCP механізми передачі даних. А саме:
  • зберігати стан з'єднання
  • використовувати нумерацію сегментів
  • використовувати спеціальні пакети підтвердження
  • використовувати спрощений механізм вікна, для збільшення пропускної здатності протоколу
Додатково, потрібно:
  • сигналізувати про початок повідомлення, для виділення ресурсів під з'єднання
  • сигналізувати про закінчення повідомлення, для передачі отриманого повідомлення вищестоящому додатком і вивільнення ресурсів протоколу
  • дозволити протоколу для конкретних сполук відключати механізм підтверджень доставки, щоб функціонувати як «чистий» UDP
Заголовок Reliable UDP
Згадаймо, що UDP дейтаграма інкапсулюється в IP дейтаграмму. Пакет Reliable UDP відповідно «загортається» UDP дейтаграмму.
Інкапсуляція заголовка Reliable UDP:

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



  • Flags — керуючі прапори пакета
  • MessageType — тип повідомлення, використовується вищестоящими додатками, для підписки на певні повідомлення
  • TransmissionId — номер передачі, разом з адресою і портом одержувача унікально визначає з'єднання
  • PacketNumber — номер пакета
  • Options — додаткові опції протоколу. У разі першого пакету використовується для вказівки розміру повідомлення
Прапори бувають наступні:
  • FirstPacket — перший пакет повідомлення
  • NoAsk — повідомлення не вимагає включення механізму підтвердження
  • LastPacket — останній пакет повідомлення
  • RequestForPacket — пакет підтвердження або запит на втрачений пакет
Загальні принципи роботи протоколу
Так як Reliable UDP орієнтований на гарантовану передачу повідомлень між двома вузлами, він повинен вміти встановлювати з'єднання з іншою стороною. Для установки з'єднання сторона-відправник посилає пакет з прапором FirstPacket, відповідь на який буде означати встановлення з'єднання. Всі відповідні пакети, або, по-іншому, пакети підтвердження, завжди виставляють значення поля PacketNumber на одиницю більше, ніж найбільше значення PacketNumber у успішно прийшли пакетів. В полі Options для першого надісланого пакету записується розмір повідомлення.

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

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

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

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

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

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



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

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

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

SendingCycle — основне стану для передачі пакетів повідомлень.

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

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

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

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

Assembling — основний стан для прийому пакетів повідомлення.

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

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

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

Глибше в код. Блок управління передачею
Один з ключових елементів 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 надає інтерфейс для стану:



Всю логіку роботи протоколу реалізують представлені вище класи, спільно з допоміжним класом, надають статичні методи, такі як, наприклад, побудови заголовка 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.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// повідомляємо про успішне отримання повідомлення
SetAsCompleted(connectionRecord); 
}

Метод ProcessPackets

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

У стані Assembling перевизначений метод і відповідає за перевірку втрачених пакетів і перехід в стан Completed, у разі отримання останнього пакету і проходження успішної перевірки
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 метод зупиняє робочий таймер і передає повідомлення передплатникам.
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));
}

У стані Assembling у методі 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 єдина завдання методу — відіслати повторне підтвердження про успішну доставку повідомлення.
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 );
}
}

Глибше в код. Створення і встановлення з'єднань
Тепер, коли ми познайомилися з основними станами і методами, використовуваними для обробки станів, можна розібрати трохи докладніше кілька прикладів роботи протоколу.
Діаграма передачі даних в нормальних умовах:

Розглянемо детально створення connection record для з'єднання і відправку першого пакету. Ініціатором передачі завжди виступає додаток, що викликає API-метод надсилання повідомлення. Далі використовується метод StartTransmission блока управління передачею, запускає передачу даних для нового повідомлення.
Створення вихідного з'єднання:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint security, 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. Розглянемо приклад, в якому на проміжним сайті стався збій і доставка даних в обох напрямках стала неможливою.
Діаграма закриття з'єднання по тайму-ауту:

Як видно з діаграми, робочий таймер у відправника включається відразу після відправки блоку пакетів. Це відбувається в методі 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
Включення робочого таймера (стан 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 встановилася в true. Дана змінна відповідає за автоматичний перезапуск робочого таймер.

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


Глибше в код. Відновлення передачі даних
Діаграма відновлення передачі даних при втраті пакету:

Як вже обговорювалося в закритті з'єднання по тайм-ауту, після закінчення робочого таймера в одержувача відбудеться перевірка на загублені пакети. У разі наявності втрат пакетів буде складено список номер пакетів, не дійшли до одержувача. Дані номери заносяться у масив 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)
// на асинхронну відправлення повідомлення
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 між вузлами з'єднання (але тільки в разі відправки великих повідомлень).

Спасибі за увагу, чекаю Ваших коментарів і зауважень.

P.s. Для тих, хто цікавиться подробицями або просто хоче протестувати протокол, посилання на проект на 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 With Networking .NET And UDP
  6. Стаття, що описує механізми подолання NAT'ов: Peer-to-Peer Communication Across Network Address Translators
  7. Реалізація асинхронної моделі програмування: Implementing the CLR Asynchronous Programming Model і How to implement the IAsyncResult design pattern

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.