Интернетът отдавна се е променил. Един от основните протоколи на интернет – UDP, се използва от приложения не само за доставка на дейтаграми и широковещателни разпратки, но и за осигуряване на „peer-to-peer“ връзки между възлите в мрежата. Поради простата си конструкция, този протокол е придобил множество непланирани начини на приложение, но недосъпненията на протокола, като липсата на гарантирана доставка, все пак не изчезват. В тази статия се описва реализацията на протокол за гарантирана доставка над 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 ще бъде описана в следващите статии.
Изисквания към протокола
- Надеждното доставяне на пакети се реализира чрез механизма на положителна обратна връзка (така нареченото positive acknowledgment).
- Необходимостта от ефективно предаване на големи данни, т.е. протоколът трябва да избягва излишни повторни предавания на пакети.
- Трябва да има възможност за отмяна на механизма за потвърждение на доставката (възможност за функциониране като "чист" UDP протокол).
- Възможност за реализиране на режим на командване с потвърждение на всяко съобщение.
- Основната единица за предаване на данни по протокола трябва да бъде съобщение.
Тези изисквания до голяма степен съвпадат с изискванията към Reliable Data Protocol, описани в и , и аз се основавах на тези стандарти при разработването на този протокол.
За да разберат тези изисквания, нека разгледаме времевите диаграми на предаване на данни между две възли в мрежата по протоколите TCP и UDP. Нека в двата случая загубим един пакет.
Предаване на неинтерактивни данни по TCP:
Както се вижда от диаграмата, в случай на загуба на пакети, TCP ще идентифицира загубения пакет и ще уведоми изпращача, поискавайки номера на загубения сегмент.
Предаване на данни по протокола UDP:
UDP не предприема никакви действия за откритие на загуби. Контролът на грешките при предаване в UDP протокола е напълно възложеният на приложението.
Откритие на грешки в TCP протокола се постига чрез установяване на връзка с крайния възел, запазване на състоянието на тази връзка, уточняване на номера на изпратените байтове във всеки заглавен пакет и уведомления за получаване с номера на потвърждение "acknowledge number".
Допълнително, за да се увеличи производителността (т.е. да се изпратят повече от един сегмент без получаване на потвърдениe), TCP протоколът използва т. нар. предавателно прозорец — брой байтове данни, които изпращачът на сегмента очаква да получи.
По-подробно с TCP протокола може да се запознаете в , с UDP в , където те са определени.
От гореописаното е ясно, че за да се създаде надежден протокол за доставка на съобщения над UDP (в по-нататъшния текст ще наричаме Reliable UDP), е необходимо да се реализират механизми за предаване на данни, подобни на тези в TCP. А именно:
- да се запази състоянието на връзката
- да се използва нумерация на сегменти
- да се използват специални пакети за потвърждение
- да се използва опростен механизъм на прозорец, за да се увеличи пропускателната способност на протокола
Допълнително, е необходимо:
- да се сигнализира за началото на съобщението, за да се отделят ресурси за връзката
- да се сигнализира за края на съобщението, за да се предаде полученото съобщение на по-горното приложение и да се освободят ресурсите на протокола
- да се позволи на протокола за конкретни връзки да деактивира механизма за потвърждение на доставката, за да функционира като „чист“ UDP
Заглавие на Reliable UDP
Да припомним, че UDP дейтаграмата е инкапсулирана в IP дейтаграма. Пакетът Reliable UDP съответно „се опакова“ в UDP дейтаграма.
Инкапсулация на заглавката на Reliable UDP:
Структурата на заглавката на Reliable UDP е достатъчно проста:

