Originalna arhitektura Interneta pretpostavljala je homogeni adresni prostor u kojem je svaki čvor imao globalnu i jedinstvenu IP adresu i mogao je komunicirati direktno sa drugim čvorovima. Sada Internet, u stvari, ima drugačiju arhitekturu – jedno područje globalnih IP adresa i mnoga područja sa privatnim adresama skrivenim iza NAT uređaja.U ovoj arhitekturi, samo uređaji u globalnom adresnom prostoru mogu lako komunicirati sa bilo kim na mreži jer imaju jedinstvenu, globalno rutabilnu IP adresu. Čvor u privatnoj mreži može se povezati s drugim čvorovima na istoj mreži, a također se može povezati s drugim dobro poznatim čvorovima u globalnom adresnom prostoru. Ova interakcija se uglavnom postiže zahvaljujući mehanizmu prevođenja mrežnih adresa. NAT uređaji, kao što su Wi-Fi ruteri, kreiraju posebne unose u tablici prijevoda za odlazne veze i modificiraju IP adrese i brojeve portova u paketima. Ovo omogućava odlazne veze iz privatne mreže na hostove u globalnom adresnom prostoru. Ali u isto vrijeme, NAT uređaji obično blokiraju sav dolazni promet osim ako nisu postavljena posebna pravila za dolazne veze.
Ova arhitektura Interneta je dovoljno ispravna za komunikaciju klijent-server, gdje klijenti mogu biti u privatnim mrežama, a serveri imaju globalnu adresu. Ali to stvara poteškoće za direktnu vezu između dva čvora razne privatne mreže. Direktna veza između dva čvora važna je za peer-to-peer aplikacije kao što je prijenos glasa (Skype), dobivanje udaljenog pristupa računaru (TeamViewer) ili igranje na mreži.
Jedna od najefikasnijih metoda za uspostavljanje peer-to-peer veze između uređaja na različitim privatnim mrežama naziva se bušenje rupa. Ova tehnika se najčešće koristi sa aplikacijama zasnovanim na UDP protokolu.
Ali ako vaša aplikacija zahtijeva zagarantovanu isporuku podataka, na primjer, prenosite datoteke između računala, tada će korištenje UDP-a imati mnogo poteškoća zbog činjenice da UDP nije zajamčeni protokol isporuke i ne pruža isporuku paketa po redu, za razliku od TCP-a. protokol.
U ovom slučaju, da bi se osigurala zagarantovana isporuka paketa, potrebno je implementirati protokol sloja aplikacije koji pruža potrebnu funkcionalnost i radi preko UDP-a.
Odmah želim napomenuti da postoji tehnika probijanja TCP rupa za uspostavljanje TCP veza između čvorova u različitim privatnim mrežama, ali zbog nedostatka podrške za to od strane mnogih NAT uređaja, obično se ne smatra glavnim načinom povezivanja. takvi čvorovi.
U nastavku ovog članka fokusirat ću se samo na implementaciju protokola zajamčene isporuke. Implementacija UDP tehnike bušenja rupa bit će opisana u sljedećim člancima.
Zahtjevi protokola
Pouzdana isporuka paketa implementirana kroz mehanizam pozitivne povratne informacije (tzv. pozitivna potvrda)
Potreba za efikasnim prijenosom velikih podataka, tj. protokol mora izbjegavati nepotrebno prenošenje paketa
Trebalo bi biti moguće otkazati mehanizam potvrde isporuke (mogućnost funkcioniranja kao "čisti" UDP protokol)
Mogućnost implementacije komandnog režima, uz potvrdu svake poruke
Osnovna jedinica prenosa podataka preko protokola mora biti poruka
Ovi zahtjevi se u velikoj mjeri poklapaju sa zahtjevima Protokola pouzdanih podataka opisanim u RFC 908 и RFC 1151, i oslanjao sam se na te standarde kada sam razvijao ovaj protokol.
Da bismo razumjeli ove zahtjeve, pogledajmo vrijeme prijenosa podataka između dva mrežna čvora koristeći TCP i UDP protokole. Neka u oba slučaja izgubimo jedan paket. Prijenos neinteraktivnih podataka preko TCP-a:
Kao što možete vidjeti iz dijagrama, u slučaju gubitka paketa, TCP će otkriti izgubljeni paket i prijaviti ga pošiljaocu tražeći broj izgubljenog segmenta. Prijenos podataka putem UDP protokola:
UDP ne poduzima nikakve korake za otkrivanje gubitaka. Kontrola grešaka u prijenosu u UDP protokolu je u potpunosti odgovornost aplikacije.
Otkrivanje greške u TCP protokolu se postiže uspostavljanjem veze sa krajnjim čvorom, pohranjivanjem stanja te veze, naznakom broja bajtova poslatih u zaglavlju svakog paketa i obavještavanjem o prijemu pomoću broja potvrde.
Dodatno, za poboljšanje performansi (tj. slanje više od jednog segmenta bez primanja potvrde), TCP protokol koristi takozvani prozor za prijenos - broj bajtova podataka koje pošiljatelj segmenta očekuje da primi.
Za više informacija o TCP protokolu, pogledajte RFC 793, od UDP do RFC 768gde su, u stvari, definisani.
Iz navedenog je jasno da u cilju stvaranja pouzdanog protokola za isporuku poruka preko UDP-a (u daljem tekstu: Pouzdan UDP), potrebno je implementirati mehanizme prijenosa podataka slične TCP-u. naime:
sačuvati stanje veze
koristite numeraciju segmenata
koristite posebne pakete potvrde
koristite pojednostavljeni mehanizam prozora za povećanje protoka protokola
Dodatno vam je potrebno:
signalizira početak poruke, da dodijeli resurse za vezu
signalizirati kraj poruke, da bi primljenu poruku proslijedili uzvodnoj aplikaciji i oslobodili resurse protokola
dozvolite protokolu specifičnom za vezu da onemogući mehanizam potvrde isporuke da funkcionira kao "čisti" UDP
Pouzdano UDP zaglavlje
Podsjetimo da je UDP datagram inkapsuliran u IP datagram. Pouzdan UDP paket je na odgovarajući način "umotan" u UDP datagram. Pouzdana enkapsulacija UDP zaglavlja:
Struktura Pouzdanog UDP zaglavlja je prilično jednostavna:
Zastavice - zastavice za kontrolu paketa
MessageType - vrsta poruke koju koriste upstream aplikacije za pretplatu na određene poruke
TransmissionId - broj prenosa, zajedno sa adresom i portom primaoca, jedinstveno identifikuje vezu
PacketNumber - broj paketa
Opcije - dodatne opcije protokola. U slučaju prvog paketa, koristi se za označavanje veličine poruke
Zastave su sljedeće:
FirstPacket - prvi paket poruke
NoAsk - poruka ne zahtijeva da se omogući mehanizam potvrde
LastPacket - posljednji paket poruke
RequestForPacket - paket potvrde ili zahtjev za izgubljenim paketom
Opšti principi protokola
Pošto je pouzdan UDP fokusiran na garantovani prenos poruka između dva čvora, mora biti u stanju da uspostavi vezu sa drugom stranom. Da bi uspostavio vezu, pošiljalac šalje paket sa oznakom FirstPacket, čiji će odgovor značiti da je veza uspostavljena. Svi paketi odgovora, ili, drugim riječima, paketi potvrde, uvijek postavljaju vrijednost polja PacketNumber na jednu više od najveće vrijednosti PacketNumber uspješno primljenih paketa. Polje Opcije za prvi poslani paket je veličina poruke.
Sličan mehanizam se koristi za prekid veze. LastPacket zastavica se postavlja na zadnji paket poruke. U paketu odgovora naveden je broj posljednjeg paketa + 1, što za stranu primateljicu znači uspješnu isporuku poruke. Dijagram uspostavljanja i prekida veze:
Kada se veza uspostavi, počinje prijenos podataka. Podaci se prenose u blokovima paketa. Svaki blok, osim posljednjeg, sadrži fiksni broj paketa. Jednaka je veličini prozora za prijem/prenos. Posljednji blok podataka može imati manje paketa. Nakon slanja svakog bloka, strana pošiljateljica čeka potvrdu isporuke ili zahtjev za ponovnom isporukom izgubljenih paketa, ostavljajući prozor za prijem/prenos otvorenim za primanje odgovora. Nakon prijema potvrde isporuke bloka, prozor za prijem/prenos se pomiče i šalje se sljedeći blok podataka.
Strana koja prima pakete prima pakete. Svaki paket se provjerava da se vidi da li spada u okvir za prijenos. Paketi i duplikati koji ne padaju u prozor se filtriraju. Jer Ako je veličina prozora fiksna i ista za primaoca i pošiljaoca, tada se u slučaju da se blok paketa isporučuje bez gubitka, prozor pomiče na prijem paketa sljedećeg bloka podataka i dobija se potvrda isporuke poslano. Ako se prozor ne popuni u roku koji je zadao radni tajmer, tada će se pokrenuti provjera koji paketi nisu isporučeni i poslat će se zahtjevi za ponovnu dostavu. Dijagram retransmisije:
Vremenska ograničenja i tajmeri protokola
Postoji nekoliko razloga zašto se veza ne može uspostaviti. Na primjer, ako je primatelj van mreže. U tom slučaju, kada pokušavate uspostaviti vezu, veza će biti zatvorena po isteku vremena. Implementacija Pouzdan UDP koristi dva tajmera za postavljanje vremenskih ograničenja. Prvi, radni tajmer, koristi se za čekanje odgovora udaljenog hosta. Ako se aktivira na strani pošiljaoca, posljednji poslani paket se ponovo šalje. Ako tajmer istekne kod primaoca, tada se vrši provjera izgubljenih paketa i šalju se zahtjevi za ponovnu dostavu.
Drugi tajmer je potreban za zatvaranje veze u slučaju nedostatka komunikacije između čvorova. Za stranu pošiljaoca, počinje odmah nakon što radni tajmer istekne i čeka odgovor od udaljenog čvora. Ako nema odgovora u navedenom periodu, veza se prekida i resursi se oslobađaju. Za prijemnu stranu, tajmer zatvaranja veze se pokreće nakon što radni tajmer istekne dva puta. Ovo je neophodno radi osiguranja od gubitka potvrdnog paketa. Kada tajmer istekne, veza se također prekida i resursi se oslobađaju.
Pouzdan dijagram stanja prijenosa UDP-a
Principi protokola su implementirani u konačnom automatu, čije je svako stanje odgovorno za određenu logiku obrade paketa.
Pouzdan UDP dijagram stanja:
Zatvoreno - nije zapravo stanje, to je početna i krajnja tačka za automat. Za državu Zatvoreno prima se kontrolni blok prijenosa koji implementirajući asinhroni UDP server prosljeđuje pakete odgovarajućim vezama i započinje obradu stanja.
FirstPacketSending – početno stanje u kojem se nalazi odlazna veza kada je poruka poslana.
U ovom stanju se šalje prvi paket za normalne poruke. Za poruke bez potvrde slanja, ovo je jedino stanje u kojem se šalje cijela poruka.
SendingCycle – osnovno stanje za prijenos paketa poruka.
Prelazak na njega iz države FirstPacketSending izvršeno nakon što je prvi paket poruke poslan. U tom stanju dolaze sva priznanja i zahtjevi za retransmisije. Izlazak iz njega moguć je u dva slučaja - u slučaju uspješne dostave poruke ili po isteku vremena.
FirstPacketReceived – početno stanje za primaoca poruke.
Provjerava ispravnost početka prijenosa, kreira potrebne strukture i šalje potvrdu o prijemu prvog paketa.
Za poruku koja se sastoji od jednog paketa i poslana je bez korištenja dokaza o isporuci, ovo je jedino stanje. Nakon obrade takve poruke, veza se prekida.
Sklapanje – osnovno stanje za prijem paketa poruka.
Zapisuje pakete u privremenu memoriju, provjerava gubitak paketa, šalje potvrde za isporuku bloka paketa i cijele poruke i šalje zahtjeve za ponovnu isporuku izgubljenih paketa. U slučaju uspješnog prijema cijele poruke, veza prelazi u stanje Završeno, u suprotnom, istekne vremensko ograničenje.
Završeno – zatvaranje veze u slučaju uspješnog prijema cijele poruke.
Ovo stanje je neophodno za sklapanje poruke i za slučaj kada je potvrda o dostavi poruke izgubljena na putu do pošiljaoca. Iz ovog stanja se izlazi nakon isteka vremena, ali se veza smatra uspješno zatvorenom.
Dublje u kod. upravljačka jedinica mjenjača
Jedan od ključnih elemenata pouzdanog UDP-a je kontrolni blok prijenosa. Zadatak ovog bloka je pohranjivanje trenutnih veza i pomoćnih elemenata, distribucija dolaznih paketa odgovarajućim vezama, obezbjeđivanje interfejsa za slanje paketa na vezu i implementacija API protokola. Kontrolni blok prijenosa prima pakete od UDP sloja i prosljeđuje ih državnom stroju na obradu. Za primanje paketa, implementira asinhroni UDP server. Neki članovi klase ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// массив байт для указанного ключа. Используется для сборки входящих сообщений
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
// массив байт для указанного ключа. Используется для отправки исходящих сообщений.
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
// connection record для указанного ключа.
private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
// список подписчиков на сообщения.
private readonly List<ReliableUdpSubscribeObject> m_subscribers;
// локальный сокет
private Socket m_socketIn;
// порт для входящих сообщений
private int m_port;
// локальный IP адрес
private IPAddress m_ipAddress;
// локальная конечная точка
public IPEndPoint LocalEndpoint { get; private set; }
// коллекция предварительно инициализированных
// состояний конечного автомата
public StatesCollection States { get; private set; }
// генератор случайных чисел. Используется для создания TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementacija asinhronog UDP servera:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// создаем новый буфер, для каждого socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// передаем буфер в качестве параметра для асинхронного метода
this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}
private void EndReceive(IAsyncResult ar)
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
//пакет получен, готовы принимать следующий
Receive();
// т.к. простейший способ решить вопрос с буфером - получить ссылку на него
// из IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// получаем заголовок пакета
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// пришел некорректный пакет - отбрасываем его
return;
}
// конструируем ключ для определения connection record’а для пакета
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
// получаем существующую connection record или создаем новую
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// запускаем пакет в обработку в конечный автомат
record.State.ReceivePacket(record, header, bytes);
}
Za svaki prijenos poruke kreira se struktura koja sadrži informacije o vezi. Takva struktura se zove zapis veze. Neki članovi klase ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord : IDisposable
{
// массив байт с сообщением
public byte[] IncomingStream { get; set; }
// ссылка на состояние конечного автомата
public ReliableUdpState State { get; set; }
// пара, однозначно определяющая connection record
// в блоке управления передачей
public Tuple<EndPoint, Int32> Key { get; private set;}
// нижняя граница приемного окна
public int WindowLowerBound;
// размер окна передачи
public readonly int WindowSize;
// номер пакета для отправки
public int SndNext;
// количество пакетов для отправки
public int NumberOfPackets;
// номер передачи (именно он и есть вторая часть Tuple)
// для каждого сообщения свой
public readonly Int32 TransmissionId;
// удаленный IP endpoint – собственно получатель сообщения
public readonly IPEndPoint RemoteClient;
// размер пакета, во избежание фрагментации на IP уровне
// не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
public readonly int BufferSize;
// блок управления передачей
public readonly ReliableUdpConnectionControlBlock Tcb;
// инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
// не отправлять пакеты подтверждения
public bool IsNoAnswerNeeded;
// последний корректно полученный пакет (всегда устанавливается в наибольший номер)
public int RcvCurrent;
// массив с номерами потерянных пакетов
public int[] LostPackets { get; private set; }
// пришел ли последний пакет. Используется как bool.
public int IsLastPacketReceived = 0;
//...
}
Dublje u kod. države
Države implementiraju državnu mašinu pouzdanog UDP protokola, gdje se odvija glavna obrada paketa. Apstraktna klasa ReliableUdpState pruža interfejs za stanje:
Čitavu logiku protokola implementiraju gore predstavljene klase, zajedno sa pomoćnom klasom koja obezbjeđuje statičke metode, kao što je, na primjer, konstruiranje ReliableUdp zaglavlja iz zapisa veze.
Zatim ćemo detaljno razmotriti implementaciju metoda interfejsa koji određuju osnovne algoritme protokola.
DisposeByTimeout metoda
Metoda DisposeByTimeout je odgovorna za oslobađanje resursa veze nakon isteka vremena i za signaliziranje uspješne/neuspješne isporuke poruke. ReliableUdpState.DisposeByTimeout:
To je samo nadjačano u državi Završeno. Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// сообщаем об успешном получении сообщения
SetAsCompleted(connectionRecord);
}
ProcessPackets metoda
Metoda ProcessPackets je odgovorna za dodatnu obradu paketa ili paketa. Poziva se direktno ili preko tajmera čekanja paketa.
U stanju Sklapanje metoda je nadjačana i odgovorna je za provjeru izgubljenih paketa i prelazak u stanje Završeno, u slučaju prijema posljednjeg paketa i prolaska uspješne provjere 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);
}
}
U stanju SendingCycle ovaj metod se poziva samo na tajmeru i odgovoran je za ponovno slanje posljednje poruke, kao i za omogućavanje tajmera za zatvaranje veze. 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);
}
U stanju Završeno metoda zaustavlja radni tajmer i šalje poruku pretplatnicima. Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// собираем сообщение и передаем его подписчикам
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
ReceivePacket metoda
U stanju FirstPacketReceived glavni zadatak metode je da utvrdi da li je prvi paket poruke zaista stigao na interfejs, kao i da prikupi poruku koja se sastoji od jednog paketa. FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// отбрасываем пакет
return;
// комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// отправляем пакет подтверждение
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// by design все packet numbers начинаются с 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// считаем кол-во пакетов, которые должны прийти
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// записываем номер последнего полученного пакета (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// после сдвинули окно приема на 1
connectionRecord.WindowLowerBound++;
// переключаем состояние
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// если не требуется механизм подтверждение
// запускаем таймер который высвободит все структуры
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
else
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
U stanju SendingCycle ovaj metod je poništen za prihvatanje potvrda isporuke i zahtjeva za ponovnim prijenosom. SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// расчет конечной границы окна
// берется граница окна + 1, для получения подтверждений доставки
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// проверка на попадание в окно
if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// проверить на последний пакет:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// передача завершена
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// это ответ на первый пакет c подтверждением
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// без сдвига окна
SendPacket(connectionRecord);
}
// пришло подтверждение о получении блока данных
else if (header.PacketNumber == windowHighestBound)
{
// сдвигаем окно прием/передачи
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// обнуляем массив контроля передачи
connectionRecord.WindowControlArray.Nullify();
// отправляем блок пакетов
SendPacket(connectionRecord);
}
// это запрос на повторную передачу – отправляем требуемый пакет
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
U stanju Sklapanje u metodi ReceivePacket odvija se glavni posao sastavljanja poruke od dolaznih paketa. Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// обработка пакетов с отключенным механизмом подтверждения доставки
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// сбрасываем таймер
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// записываем данные
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// если получили пакет с последним флагом - делаем завершаем
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// расчет конечной границы окна
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// отбрасываем не попадающие в окно пакеты
if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
return;
// отбрасываем дубликаты
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// записываем данные
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// увеличиваем счетчик пакетов
connectionRecord.PacketCounter++;
// записываем в массив управления окном текущий номер пакета
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// устанавливаем наибольший пришедший пакет
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// перезапускам таймеры
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// если пришел последний пакет
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// если нам пришли все пакеты окна, то сбрасываем счетчик
// и высылаем пакет подтверждение
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// сбрасываем счетчик.
connectionRecord.PacketCounter = 0;
// сдвинули окно передачи
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// обнуление массива управления передачей
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// если последний пакет уже имеется
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// проверяем пакеты
ProcessPackets(connectionRecord);
}
}
U stanju Završeno jedini zadatak metode je da pošalje ponovnu potvrdu uspješne isporuke poruke. Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// повторная отправка последнего пакета в связи с тем,
// что последний ack не дошел до отправителя
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Metoda slanja paketa
U stanju FirstPacketSending ovaj metod šalje prvi paket podataka, ili, ako poruka ne zahtijeva potvrdu isporuke, cijelu poruku. 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);
}
U stanju SendingCycle u ovoj metodi se šalje blok paketa. 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 );
}
}
Dublje u kod. Stvaranje i uspostavljanje veza
Sada kada smo vidjeli osnovna stanja i metode koje se koriste za rukovanje stanjima, razložimo nekoliko primjera kako protokol radi malo detaljnije. Dijagram prijenosa podataka u normalnim uvjetima:
Razmotrite detaljno stvaranje zapis veze za povezivanje i slanje prvog paketa. Prijenos uvijek inicira aplikacija koja poziva API za slanje poruke. Zatim se poziva metoda StartTransmission kontrolnog bloka prijenosa, koja započinje prijenos podataka za novu poruku. Kreiranje odlazne veze:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
}
// запускаем обработку входящих пакетов
StartListener(LocalEndpoint);
}
// создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// создаем случайный номер transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
// создаем новую запись для соединения и проверяем,
// существует ли уже такой номер в наших словарях
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// если существует – то повторно генерируем случайный номер
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// если снова не удалось – генерируем исключение
throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
}
// запустили состояние в обработку
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Slanje prvog paketa (stanje 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);
}
Nakon slanja prvog paketa, pošiljalac ulazi u stanje SendingCycle – sačekajte potvrdu isporuke paketa.
Strana koja prima, koristeći metodu EndReceive, prima poslani paket, kreira novi zapis veze i prosljeđuje ovaj paket, s unaprijed raščlanjenim zaglavljem, metodi ReceivePacket stanja na obradu FirstPacketReceived Kreiranje veze na prijemnoj strani:
private void EndReceive(IAsyncResult ar)
{
// ...
// пакет получен
// парсим заголовок пакета
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// пришел некорректный пакет - отбрасываем его
return;
}
// конструируем ключ для определения connection record’а для пакета
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
// получаем существующую connection record или создаем новую
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
// запускаем пакет в обработку в конечный автомат
record.State.ReceivePacket(record, header, bytes);
}
Primanje prvog paketa i slanje potvrde (FirstPacketReceived stanje):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// отбрасываем пакет
return;
// ...
// by design все packet numbers начинаются с 0;
if (header.PacketNumber != 0)
return;
// инициализируем массив для хранения частей сообщения
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// записываем данные пакет в массив
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// считаем кол-во пакетов, которые должны прийти
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// записываем номер последнего полученного пакета (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// после сдвинули окно приема на 1
connectionRecord.WindowLowerBound++;
// переключаем состояние
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*если не требуется механизм подтверждение*/)
// ...
else
{
// отправляем подтверждение
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Dublje u kod. Zatvaranje veze nakon isteka
Rukovanje timeout-om je važan dio pouzdanog UDP-a. Razmotrimo primjer u kojem je posredni čvor pokvario i isporuka podataka u oba smjera postala je nemoguća. Dijagram za zatvaranje veze po vremenskom ograničenju:
Kao što se vidi iz dijagrama, pošiljateljev radni tajmer počinje odmah nakon slanja bloka paketa. Ovo se dešava u metodi SendPacket stanja SendingCycle. Omogućavanje radnog tajmera (SendingCycle stanje):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// отправляем блок пакетов
// ...
// перезапускаем таймер после отправки
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
Vremenski periodi se postavljaju kada se veza uspostavi. Zadani ShortTimerPeriod je 5 sekundi. U primjeru je postavljeno na 1,5 sekunde.
Za dolaznu vezu, tajmer počinje nakon prijema posljednjeg dolaznog paketa podataka, to se događa u metodi ReceivePacket stanja Sklapanje Omogućavanje radnog tajmera (stanje sklapanja):
Više paketa nije stizalo na dolaznu vezu dok se čeka radni tajmer. Tajmer se isključio i pozvao metodu ProcessPackets, gdje su izgubljeni paketi pronađeni i zahtjevi za ponovnu isporuku poslani po prvi put. Slanje zahtjeva za ponovnu isporuku (stanje sklapanja):
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);
}
}
Varijabla TimerSecondTry je postavljena na istinski. Ova varijabla je odgovorna za ponovno pokretanje radnog tajmera.
Na strani pošiljaoca se takođe aktivira radni tajmer i ponovo se šalje poslednji poslani paket. Omogućavanje tajmera zatvaranja veze (SendingCycle stanje):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// отправляем повторно последний пакет
// ...
// включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
StartCloseWaitTimer(connectionRecord);
}
Nakon toga, tajmer zatvaranja veze počinje u odlaznoj vezi. 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);
}
Vremensko ograničenje tajmera za zatvaranje veze je podrazumevano 30 sekundi.
Nakon kratkog vremena ponovo se aktivira radni tajmer na strani primaoca, ponovo se šalju zahtjevi, nakon čega počinje tajmer zatvaranja veze za dolaznu vezu
Kada se aktiviraju tajmeri zatvaranja, svi resursi oba zapisa veze se oslobađaju. Pošiljalac prijavljuje neuspjeh isporuke uzvodnoj aplikaciji (pogledajte Pouzdan UDP API). Otpuštanje resursa zapisa veze:
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);
}
}
Dublje u kod. Vraćanje prijenosa podataka
Dijagram oporavka prijenosa podataka u slučaju gubitka paketa:
Kao što je već rečeno u zatvaranju veze na vremenskom ograničenju, kada radni tajmer istekne, primalac će proveriti da li postoje izgubljeni paketi. U slučaju gubitka paketa, sastavlja se lista broja paketa koji nisu stigli do primaoca. Ovi brojevi se unose u niz LostPackets određene veze i šalju se zahtjevi za ponovnu isporuku. Slanje zahtjeva za ponovnu isporuku paketa (stanje sklapanja):
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);
}
}
// ...
}
}
Pošiljalac će prihvatiti zahtjev za ponovnu dostavu i poslati pakete koji nedostaju. Vrijedi napomenuti da je u ovom trenutku pošiljatelj već pokrenuo tajmer zatvaranja veze i, kada se primi zahtjev, on se resetuje. Ponovno slanje izgubljenih paketa (SendingCycle stanje):
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));
}
Ponovo poslani paket (paket #3 na dijagramu) prima dolazna veza. Provjerava se da li je prozor za prijem pun i da li se normalan prijenos podataka vraća. Provjera pogodaka u prozoru za prijem (stanje sklapanja):
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);
}
// ...
}
Pouzdan UDP API
Za interakciju s protokolom prijenosa podataka, postoji otvorena klasa Pouzdani Udp, koja je omotač preko kontrolnog bloka prijenosa. Evo najvažnijih članova klase:
public sealed class ReliableUdp : IDisposable
{
// получает локальную конечную точку
public IPEndPoint LocalEndpoint
// создает экземпляр ReliableUdp и запускает
// прослушивание входящих пакетов на указанном IP адресе
// и порту. Значение 0 для порта означает использование
// динамически выделенного порта
public ReliableUdp(IPAddress localAddress, int port = 0)
// подписка на получение входящих сообщений
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// отписка от получения сообщений
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// асинхронно отправить сообщение
// Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// начать асинхронную отправку сообщения
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// получить результат асинхронной отправки
public bool EndSendMessage(IAsyncResult asyncResult)
// очистить ресурсы
public void Dispose()
}
Poruke se primaju putem pretplate. Potpis delegata za metodu povratnog poziva:
public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );
Poruka:
public class ReliableUdpMessage
{
// тип сообщения, простое перечисление
public ReliableUdpMessageTypes Type { get; private set; }
// данные сообщения
public byte[] Body { get; private set; }
// если установлено в true – механизм подтверждения доставки будет отключен
// для передачи конкретного сообщения
public bool NoAsk { get; private set; }
}
Za pretplatu na određeni tip poruke i/ili određenog pošiljatelja, koriste se dva opciona parametra: ReliableUdpMessageTypes messageType i IPEndPoint ipEndPoint.
Vrste poruka:
public enum ReliableUdpMessageTypes : short
{
// Любое
Any = 0,
// Запрос к STUN server
StunRequest = 1,
// Ответ от STUN server
StunResponse = 2,
// Передача файла
FileTransfer =3,
// ...
}
Poruka se šalje asinhrono; za to protokol implementira model asinhronog programiranja:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Rezultat slanja poruke bit će istinit - ako je poruka uspješno stigla do primaoca i lažan - ako je veza prekinuta do isteka vremena:
public bool EndSendMessage(IAsyncResult asyncResult)
zaključak
Mnogo toga nije opisano u ovom članku. Mehanizmi uparivanja niti, rukovanje izuzetcima i greškama, implementacija asinhronih metoda slanja poruka. Ali srž protokola, opis logike za obradu paketa, uspostavljanje veze i rukovanje vremenskim ograničenjima, trebao bi vam biti jasan.
Demonstrirana verzija pouzdanog protokola isporuke je dovoljno robusna i fleksibilna da ispuni prethodno definirane zahtjeve. Ali želim da dodam da se opisana implementacija može poboljšati. Na primjer, da bi se povećala propusnost i dinamički mijenjali periodi tajmera, mehanizmi kao što su klizni prozor i RTT mogu se dodati u protokol, također će biti korisno implementirati mehanizam za određivanje MTU između čvorova veze (ali samo ako se šalju velike poruke) .
Hvala vam na pažnji, radujem se vašim komentarima i komentarima.
PS Za one koje zanimaju detalji ili samo žele da testiraju protokol, link ka projektu na GitHubeu: Pouzdan UDP projekat
Ažuriranje: Hvala mayorovp и sidristij za ideju dodavanja zadatka u interfejs. Kompatibilnost biblioteke sa starim operativnim sistemima nije narušena, jer Četvrti okvir podržava i XP i 4 server.