Першапачатковая архітэктура Інтэрнэту мела на ўвазе аднастайную адрасную прастору, у якой кожны вузел меў глабальны і ўнікальны 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 будзе апісана ў наступных артыкулах.
Патрабаванні да пратакола
Надзейная дастаўка пакетаў, рэалізаваная праз механізм дадатнай зваротнай сувязі (так званы positive acknowledgment )
Неабходнасць эфектыўнай перадачы вялікіх даных, г.зн. пратакол павінен пазбягаць лішніх рэтрансляцый пакетаў
Павінна быць магчымасць адмены механізму пацверджання дастаўкі (магчымасць функцыянаваць як «чысты» UDP пратакол)
Магчымасць рэалізацыі каманднага рэжыму, з пацвярджэннем кожнага паведамлення
Базавай адзінкай перадачы даных па пратаколе павінна быць паведамленне
Гэтыя патрабаванні шмат у чым супадаюць з патрабаваннямі да 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:
Зачынена - у рэчаіснасці не з'яўляецца станам, гэта стартавая і канчатковая кропка для аўтамата. За стан Зачынена прымаецца блок кіравання перадачай, які, рэалізуючы асінхронны 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 дае інтэрфейс для стану:
Усю логіку працы пратакола рэалізуюць прадстаўленыя вышэй класы, сумесна з дапаможным класам, якія прадстаўляюць статычныя метады, такія як, напрыклад, пабудовы загалоўка ReliableUdp з connection record.
Далей будуць разгледжаны ў падрабязнасцях рэалізацыі метадаў інтэрфейсу, якія вызначаюць асноўныя алгарытмы працы пратакола.
Метад DisposeByTimeout
Метад DisposeByTimeout адказвае за вызваленне рэсурсаў злучэння па заканчэнні тайм-аўту і для сігналізацыі аб паспяховай/няўдалай дастаўкі паведамлення. ReliableUdpState.DisposeByTimeout:
Ён перавызначаны толькі ў стане Завершаны. 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 );
}
}
Глыбей у код. Стварэнне і ўстанаўленне злучэнняў
Цяпер, калі мы пазнаёміліся з асноўнымі станамі і метадамі, якія выкарыстоўваюцца для апрацоўкі станаў, можна разабраць крыху больш падрабязна некалькі прыкладаў работы пратакола. Дыяграма перадачы даных у нармальных умовах:
Разгледзім падрабязна стварэнне 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. Разгледзім прыклад, у якім на прамежкавым вузле адбыўся збой і дастаўка дадзеных у абодвух кірункі стала немагчымай. Дыяграма закрыцця злучэння па тайма-аўту:
Як відаць з дыяграмы, працоўны таймер у адпраўніка ўключаецца адразу пасля адпраўкі блока пакетаў. Гэта адбываецца ў метадзе 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):
Ва ўваходным злучэнні за час чакання працоўнага таймера не прыйшло больш пакетаў. Таймер спрацаваў і выклікаў метад 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);
}
}
Глыбей у код. Аднаўленне перадачы даных
Дыяграма аднаўлення перадачы дадзеных пры страце пакета:
Як ужо абмяркоўвалася ў зачыненні злучэння па тайм-аўце, па заканчэнні працоўнага таймера ў атрымальніка адбудзецца праверка на страчаныя пакеты. У выпадку наяўнасці страт пакетаў будзе складзены спіс нумар пакетаў, якія не дайшлі да атрымальніка. Гэтыя нумары заносяцца ў масіў 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
Update: Дзякуй mayorovp и sidristij за ідэю дадання task'а да інтэрфейсу. Сумяшчальнасць бібліятэкі са старымі АС не парушаецца, т.я. 4-ы фрэймворк падтрымлівае і XP і 2003 server.