- Flags – управляващи флагове на пакета
- MessageType – тип на съобщението, използва се от по-горните приложения за абониране за определени съобщения
- TransmissionId — номер на предаване, заедно с адреса и порта на получателя уникално определя връзката
- PacketNumber – номер на пакета
- Options – допълнителни опции на протокола. В случай на първия пакет се използва за указване на размера на съобщението
Флаговете са следните:
- FirstPacket — първи пакет на съобщението
- NoAsk — съобщението не изисква включване на механизма за потвърждение
- LastPacket — последен пакет на съобщението
- RequestForPacket — пакет за потвърждение или искане за загубен пакет
Основни принципи на работа на протокола
Тъй като Reliable UDP е ориентиран към гарантирано предаване на съобщения между два възела, той трябва да може да установи връзка с другата страна. За установяване на връзка страната-изпращач изпраща пакет с флага FirstPacket, отговорът на който означава установяване на връзката. Всички отговорни пакети, или, по друг начин, пакети за потвърждение, винаги поставят стойността на полето PacketNumber на едно по-голямо число от най-голямото значение на PacketNumber на успешно получените пакети. В полето Options за първия изпратен пакет се записва размерът на съобщението.
За завършване на връзката се използва подобен механизъм. В последния пакет съобщение се установява флаг LastPacket. В отговорния пакет се посочва номерът на последния пакет + 1, което за приемащата страна означава успешно доставяне на съобщението.
Диаграма за установяване и приключване на връзката:
Когато връзката е установена, започва предаването на данни. Данните се предават на блокове пакети. Всеки блок, с изключение на последния, съдържа фиксирано количество пакети. То е равно на размера на прозореца за прием/предаване. Последният блок данни може да има по-малко пакети. След изпращането на всеки блок, страната-изпращач очаква потвърждение за доставяне или искане за повторно предаване на загубени пакети, оставяйки прозореца за прием/предаване отворен, за да получи отговори. След получаване на потвърждение за доставяне на блока, прозорецът за прием/предаване се изместват и се изпраща следващият блок данни.
Страната-получател приема пакети. Всеки пакет се проверява дали попада в прозореца за предаване. Не попадащите в прозореца пакети и дубликатите се отсяват. Тъй като размерът на прозореца е строго фиксиран и еднакъв както за получателя, така и за изпращача, в случай на доставяне на блок пакети без загуби, прозорецът се измести, за да приеме пакети от следващия блок данни и се изпраща потвърждение за доставянето. Ако прозорецът не се запълни за установения период на работния таймер, ще бъде стартирана проверка за това, кои пакети не са били доставени и ще бъдат изпратени искания за повторно предаване.
Диаграма за повторна предаване:
Тайм-аутове и таймери на протокола
Съществуват няколко причини, поради които не може да бъде установена връзка. Например, ако приемащата страна е извън мрежата. В такъв случай, при опит за установяване на връзка, тя ще бъде затворена поради тайм-аут. В реализацията на Reliable UDP се използват два таймера за задаване на тайм-аутите. Първият, работният таймер, служи за изчакване на отговор от отдалечен хост. Ако той сработи от страна на изпращача, последният изпратен пакет ще бъде повторно изпратен. Ако таймерът сработи при получателя, ще се извърши проверка за загубени пакети и ще се изпратят искания за повторно предаване.
Вторият таймер е необходим, за да затвори връзката в случай на загуба на свързаност между възлите. На страната на изпращача той се задейства веднага след активацията на работния таймер и очаква отговор от отдалечения възел. При липса на отговор за установения период, връзката се прекратява и ресурсите се освобождават. На страната на получателя таймерът за затваряне на връзката се задейства след двойно сработване на работния таймер. Това е необходимо, за да се застраховаме срещу загуба на пакета за потвърждение. Когато таймерът сработи, връзката също се прекратява и ресурсите се освобождават.
Схема на състоянията на Reliable UDP
Принципите на работа на протокола са реализирани в краен автомат, всяко състояние от който отговаря за определена логика по обработка на пакетите.
Диаграма на състоянията на Reliable UDP:

