Internetin alkuperäinen arkkitehtuuri olettaa homogeenista osoiteavaruutta, jossa jokaisella solmulla oli globaali ja ainutlaatuinen IP-osoite ja joka pystyi kommunikoimaan suoraan muiden solmujen kanssa. Nyt Internetillä on itse asiassa erilainen arkkitehtuuri - yksi globaalien IP-osoitteiden alue ja monia alueita, joissa yksityiset osoitteet on piilotettu NAT-laitteiden taakse.Tässä arkkitehtuurissa vain globaalissa osoiteavaruudessa olevat laitteet voivat helposti kommunikoida kenen tahansa verkossa, koska niillä on ainutlaatuinen, maailmanlaajuisesti reititettävissä oleva IP-osoite. Yksityisen verkon solmu voi muodostaa yhteyden muihin saman verkon solmuihin ja myös muihin maailmanlaajuisen osoiteavaruuden tunnettuihin solmuihin. Tämä vuorovaikutus saavutetaan suurelta osin verkko-osoitteen muunnosmekanismin ansiosta. NAT-laitteet, kuten Wi-Fi-reitittimet, luovat erityisiä käännöstaulukkomerkintöjä lähteville yhteyksille ja muokkaavat IP-osoitteita ja porttinumeroita paketeissa. Tämä mahdollistaa lähtevät yhteydet yksityisestä verkosta globaalissa osoiteavaruudessa oleviin isänteihin. Mutta samaan aikaan NAT-laitteet yleensä estävät kaiken saapuvan liikenteen, ellei saapuville yhteyksille ole asetettu erillisiä sääntöjä.
Tämä Internetin arkkitehtuuri on riittävän oikea asiakas-palvelin-viestintään, jossa asiakkaat voivat olla yksityisissä verkoissa ja palvelimilla on globaali osoite. Mutta se vaikeuttaa kahden solmun suoraa yhteyttä toisiinsa eri yksityiset verkot. Suora yhteys kahden solmun välillä on tärkeä peer-to-peer-sovelluksissa, kuten puheensiirrossa (Skype), etäkäytössä tietokoneessa (TeamViewer) tai online-pelaamisessa.
Yksi tehokkaimmista menetelmistä peer-to-peer-yhteyden muodostamiseksi eri yksityisten verkkojen laitteiden välille on nimeltään rei'itys. Tätä tekniikkaa käytetään yleisimmin UDP-protokollaan perustuvissa sovelluksissa.
Mutta jos sovelluksesi tarvitsee taatun tiedon toimituksen, esimerkiksi siirrät tiedostoja tietokoneiden välillä, UDP:n käytössä on monia vaikeuksia, koska UDP ei ole taattu toimitusprotokolla eikä tarjoa pakettien toimitusta järjestyksessä, toisin kuin TCP. protokollaa.
Tässä tapauksessa taatun pakettien toimituksen varmistamiseksi tarvitaan sovelluskerroksen protokolla, joka tarjoaa tarvittavat toiminnot ja toimii UDP:n yli.
Haluan heti huomauttaa, että on olemassa TCP-rei'itystekniikka TCP-yhteyksien muodostamiseen solmujen välille eri yksityisissä verkoissa, mutta koska monet NAT-laitteet eivät tue sitä, sitä ei yleensä pidetä pääasiallisena yhteyden muodostamisena. sellaiset solmut.
Tämän artikkelin loppuosassa keskityn vain taatun toimitusprotokollan toteuttamiseen. UDP-rei'itystekniikan toteutus kuvataan seuraavissa artikkeleissa.
Protokollavaatimukset
Luotettava pakettien toimitus positiivisen palautemekanismin kautta (ns. positiivinen kuittaus)
Suuren datan tehokkaan siirron tarve, ts. protokollan tulee välttää tarpeetonta pakettien välitystä
Toimituksen vahvistusmekanismin (kyky toimia "puhtaan" UDP-protokollana) pitäisi olla mahdollista peruuttaa
Kyky toteuttaa komentotila jokaisen viestin vahvistuksella
Protokollan yli tapahtuvan tiedonsiirron perusyksikön tulee olla viesti
Nämä vaatimukset ovat suurelta osin yhtenevät kohdassa kuvattujen Reliable Data Protocol -vaatimusten kanssa RFK 908 и RFK 1151, ja luotin näihin standardeihin tätä protokollaa kehitettäessä.
Näiden vaatimusten ymmärtämiseksi tarkastellaan tiedonsiirron ajoitusta kahden TCP- ja UDP-protokollia käyttävän verkkosolmun välillä. Olkoon molemmissa tapauksissa yksi paketti kadonnut. Ei-interaktiivisten tietojen siirto TCP:n kautta:
Kuten kaaviosta näkyy, pakettien katoamisen tapauksessa TCP havaitsee kadonneen paketin ja raportoi sen lähettäjälle pyytämällä kadonneen segmentin numeroa. Tiedonsiirto UDP-protokollan kautta:
UDP ei suorita mitään häviön havaitsemisvaiheita. UDP-protokollan lähetysvirheiden hallinta on täysin sovelluksen vastuulla.
Virheiden havaitseminen TCP-protokollassa saavutetaan muodostamalla yhteys päätesolmuun, tallentamalla kyseisen yhteyden tila, osoittamalla kussakin paketin otsikossa lähetettyjen tavujen lukumäärä ja ilmoittamalla kuittauksista kuittausnumerolla.
Lisäksi suorituskyvyn parantamiseksi (eli useamman kuin yhden segmentin lähettämiseksi vastaanottamatta kuittausta) TCP-protokolla käyttää niin kutsuttua lähetysikkunaa - datatavumäärää, jonka segmentin lähettäjä odottaa vastaanottavansa.
Lisätietoja TCP-protokollasta on kohdassa RFK 793, UDP:stä kohteeseen RFK 768missä ne itse asiassa määritellään.
Edellä olevan perusteella on selvää, että luotettavan viestinvälitysprotokollan luomiseksi UDP:n yli (jäljempänä ns. Luotettava UDP), sen on toteutettava TCP:n kaltaisia tiedonsiirtomekanismeja. Nimittäin:
ilmoittaa viestin alkamisesta resurssien varaamiseksi yhteydelle
viestiä viestin lopusta, välittääkseen vastaanotetun viestin ylävirran sovellukselle ja vapauttaakseen protokollaresursseja
salli yhteyskohtaisen protokollan estää toimituksen vahvistusmekanismin toimimasta "puhtaan" UDP:nä
Luotettava UDP-otsikko
Muista, että UDP-datagrammi on kapseloitu IP-datagrammiin. Luotettava UDP-paketti "kääritään" asianmukaisesti UDP-datagrammiin. Luotettava UDP-otsikon kapselointi:
Luotettavan UDP-otsikon rakenne on melko yksinkertainen:
Liput - paketin valvontaliput
MessageType - viestityyppi, jota ylävirran sovellukset käyttävät tiettyjen viestien tilaamiseen
TransmissionId - lähetyksen numero yhdessä vastaanottajan osoitteen ja portin kanssa, joka yksilöi yhteyden
PacketNumber - paketin numero
Options - lisäprotokollaasetukset. Ensimmäisen paketin tapauksessa sitä käytetään ilmaisemaan viestin koko
Liput ovat seuraavat:
FirstPacket - viestin ensimmäinen paketti
NoAsk - viesti ei vaadi kuittausmekanismin ottamista käyttöön
LastPacket - viestin viimeinen paketti
RequestForPacket - vahvistuspaketti tai pyyntö kadonneesta paketista
Protokollan yleiset periaatteet
Koska Reliable UDP keskittyy taattuun sanomanvälitykseen kahden solmun välillä, sen on kyettävä muodostamaan yhteys toisen puolen kanssa. Yhteyden muodostamiseksi lähettäjä lähettää paketin FirstPacket-lipulla, johon vastaus tarkoittaa, että yhteys on muodostettu. Kaikki vastauspaketit tai toisin sanoen kuittauspaketit asettavat PacketNumber-kentän arvoksi aina yhden suuremman kuin onnistuneesti vastaanotettujen pakettien suurimman PacketNumber-arvon. Ensimmäisen lähetetyn paketin Asetukset-kenttä on viestin koko.
Samanlaista mekanismia käytetään yhteyden katkaisemiseen. LastPacket-lippu asetetaan viestin viimeiseen pakettiin. Vastauspaketissa ilmoitetaan viimeisen paketin numero + 1, mikä tarkoittaa vastaanottavalle puolelle viestin onnistunutta toimittamista. Kytkentä- ja päätekaavio:
Kun yhteys on muodostettu, tiedonsiirto alkaa. Tiedot siirretään pakettilohkoina. Jokainen lohko, paitsi viimeinen, sisältää kiinteän määrän paketteja. Se on yhtä suuri kuin vastaanotto-/lähetysikkunan koko. Viimeisessä tietolohkossa voi olla vähemmän paketteja. Kunkin lohkon lähettämisen jälkeen lähettävä puoli odottaa toimitusvahvistusta tai pyyntöä toimittaa kadonneet paketit uudelleen jättäen vastaanotto-/lähetysikkunan auki vastaanottamaan vastauksia. Saatuaan vahvistuksen lohkon toimituksesta vastaanotto-/lähetysikkuna siirtyy ja seuraava datalohko lähetetään.
Vastaanottava puoli vastaanottaa paketit. Jokainen paketti tarkistetaan sen selvittämiseksi, kuuluuko se lähetysikkunaan. Paketit ja kaksoiskappaleet, jotka eivät putoa ikkunaan, suodatetaan pois. Koska Jos ikkunan koko on kiinteä ja sama vastaanottajalle ja lähettäjälle, niin jos pakettilohko toimitetaan ilman häviötä, ikkuna siirtyy vastaanottamaan seuraavan tietolohkon paketteja ja lähetetään toimitusvahvistus. lähetetty. Jos ikkuna ei täyty työajastimen asettamassa ajassa, käynnistetään tarkastus siitä, mitä paketteja ei ole toimitettu ja lähetetään uudelleentoimituspyyntöjä. Uudelleenlähetyskaavio:
Aikakatkaisut ja protokollaajastimet
On useita syitä, miksi yhteyttä ei voida muodostaa. Esimerkiksi jos vastaanottava osapuoli on offline-tilassa. Tässä tapauksessa yhteys suljetaan aikakatkaisulla, kun yrität muodostaa yhteyttä. Luotettava UDP-toteutus käyttää kahta ajastinta aikakatkaisujen asettamiseen. Ensimmäistä, työajastinta, käytetään odottamaan vastausta etäisännältä. Jos se laukeaa lähettäjän puolella, viimeksi lähetetty paketti lähetetään uudelleen. Jos ajastin vanhenee vastaanottajan kohdalla, suoritetaan kadonneiden pakettien tarkistus ja lähetetään uudelleentoimituspyynnöt.
Toinen ajastin tarvitaan sulkemaan yhteys, jos solmujen välinen kommunikaatio puuttuu. Lähettäjäpuolella se käynnistyy välittömästi työajastimen umpeutumisen jälkeen ja odottaa vastausta etäsolmulta. Jos vastausta ei saada määritetyn ajan kuluessa, yhteys katkaistaan ja resurssit vapautetaan. Vastaanottavan puolen yhteyden sulkemisajastin käynnistetään sen jälkeen, kun työajastin vanhenee kahdesti. Tämä on tarpeen vahvistuspaketin katoamisen varalta. Kun ajastin umpeutuu, myös yhteys katkeaa ja resurssit vapautuvat.
Luotettava UDP-lähetyksen tilakaavio
Protokollan periaatteet on toteutettu äärellistilakoneessa, jonka jokainen tila vastaa tietystä paketinkäsittelylogiikasta.
Luotettava UDP-tilakaavio:
Suljettu - ei todellakaan ole tila, se on automaatin alku- ja loppupiste. Valtion puolesta Suljettu vastaanotetaan lähetyksen ohjauslohko, joka asynkronisen UDP-palvelimen toteuttaen välittää paketit asianmukaisille yhteyksille ja aloittaa tilankäsittelyn.
FirstPacketSending – lähtötila, jossa lähtevä yhteys on, kun viesti lähetetään.
Tässä tilassa lähetetään ensimmäinen paketti normaaleille viesteille. Viesteissä ilman lähetysvahvistusta tämä on ainoa tila, jossa koko viesti lähetetään.
Lähetyssykli – perustila viestipakettien lähettämistä varten.
Siirtyminen siihen osavaltiosta FirstPacketSending suoritetaan sen jälkeen, kun ensimmäinen viestin paketti on lähetetty. Tässä tilassa kaikki kuittaukset ja uudelleenlähetyspyynnöt tulevat. Siitä poistuminen on mahdollista kahdessa tapauksessa - viestin onnistuneen toimituksen yhteydessä tai aikakatkaisulla.
FirstPacketReceived – viestin vastaanottajan alkutila.
Se tarkistaa lähetyksen alun oikeellisuuden, luo tarvittavat rakenteet ja lähettää kuittauksen ensimmäisen paketin vastaanottamisesta.
Viestille, joka koostuu yhdestä paketista ja lähetettiin ilman toimitustodistusta, tämä on ainoa tila. Kun tällainen viesti on käsitelty, yhteys suljetaan.
Se kirjoittaa paketteja väliaikaiseen tallennustilaan, tarkistaa pakettien katoamisen, lähettää kuittaukset pakettilohkon ja koko viestin toimittamisesta sekä lähettää pyyntöjä kadonneiden pakettien uudelleentoimittamisesta. Jos koko viesti vastaanotetaan onnistuneesti, yhteys siirtyy tilaan Valmistunut, muuten aikakatkaisu päättyy.
Valmistunut – yhteyden sulkeminen, jos koko viesti on vastaanotettu.
Tämä tila on välttämätön viestin kokoamiseksi ja siinä tapauksessa, että viestin toimitusvahvistus katosi matkalla lähettäjälle. Tästä tilasta poistuu aikakatkaisu, mutta yhteys katsotaan onnistuneesti suljetuksi.
Syvemmälle koodiin. voimansiirron ohjausyksikkö
Yksi luotettavan UDP:n avainelementeistä on lähetyksen ohjauslohko. Tämän lohkon tehtävänä on tallentaa nykyiset yhteydet ja apuelementit, jakaa saapuvat paketit vastaaville yhteyksille, tarjota rajapinta pakettien lähettämiseksi yhteyteen sekä toteuttaa protokolla-API. Lähetyksen ohjauslohko vastaanottaa paketit UDP-kerrokselta ja välittää ne edelleen tilakoneelle käsittelyä varten. Pakettien vastaanottamiseksi se toteuttaa asynkronisen UDP-palvelimen. Jotkut ReliableUdpConnectionControlBlock-luokan jäsenet:
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;
//...
}
Asynkronisen UDP-palvelimen käyttöönotto:
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);
}
Jokaista viestin siirtoa varten luodaan rakenne, joka sisältää tiedot yhteydestä. Tällaista rakennetta kutsutaan yhteystietue. Jotkut ReliableUdpConnectionRecord-luokan jäsenet:
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;
//...
}
Syvemmälle koodiin. valtioita
Osavaltiot toteuttavat Reliable UDP -protokollan tilakoneen, jossa pakettien pääkäsittely tapahtuu. Abstrakti luokka ReliableUdpState tarjoaa käyttöliittymän tilalle:
Protokollan koko logiikka on toteutettu yllä esitetyillä luokilla yhdessä apuluokan kanssa, joka tarjoaa staattisia menetelmiä, kuten esimerkiksi ReliableUdp-otsikon muodostamisen yhteystietueesta.
Seuraavaksi tarkastelemme yksityiskohtaisesti protokollan perusalgoritmit määrittävien liitäntämenetelmien toteutusta.
DisposeByTimeout -menetelmä
DisposeByTimeout-menetelmä vastaa yhteysresurssien vapauttamisesta aikakatkaisun jälkeen ja viestien onnistumisesta/epäonnistumisesta. ReliableUdpState.DisposeByTimeout:
Se on ohitettu vain osavaltiossa Valmistunut. Valmis.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// сообщаем об успешном получении сообщения
SetAsCompleted(connectionRecord);
}
ProcessPackets-menetelmä
ProcessPackets-menetelmä vastaa paketin tai pakettien lisäkäsittelystä. Soitetaan suoraan tai paketin odotusajastimen kautta.
Kunnossa kokoaminen menetelmä on ohitettu ja vastaa kadonneiden pakettien tarkistamisesta ja siirtymisestä tilaan Valmistunut, jos vastaanotat viimeisen paketin ja läpäisit onnistuneen tarkistuksen Kokoaminen. Prosessipaketit:
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);
}
}
Kunnossa Lähetyssykli tätä menetelmää kutsutaan vain ajastimella, ja se vastaa viimeisen viestin uudelleen lähettämisestä sekä yhteyden sulkemisajastimen käyttöönotosta. 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);
}
Kunnossa Valmistunut menetelmä pysäyttää käynnissä olevan ajastimen ja lähettää viestin tilaajille. Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// собираем сообщение и передаем его подписчикам
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
ReceivePacket Method
Kunnossa FirstPacketReceived menetelmän päätehtävänä on selvittää, onko ensimmäinen viestipaketti todella saapunut rajapinnalle, ja myös kerätä yhdestä paketista koostuva viesti. 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);
}
}
Kunnossa Lähetyssykli tämä menetelmä ohitetaan toimituskuittausten ja uudelleenlähetyspyyntöjen hyväksymiseksi. 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));
}
Kunnossa kokoaminen ReceivePacket-menetelmässä pääasiallinen työ viestin kokoamisessa saapuvista paketeista tapahtuu. 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);
}
}
Kunnossa Valmistunut menetelmän ainoa tehtävä on lähettää kuittaus viestin onnistuneesta toimituksesta. Valmis.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// повторная отправка последнего пакета в связи с тем,
// что последний ack не дошел до отправителя
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Lähetä paketti -menetelmä
Kunnossa FirstPacketSending tämä menetelmä lähettää ensimmäisen tietopaketin tai jos viesti ei vaadi toimitusvahvistusta, koko viestin. 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);
}
Kunnossa Lähetyssykli tässä menetelmässä pakettilohko lähetetään. 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 );
}
}
Syvemmälle koodiin. Yhteyksien luominen ja luominen
Nyt kun olemme nähneet perustilat ja tilojen käsittelyyn käytetyt menetelmät, puretaan muutama esimerkki protokollan toiminnasta hieman yksityiskohtaisemmin. Tiedonsiirtokaavio normaaleissa olosuhteissa:
Harkitse luomista yksityiskohtaisesti yhteystietue muodostaa yhteys ja lähettää ensimmäinen paketti. Siirron aloittaa aina sovellus, joka kutsuu lähetysviestin API:n. Seuraavaksi kutsutaan lähetyksen ohjauslohkon StartTransmission-menetelmää, joka aloittaa tiedonsiirron uutta viestiä varten. Lähtevän yhteyden luominen:
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]);
}
Ensimmäisen paketin lähettäminen (FirstPacketSending-tila):
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);
}
Ensimmäisen paketin lähettämisen jälkeen lähettäjä siirtyy tilaan Lähetyssykli – odota vahvistusta paketin toimituksesta.
Vastaanottava puoli EndReceive-menetelmää käyttäen vastaanottaa lähetetyn paketin, luo uuden yhteystietue ja välittää tämän paketin valmiiksi jäsennellyllä otsikolla tilan ReceivePacket-menetelmälle käsittelyä varten FirstPacketReceived Yhteyden luominen vastaanottavalla puolella:
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);
}
Ensimmäisen paketin vastaanottaminen ja kuittauksen lähettäminen (FirstPacketReceived-tila):
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);
}
}
Syvemmälle koodiin. Yhteyden sulkeminen aikakatkaisun jälkeen
Aikakatkaisukäsittely on tärkeä osa luotettavaa UDP:tä. Harkitse esimerkkiä, jossa välisolmu epäonnistui ja tietojen toimittaminen molempiin suuntiin tuli mahdottomaksi. Kaavio yhteyden sulkemisesta aikakatkaisulla:
Kuten kaaviosta näkyy, lähettäjän työajastin käynnistyy heti pakettilohkon lähettämisen jälkeen. Tämä tapahtuu tilan SendPacket-menetelmässä Lähetyssykli. Työajastimen käyttöönotto (SendingCycle-tila):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// отправляем блок пакетов
// ...
// перезапускаем таймер после отправки
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
Ajastimen jaksot asetetaan, kun yhteys muodostetaan. Oletusarvoinen ShortTimerPeriod on 5 sekuntia. Esimerkissä se on asetettu 1,5 sekuntiin.
Saapuvan yhteyden ajastin käynnistyy viimeisen saapuvan datapaketin vastaanottamisen jälkeen, tämä tapahtuu tilan ReceivePacket-metodissa kokoaminen Työajastimen käyttöönotto (kokoamistila):
Saapuvalle yhteydelle ei saapunut enempää paketteja odotellessa työajastinta. Ajastin sammui ja kutsui ProcessPackets-menetelmän, jossa kadonneet paketit löydettiin ja uudelleentoimituspyyntöjä lähetettiin ensimmäistä kertaa. Uudelleentoimituspyyntöjen lähettäminen (kokoamistila):
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-muuttuja on asetettu arvoon totta. Tämä muuttuja on vastuussa työajastimen uudelleenkäynnistämisestä.
Lähettäjän puolella myös työajastin laukeaa ja viimeksi lähetetty paketti lähetetään uudelleen. Yhteyden sulkemisajastimen ottaminen käyttöön (SendingCycle-tila):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// отправляем повторно последний пакет
// ...
// включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
StartCloseWaitTimer(connectionRecord);
}
Tämän jälkeen yhteyden sulkemisajastin käynnistyy lähtevässä yhteydessä. 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);
}
Yhteyden sulkemisajastimen aikakatkaisuaika on oletusarvoisesti 30 sekuntia.
Hetken kuluttua vastaanottajan puolella oleva työajastin laukeaa uudelleen, pyyntöjä lähetetään uudelleen, minkä jälkeen yhteyden sulkemisajastin käynnistyy tulevalle yhteydelle
Kun sulkemisajastimet käynnistyvät, molempien yhteystietueiden kaikki resurssit vapautetaan. Lähettäjä raportoi toimitushäiriöstä ylävirran sovellukselle (Katso Luotettava UDP API). Yhteystietueresurssien vapauttaminen:
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);
}
}
Kuten jo mainittiin yhteyden sulkemisessa aikakatkaisun yhteydessä, kun työajastin umpeutuu, vastaanotin tarkistaa kadonneiden pakettien varalta. Pakettien katoamisen sattuessa laaditaan luettelo pakettien määrästä, jotka eivät saapuneet vastaanottajalle. Nämä numerot syötetään tietyn yhteyden LostPackets-taulukkoon ja lähetetään uudelleentoimituspyynnöt. Pyyntöjen lähettäminen pakettien uudelleentoimittamiseen (kokoamistila):
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);
}
}
// ...
}
}
Lähettäjä hyväksyy uudelleentoimituspyynnön ja lähettää puuttuvat paketit. On syytä huomata, että tällä hetkellä lähettäjä on jo käynnistänyt yhteyden sulkemisajastimen ja kun pyyntö vastaanotetaan, se nollataan. Kadonneiden pakettien uudelleenlähetys (SendingCycle-tila):
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));
}
Uudelleenlähetetty paketti (paketti #3 kaaviossa) vastaanotetaan saapuvan yhteyden kautta. Tarkistetaan, onko vastaanottoikkuna täynnä ja palautuuko normaali tiedonsiirto. Osumien tarkistaminen vastaanottoikkunassa (kokoamistila):
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);
}
// ...
}
Luotettava UDP API
Tiedonsiirtoprotokollan kanssa vuorovaikutusta varten on olemassa avoin Reliable Udp -luokka, joka on siirron ohjauslohkon kääre. Tässä luokan tärkeimmät jäsenet:
public sealed class ReliableUdp : IDisposable
{
// получает локальную конечную точку
public IPEndPoint LocalEndpoint
// создает экземпляр ReliableUdp и запускает
// прослушивание входящих пакетов на указанном IP адресе
// и порту. Значение 0 для порта означает использование
// динамически выделенного порта
public ReliableUdp(IPAddress localAddress, int port = 0)
// подписка на получение входящих сообщений
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// отписка от получения сообщений
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// асинхронно отправить сообщение
// Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// начать асинхронную отправку сообщения
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// получить результат асинхронной отправки
public bool EndSendMessage(IAsyncResult asyncResult)
// очистить ресурсы
public void Dispose()
}
public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );
Viesti:
public class ReliableUdpMessage
{
// тип сообщения, простое перечисление
public ReliableUdpMessageTypes Type { get; private set; }
// данные сообщения
public byte[] Body { get; private set; }
// если установлено в true – механизм подтверждения доставки будет отключен
// для передачи конкретного сообщения
public bool NoAsk { get; private set; }
}
Tietyn viestityypin ja/tai tietyn lähettäjän tilaamiseen käytetään kahta valinnaista parametria: ReliableUdpMessageTypes messageType ja IPEndPoint ipEndPoint.
Viestityypit:
public enum ReliableUdpMessageTypes : short
{
// Любое
Any = 0,
// Запрос к STUN server
StunRequest = 1,
// Ответ от STUN server
StunResponse = 2,
// Передача файла
FileTransfer =3,
// ...
}
Viesti lähetetään asynkronisesti, tätä varten protokolla toteuttaa asynkronisen ohjelmointimallin:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Viestin lähettämisen tulos on tosi - jos viesti on saapunut vastaanottajalle ja epätosi - jos yhteys suljettiin aikakatkaisun vuoksi:
public bool EndSendMessage(IAsyncResult asyncResult)
Johtopäätös
Paljon ei ole kuvattu tässä artikkelissa. Säikeen sovitusmekanismit, poikkeus- ja virhekäsittely, asynkronisten viestien lähetysmenetelmien toteutus. Mutta protokollan ytimen, pakettien käsittelyn, yhteyden muodostamisen ja aikakatkaisujen käsittelyn logiikan kuvauksen, pitäisi olla sinulle selvä.
Luotettavan toimitusprotokollan esitelty versio on riittävän vankka ja joustava täyttääkseen aiemmin määritellyt vaatimukset. Mutta haluan lisätä, että kuvattua toteutusta voidaan parantaa. Esimerkiksi suorituskyvyn lisäämiseksi ja ajastimen jaksojen dynaamiseksi muuttamiseksi protokollaan voidaan lisätä mekanismeja, kuten liukuva ikkuna ja RTT, on myös hyödyllistä toteuttaa mekanismi MTU:n määrittämiseksi yhteyssolmujen välillä (mutta vain jos lähetetään suuria viestejä) .
Kiitos huomiostasi, odotan kommenttejasi ja kommenttejasi.
PS Niille, jotka ovat kiinnostuneita yksityiskohdista tai haluavat vain testata protokollaa, linkki projektiin GitHubessa: Luotettava UDP-projekti
Päivitys: Kiitos Mayorovp и sidristij ideasta tehtävän lisäämisestä käyttöliittymään. Kirjaston yhteensopivuutta vanhojen käyttöjärjestelmien kanssa ei rikota, koska Neljäs kehys tukee sekä XP- että 4-palvelinta.