Интернет отдавна се е променил. Един от основните протоколи на Интернет – 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'.
Допълнително, за повишаване на производителността (т.е. изпращане на повече от един сегмент без получаване на потвърдение), 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:

Затворено – всъщност не е състояние, а стартова и крайната точка за автомата. Състоянието Затворено представлява блок за управление на предаванията, който, реализирайки асинхронен 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);
}
За всяко предаване на съобщение се създава структура, съдържаща информация за връзката. Такава структура се нарича connection record.
Някои членове на класа ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord : IDisposable
{
// масив от байтов с съобщението
public byte[] IncomingStream { get; set; }
// препратка към състоянието на крайния автомат
public ReliableUdpState State { get; set; }
// двойка, която еднозначно определя записа на връзката
// в блока за управление на предаването
public Tuple 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 от записа на връзката.
По-нататък ще бъдат разгледани в детайли реализациите на методите на интерфейса, определящи основните алгоритми на работа на протокола.
Метод DisposeByTimeout
Методът DisposeByTimeout е отговорен за освобождаването на ресурсите на връзката след изтичането на тайм-аута и за сигнализиране на успешната/неуспешната доставка на съобщението.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Той е преопределен само в състояние Завършено.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// информираме за успешно получено съобщение
SetAsCompleted(connectionRecord);
}
Метод ProcessPackets
Методът ProcessPackets отговаря за допълнителна обработка на пакет или пакети. Извиква се директно или чрез таймер за изчакване на пакети.
В състояние Assembling методът е преопределен и отговаря за проверката на загубените пакети и прехода в състояние Завършено, в случай че последният пакет е получен и успешната проверка е премината
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;
}
// по замисъл всички номера на пакети започват от 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 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);
}
}
Дълбочинно в кода. Създаване и установяване на връзки
Сега, след като се запознахме с основните състояния и методи, използвани за обработка на състояния, можем да разгледаме малко по-подробно няколко примера за работа на протокола.
Диаграма на предаване на данни при нормални условия:
Нека подробно разгледаме създаването connection record за свързване и изпращане на първоначалния пакет. Инициатор на предаването винаги е приложението, което извиква 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 key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// създаваме нова запис за свързване и проверяваме,
// дали вече съществува такъв номер в нашите речници
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// ако съществува – генерираме случайно число отново
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(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, приема изпратения пакет, създава нова connection record и предава този пакет, с предварително обработено заглавие, на метода ReceivePacket за обработка на състоянието. FirstPacketReceived
Създаване на връзка от страната на получателя:
private void EndReceive(IAsyncResult ar)
{
// ...
// пакетът е получен
// парсваме заглавката на пакета
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// получен е некоректен пакет - отхвърляме го
return;
}
// конструираме ключ за определяне на connection record'а за пакета
Tuple key = new Tuple(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;
// ...
// по проект всички номера на пакети започват от 0;
if (header.PacketNumber != 0)
return;
// инициализиране на масив за съхранение на части от съобщението
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// записваме данните от пакета в масива
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// изчисляваме броя на пакетите, които трябва да пристигнат
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length/(double)connectionRecord.BufferSize));
// записваме номера на последния получен пакет (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// след това увеличаваме долната граница на приемната врата с 1
connectionRecord.WindowLowerBound++;
// превключваме състоянието
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*ако не е необходим механизма за потвърждение*/)
// ...
else
{
// изпращаме потвърждение
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Дълбочинно в кода. Затваряне на връзка при таймаут
Обработката на тайм-аутите е важна част от Reliable UDP. Нека разгледаме пример, в който на междинния възел е настъпила повреда и доставката на данни в двете посоки е станала невъзможна.
Диаграма за затваряне на връзката по тайм-аут:
Както се вижда от диаграмата, работният таймер на подателя се включва веднага след изпращането на блока пакети. Това се случва в метода SendPacket в състоянието SendingCycle.
Включване на работния таймер (състояние SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// изпращаме блок от пакети
// ...
// рестартираме таймера след изпращането
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
Периодите на таймера се задават по време на създаване на връзка. По подразбиране ShortTimerPeriod е 5 секунди. В примера той е зададен на 1,5 секунди.
При входяща връзка таймерът стартира след получаване на последния пакет данни, което се случва в метода ReceivePacket на състоянието. Assembling
Включване на работния таймер (състояние Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// перезапускаме таймерите
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
При входящата връзка за времето на изчакване на работния таймер не са получени допълнителни пакети. Таймерът сработи и извика метода ProcessPackets, в който бяха открити изгубени пакети и за първи път бяха изпратени искания за повторна доставка.
Изпращане на искания за повторна доставка (състояние Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*проверка на изгубени пакети */)
{
// изпращаме искания за повторна доставка
// установяваме таймер за втори път, за повторна опит за предаване
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// ако след два опита на WaitForPacketTimer
// не са получени пакети - стартираме таймера за завършване на връзката
StartCloseWaitTimer(connectionRecord);
}
else if (/*пришел последният пакет и успешна проверка */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// ако ack за блока пакети е изгубен
else
{
if (!connectionRecord.TimerSecondTry)
{
// повторно отдаваме ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// стартираме таймера за завършване на връзката
StartCloseWaitTimer(connectionRecord);
}
}
Променливата TimerSecondTry беше установена в true. Тази променлива отговаря за повторното стартиране на работния таймер.
От страна на изпращача също сработва работния таймер и последният изпратен пакет се изпраща отново.
Включване на таймера за затваряне на връзката (състояние SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// изпращаме отново последния пакет
// ...
// включваме таймера CloseWait – за изчакване на възстановяването на връзката или нейното завършване
StartCloseWaitTimer(connectionRecord);
}
След това в изходящото съединение се стартира таймер за затваряне на съединението.
ReliableUdpState.StartCloseWaitTimer:
protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
else
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}
Периодът на изчакване на таймера за затваряне на съединението по подразбиране е 30 секунди.
След кратко време, работният таймер от страната на получателя се активира отново, изпращат се заявки, след което стартира таймер за затваряне на съединението на входящото съединение.
При активиране на таймерите за затваряне, всички ресурси на двата connection record-а се освобождават. Изпращачът съобщава за неуспешно доставяне на по-горното приложение (вж. API Reliable UDP).
Освобождаване на ресурсите на connection record-а:
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 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 сървър
StunRequest = 1,
// Отговор от STUN сървър
StunResponse = 2,
// Предаване на файл
FileTransfer =3,
// ...
}
Изпращането на съобщение става асинхронно, за целта в протокола е реализирана асинхронна модел на програмиране:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Резултатът от изпращането на съобщението ще бъде true – ако съобщението е стигнало успешно до получателя и false – ако връзката е била затворена поради тайм-аут:
public bool EndSendMessage(IAsyncResult asyncResult)
Заключение
Много не беше описано в рамките на тази статия. Механизмите за синхронизиране на потоците, обработката на изключения и грешки, реализирането на асинхронни методи за изпращане на съобщения. Но ядрата на протокола, описанието на логиката за обработка на пакети, установяването на връзка и управлението на таймаутите трябва да станат ясни за вас.
Демонстрираната версия на протокола за надеждно доставяне е достатъчно устойчива и гъвкава, и отговаря на предвидените изисквания. Но искам да добавя, че описаната реализация може да бъде усъвършенствана. Например, за увеличаване на пропускателната способност и динамична смяна на периодите на таймерите, в протокола могат да се добавят механизми като sliding window и RTT, а реализирането на механизъм за определяне на MTU между възлите на връзката (но само в случай на изпращане на големи съобщения) също би било полезно.
Благодаря за вниманието, очаквам вашите коментари и забележки.
P.S. За тези, които се интересуват от подробности или просто искат да тестват протокола, линкът към проекта в GitHub:
Полезни линкове и статии
- Спецификация на протокол TCP: и
- Спецификация на протокол UDP: и
- Обсъждане на протокола RUDP:
- Reliable Data Protocol: и
- Проста реализация на потвърждаване на доставката по UDP:
- Статия, описваща механизмите за преодоляване на NAT:
- Реализиране на асинхронен модел за програмиране: и
- Пренасяне на асинхронния модел за програмиране в асинхронен шаблон, базиран на задачи (APM в TAP):
Актуализация: Благодаря и за идеята за добавяне на таска към интерфейса. Съвместимостта на библиотеката със стари ОС не е нарушена, тъй като 4-ият фреймворк поддържа и XP, и 2003 сървър.
Източник: habr.com