Closed не е реално състояние, а стартова и крайна точка за автомата. Състоянието Closed приемаме за блок за управление на предаването, който, реализирайки асинхронен UDP сървър, препраща пакетите в съответните връзки и стартира обработката на състоянията.
FirstPacketSending е начално състояние, в което изходящата връзка се намира при изпращане на съобщение.
В това състояние се изпраща първият пакет за стандартни съобщения. За съобщения без потвърждение за изпращане, това е единственото състояние – в него се осъществява изпращането на цялото съобщение.
SendingCycle е основното състояние за предаване на пакети със съобщения.
Преминаването в него от състояние FirstPacketSending се осъществява след изпращането на първия пакет на съобщението. В това състояние се получават всички потвърждения и искания за повторни предавания. Изходът от него е възможен в два случая – при успешно доставяне на съобщението или по времева граница.
FirstPacketReceived е начално състояние за получателя на съобщението.
В него се проверява коректността на началото на предаването, създават се необходимите структури и се изпраща потвърждение за получаване на първия пакет.
За съобщение, състоящо се от един единствен пакет и изпратено без потвърждение за доставка – това е единственото състояние. След обработката на такова съобщение връзката се затваря.
Assembling е основното състояние за прием на пакети със съобщения.
В него се записват пакети във временно хранилище, проверява се за загуба на пакети, изпращат се потвърждения за доставката на блокове пакети и съобщения изцяло, а също така се изпращат запитвания за повторна доставка на изгубени пакети. В случаят, че цялото съобщение бъде успешно получено – връзката преминава в състояние Завършено, в противен случай се извършва изход по тайм-аут.
Завършено – затваряне на връзката в случаят на успешно получено съобщение.
Това състояние е необходимо за сглобяване на съобщението и в случай, когато потвърждението за доставката на съобщението е изгубено по пътя към изпращача. Изходът от това състояние се извършва по тайм-аут, но връзката се счита за успешно затворена.
По-дълбоко в кода. Блок за управление на предаването
Един от ключовите елементи на Reliable UDP – блокът за управление на предаването. Задачата на този блок е да съхранява текущите връзки и помощните елементи, да разпределя получените пакети по съответните връзки, да предлага интерфейс за изпращане на пакети през връзката и да реализира API протокола. Блокът за управление на предаването приема пакети от UDP слоя и ги пренасочва за обработка в крайния автомат. За приема на пакети в него е реализиран асинхронен UDP сървър.
Някои членове на класа ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// масив от байтове за зададения ключ. Използва се за сглобяване на входящите съобщения
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// масив от байтове за зададения ключ. Използва се за изпращане на изходящи съобщения.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// запис на връзка за зададения ключ.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// списък на абонатите на съобщения.
private readonly List 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;
}
// конструираме ключ за определяне на запис на връзката за пакета
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// получаваме съществуващ запис на връзката или създаваме нов
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// стартираме пакета за обработка в краен автомат
record.State.ReceivePacket(record, header, bytes);
}
За всяка трансфер на съобщение се създава структура, съдържаща сведения за връзката. Тази структура се нарича запис на връзка.
Някои членове на класа ReliableUdpConnectionRecord:
вътрешен клас ReliableUdpConnectionRecord : IDisposable
{
\/\/ масив от байтове със съобщение
публичен byte[] IncomingStream { get; set; }
\/\/ препратка към състоянието на автомат
публичен ReliableUdpState State { get; set; }
\/\/ двойка, която недвусмислено определя запис на връзката
\/\/ в блока за контрол на предаването
публичен Tuple Key { get; private set;}
\/\/ долна граница на приемното прозорец
публичен int WindowLowerBound;
\/\/ размер на прозореца за предаване
публичен readonly int WindowSize;
\/\/ номер на пакета за изпращане
публичен int SndNext;
\/\/ брой пакети за изпращане
публичен int NumberOfPackets;
\/\/ номер на предаването (това е втората част на Tuple)
\/\/ за всяко съобщение свое
публичен readonly Int32 TransmissionId;
\/\/ отдалечен IP крайна точка – получател на съобщението
публичен readonly IPEndPoint RemoteClient;
\/\/ размер на пакета, за да се избегне фрагментацията на IP ниво
\/\/ не трябва да надвишава MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
публичен readonly int BufferSize;
\/\/ блок за управление на предаването
публичен readonly ReliableUdpConnectionControlBlock Tcb;
\/\/ инкапсулира резултатите от асинхронната операция за BeginSendMessage\/EndSendMessage
публичен readonly AsyncResultSendMessage AsyncResult;
\/\/ не изпращайте пакети за потвърждение
публичен bool IsNoAnswerNeeded;
\/\/ последният коректно получен пакет (винаги се задава на най-голямото число)
публичен int RcvCurrent;
\/\/ масив с номера на загубени пакети
публичен int[] LostPackets { get; private set; }
\/\/ получен ли е последният пакет. Използва се като bool.
публичен int IsLastPacketReceived = 0;
\/\/...
}
По-дълбоко в кода. Състояния
Състоянията реализират краен автомат на протокола Reliable UDP, в който се извършва основната обработка на пакетите. Абстрактният клас ReliableUdpState предоставя интерфейс за състоянието:

