Рэалізацыя Reliable Udp пратакола для.

Інтэрнэт даўно змяніўся. Адзін з асноўных пратаколаў Інтэрнэту - 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 пратакола для.

Як відаць з дыяграмы, у выпадку страты пакетаў, TCP выявіць страчаны пакет і паведаміць пра гэта адпраўніку, запытаўшы нумар страчанага сегмента.
Перадача даных па пратаколе UDP:Рэалізацыя Reliable 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 пратакола для.

Структура загалоўка 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 выкарыстоўваюцца два таймера для ўсталёўкі тайм-аўтаў. Першы, працоўны таймер, служыць для чакання адказу ад выдаленага хаста. Калі ён спрацоўвае на боку-адпраўніку, тое выконваецца паўторная адпраўка апошняга адпраўленага пакета. Калі ж таймер спрацоўвае ў атрымальніка, тое выконваецца праверка на страчаныя пакеты і адпраўляюцца запыты на паўторную дастаўку.

Другі таймер - неабходны для зачынення злучэння ў выпадку адсутнасці сувязі паміж вузламі. Для боку-адпраўніка ён запускаецца адразу пасля спрацоўвання працоўнага таймера, і чакае адказу ад выдаленага вузла. У выпадку адсутнасці адказу за вызначаны перыяд - злучэнне завяршаецца і рэсурсы вызваляюцца. Для боку-атрымальніка, таймер зачынення злучэння запускаецца пасля падвойнага спрацоўвання працоўнага таймера. Гэта неабходна для страхоўкі ад страты пакета пацверджання. Пры спрацоўванні таймера таксама завяршаецца злучэнне і вызваляюцца рэсурсы.

Дыяграма станаў перадачы Reliable UDP

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

Рэалізацыя Reliable Udp пратакола для.

Зачынена - у рэчаіснасці не з'яўляецца станам, гэта стартавая і канчатковая кропка для аўтамата. За стан Зачынена прымаецца блок кіравання перадачай, які, рэалізуючы асінхронны 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 пратакола для.

Усю логіку працы пратакола рэалізуюць прадстаўленыя вышэй класы, сумесна з дапаможным класам, якія прадстаўляюць статычныя метады, такія як, напрыклад, пабудовы загалоўка 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 пратакола для.

Разгледзім падрабязна стварэнне 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 пратакола для.

Як відаць з дыяграмы, працоўны таймер у адпраўніка ўключаецца адразу пасля адпраўкі блока пакетаў. Гэта адбываецца ў метадзе 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 пратакола для.

Як ужо абмяркоўвалася ў зачыненні злучэння па тайм-аўце, па заканчэнні працоўнага таймера ў атрымальніка адбудзецца праверка на страчаныя пакеты. У выпадку наяўнасці страт пакетаў будзе складзены спіс нумар пакетаў, якія не дайшлі да атрымальніка. Гэтыя нумары заносяцца ў масіў 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: Такі 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 and Traditional .NET Asynchronous Programming
    Interop with Other Asynchronous Patterns and Types

Update: Дзякуй mayorovp и sidristij за ідэю дадання task'а да інтэрфейсу. Сумяшчальнасць бібліятэкі са старымі АС не парушаецца, т.я. 4-ы фрэймворк падтрымлівае і XP і 2003 server.

Крыніца: habr.com

Дадаць каментар