Цялата логика на работа на протокола се реализира от представените по-горе класове, заедно с помощен клас, предоставящ статични методи, като например построяване на заглавки ReliableUdp от запис на връзка.
По-долу ще бъдат разгледани в детайли имплементациите на методите на интерфейса, определящи основните алгоритми на работа на протокола.
Метод DisposeByTimeout
Методът DisposeByTimeout отговаря за освобождаването на ресурсите на връзката вследствие на изтичането на времето за изчакване и за сигнализиране за успешно\/неуспешно доставяне на съобщението.
ReliableUdpState.DisposeByTimeout:
защитен виртуален удостоверение DisposeByTimeout(обект запис)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) запис;
ако (запис.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Той е overridden само в състоянието Завършено.
Completed.DisposeByTimeout:
защитен обхват void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// уведомяване за успешно получаване на съобщението
SetAsCompleted(connectionRecord);
}
Метод ProcessPackets
Методът ProcessPackets отговаря за допълнителна обработка на пакет или пакети. Извиква се директно или чрез таймер за изчакване на пакети.
в състояние Assembling методът е преопределен и отговаря за проверка на изгубените пакети и прехода към състояние Завършено, в случай на получаване на последния пакет и успешна проверка
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;
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;
}
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));
connectionRecord.RcvCurrent = header.PacketNumber;
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 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;
}
// това е отговор на първия пакет с потвърждение
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// без изместване на прозореца
SendPacket(connectionRecord);
}
// пристигна потвърждение за получаване на блок данни
else if (header.PacketNumber == windowHighestBound)
{
// изместваме прозореца за прием/предаване
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// нулираме масива за контрол на предаването
connectionRecord.WindowControlArray.Nullify();
// изпращаме блок от пакети
SendPacket(connectionRecord);
}
// това е заявка за повторно предаване – изпращаме необходимия пакет
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
в състояние Assembling В метода ReceivePacket се извършва основната работа по събиране на съобщението от получените пакети.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// обработка на пакети без механизъм за потвърждение на доставката
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// нулиране на таймера
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// записване на данните
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// ако получим пакет с последния флаг - приключваме
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// изчисляване на крайната граница на прозореца
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// отхвърляне на пакети, които не попадат в прозореца
if (header.PacketNumber (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);
}
}
По-дълбоко в кода. Създаване и установяване на връзки
Сега, след като се запознахме с основните състояния и методи, използвани за обработка на състоянията, можем да разгледаме малко по-подробно няколко примера за работа на протокола.
Диаграма на предаване на данни при нормални условия:
Нека разгледаме подробно създаването запис на връзка за свързване и изпращане на първия пакет. Инициатор на предаването винаги е приложението, което извиква API метода за изпращане на съобщение. След това се задейства методът StartTransmission на блока за управление на предаването, който стартира предаването на данни за ново съобщение.
Създаване на изходящо свързване:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "Трябва да използвате конструктор с параметри или да стартирате слушателя преди изпращането на съобщение" );
}
\/\/ стартираме обработката на входящите пакети
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("Комбинацията TransmissionId & EndPoint вече съществува в речника");
}
\/\/ стартирахме състоянието в обработка
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, приема изпратения пакет, създава нова запис на връзка и предава този пакет, с предварително обработено заглавие, на метода ReceivePacket в състояние FirstPacketReceived
Създаване на връзка от страната на получателя:
private void EndReceive(IAsyncResult ar)
{
// ...
// пакетът е получен
// парсираме заглавката на пакета
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// получен некоректен пакет - отхвърляме го
return;
}
// конструираме ключ за определяне на записа за връзката за пакета
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// получаваме съществуващия запис за връзка или създаваме нов
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;
// ...
// по проект всички номера на пакети започват от 0;
if (header.PacketNumber != 0)
return;
// инициализиране на масив за съхранение на части от съобщението
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// записваме данните на пакета в масива
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// изчисляваме броя на пакетите, които трябва да пристигнат
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
// записваме номера на последно получен пакет (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// след това местим прозореца за приемане с 1
connectionRecord.WindowLowerBound++;
// сменяме състоянието
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*ако не се изисква механизъм за потвърждение*/)
// ...
else
{
// изпращаме потвърждение
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
По-дълбоко в кода. Затваряне на връзка по тайм-аут
Обработката на тайм-аутите е важна част от Reliable UDP. Нека разгледаме пример, в който на междинния възел е настъпил срив и доставката на данни в двете направления е станала невъзможна.
Диаграма за затваряне на връзката по тайм-аут:
Както се вижда от диаграмата, работният таймер на изпращача се включва веднага след изпращането на блока от пакети. Това се случва в метода SendPacket на състоянието SendingCycle.
Включване на работния таймер (състояние SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// изпращаме блок от пакети
// ...
// рестартираме таймера след изпращането
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
Периодите на таймера се задават при създаването на връзка. По подразбиране ShortTimerPeriod е 5 секунди. В примера, той е зададен на 1,5 секунди.
За входящата връзка таймерът започва след получаване на последния пристигнал пакет данни, което се случва в метода ReceivePacket на състоянието. Assembling
Включване на работния таймер (състояние Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// рестартиране на таймерите
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
В входящото съединение в периода на изчакване на работния таймер не са пристигнали повече пакети. Таймерът сработи и извика метода ProcessPackets, в който бяха открити загубени пакети и за първи път бяха изпратени заявки за повторна доставка.
Изпращане на заявки за повторна доставка (състояние Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*проверка на загубените пакети */)
{
// изпращаме заявки за повторна доставка
// задаваме таймера за втори път, за повторна опит за предаване
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// ако след два опита за сработване на WaitForPacketTimer
// не успеем да получим пакети - стартираме таймер за завършване на връзката
StartCloseWaitTimer(connectionRecord);
}
else if (/*пристигна последният пакет и успешна проверка */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// ако ack на блока пакети е загубен
else
{
if (!connectionRecord.TimerSecondTry)
{
// отново изпращаме ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// стартираме таймер за завършване на връзката
StartCloseWaitTimer(connectionRecord);
}
}
Променливата TimerSecondTry е зададена на истинно. Тази променлива отговаря за повторното стартиране на работния таймер.
От страна на изпращача също сработва работния таймер и последният изпратен пакет се изпраща отново.
Включване на таймера за затваряне на връзката (състояние 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 секунди.
След кратко време, работният таймер на страната на получателя отново сработва, изпращат се нови заявки, след което се активира таймерът за затваряне на връзката за входящата връзка.
При сработването на таймерите за затваряне, всички ресурси на двете записи на връзките се освобождават. Изпращачът съобщава за неуспешна доставка на по-горното приложение (вж. API Reliable UDP).
Освобождаване на ресурсите на записа на връзката:
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
За взаимодействие с протоколa за пренос на данни съществува отворен клас Reliable Udp, представляващ обвивка над блока за управление на преноса. Ето най-важните членове на класа:
публичный запечатанный класс ReliableUdp : IDisposable
{
// получает локальную конечную точку
публичный IPEndPoint LocalEndpoint
// создает экземпляр ReliableUdp и запускает
// прослушивание входящих пакетов на указанном IP адресе
// и порту. Значение 0 для порта означает использование
// динамически выделенного порта
публичный ReliableUdp(IPAddress localAddress, int port = 0)
// подписка на получение входящих сообщений
публичный ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// отписка от получения сообщений
публичный void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// асинхронно отправить сообщение
// Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
публичный Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// начать асинхронную отправку сообщения
публичный IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// получить результат асинхронной отправки
публичный bool EndSendMessage(IAsyncResult asyncResult)
// очистить ресурсы
публичный void Dispose()
}
Получаването на съобщение става чрез абонамент. Сигнатура на делегат за метода за обратна връзка:
публичен делегат void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Съобщение:
публичен клас ReliableUdpMessage
{
// тип на съобщението, простo изброение
публичен ReliableUdpMessageTypes Type { get; private set; }
// данни на съобщението
публичен byte[] Body { get; private set; }
// ако е зададено на true – механизмът за потвърждение на доставката ще бъде деактивиран
// за предаване на конкретно съобщение
публичен bool NoAsk { get; private set; }
}
За абониране за конкретен тип съобщения и/или за конкретен изпращач се използват два незадължителни параметъра: ReliableUdpMessageTypes messageType и IPEndPoint ipEndPoint.
Типове съобщения:
публичен перечисление ReliableUdpMessageTypes : short
{
// Всяко
Any = 0,
// Запитване към STUN сървър
StunRequest = 1,
// Отговор от STUN сървър
StunResponse = 2,
// Предаване на файл
FileTransfer = 3,
// ...
}
Изпращането на съобщение се осъществява асинхронно, за целта в протокола е реализирана асинхронна модел на програмиране:
публичен IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Резултатът от изпращането на съобщение ще бъде true – ако съобщението успешно е достигнало до получателя и false – ако връзката е била затворена поради изтичане на времето:
публичен bool EndSendMessage(IAsyncResult asyncResult)
Заключение
Много не беше описано в рамките на тази статия. Механизмите за синхронизация на потоците, обработката на изключения и грешки, реализацията на асинхронни методи за изпращане на съобщения. Но ядрото на протокола, описанието на логиката за обработка на пакети, установяването на връзка и обработката на тайм-аутите трябва да станат ясни за Вас.
Демонстрираната версия на протокола за надеждно предаване е достатъчно стабилна и гъвкава и отговаря на определени предходни изисквания. Но искам да добавя, че описаната реализация може да бъде усъвършенствана. Например, за увеличаване на пропускателната способност и динамично изменение на периодите на таймерите, в протокола могат да бъдат добавени механизми като sliding window и RTT, а също така ще е полезно реализирането на механизъм за определяне на MTU между възлите на връзката (но само в случай на изпращане на големи съобщения).
Благодаря за вниманието, очаквам Вашите коментари и забележки.
P.S. За тези, които се интересуват от подробности или просто искат да тестват протокола, линк към проекта в GitHub:
Полезни връзки и статии
- Спецификация на протокола TCP: и
- Спецификация на протокола UDP: и
- Дискусия за RUDP протокола:
- Reliable Data Protocol: и
- Проста реализация на потвърждение за доставка по UDP:
- Статия, описваща механизмите за преодоляване на NAT:
- Реализация на асинхронен модел на програмиране: и
- Пренасяне на асинхронния модел на програмиране в асинхронен шаблон, основан на задачи (APM в TAP):
Актуализация: Благодаря и за идеята да добавите задача към интерфейса. Съвместимостта на библиотеката със стари ОС не е нарушена, тъй като 4-ият фреймворк поддържа и XP и 2003 server.
Източник: habr.com
