Internetning asl arxitekturasi har bir tugun global va noyob IP-manzilga ega bo'lgan va boshqa tugunlar bilan to'g'ridan-to'g'ri aloqa qila oladigan bir hil manzil maydonini nazarda tutgan. Endi Internet, aslida, boshqa arxitekturaga ega - global IP-manzillarning bir sohasi va NAT qurilmalari orqasida maxfiy manzillar yashiringan ko'plab sohalar.Ushbu arxitekturada faqat global manzil maydonidagi qurilmalar tarmoqdagi har qanday odam bilan osongina bog'lana oladi, chunki ular noyob, global yo'naltiriladigan IP-manzilga ega. Shaxsiy tarmoqdagi tugun bir xil tarmoqdagi boshqa tugunlarga ulanishi mumkin, shuningdek, global manzil maydonidagi boshqa taniqli tugunlarga ham ulanishi mumkin. Ushbu o'zaro ta'sirga asosan tarmoq manzillarini tarjima qilish mexanizmi tufayli erishiladi. Wi-Fi routerlar kabi NAT qurilmalari chiquvchi ulanishlar uchun maxsus tarjima jadvali yozuvlarini yaratadi va paketlardagi IP manzillari va port raqamlarini o'zgartiradi. Bu shaxsiy tarmoqdan global manzil maydonidagi xostlarga chiqish ulanishlariga imkon beradi. Biroq, shu bilan birga, NAT qurilmalari, agar kiruvchi ulanishlar uchun alohida qoidalar o'rnatilmagan bo'lsa, odatda barcha kiruvchi trafikni bloklaydi.
Internetning ushbu arxitekturasi mijoz-server aloqasi uchun etarlicha to'g'ri, bu erda mijozlar shaxsiy tarmoqlarda bo'lishi mumkin va serverlar global manzilga ega. Ammo bu ikkita tugunning to'g'ridan-to'g'ri ulanishi uchun qiyinchiliklar tug'diradi har xil xususiy tarmoqlar. Ikki tugun o'rtasidagi to'g'ridan-to'g'ri aloqa ovozli uzatish (Skype), kompyuterga masofadan kirish (TeamViewer) yoki onlayn o'yin kabi tengdoshlar orasidagi ilovalar uchun muhimdir.
Turli xil xususiy tarmoqlardagi qurilmalar o'rtasida tengdoshga ulanishni o'rnatishning eng samarali usullaridan biri teshik ochish deb ataladi. Ushbu usul ko'pincha UDP protokoliga asoslangan ilovalarda qo'llaniladi.
Ammo agar sizning ilovangiz ma'lumotlarning kafolatlangan yetkazib berilishini talab qilsa, masalan, siz kompyuterlar o'rtasida fayllarni uzatsangiz, UDP dan foydalanish TCP dan farqli ravishda UDP kafolatlangan yetkazib berish protokoli emasligi va paketlarni tartibda yetkazib berishni ta'minlamasligi sababli ko'p qiyinchiliklarga duch keladi. protokol.
Bunday holda, kafolatlangan paket yetkazib berishni ta'minlash uchun zarur funksionallikni ta'minlaydigan va UDP orqali ishlaydigan dastur qatlami protokolini amalga oshirish talab etiladi.
Darhol ta'kidlashni istardimki, turli xil xususiy tarmoqlardagi tugunlar o'rtasida TCP ulanishlarini o'rnatish uchun TCP teshiklarini ochish texnikasi mavjud, ammo ko'plab NAT qurilmalari uni qo'llab-quvvatlamasligi sababli, odatda ulanishning asosiy usuli sifatida qaralmaydi. bunday tugunlar.
Ushbu maqolaning qolgan qismida men faqat kafolatlangan etkazib berish protokolini amalga oshirishga e'tibor qarataman. UDP teshik ochish texnikasini amalga oshirish keyingi maqolalarda tasvirlanadi.
Protokol talablari
Ijobiy qayta aloqa mexanizmi (ijobiy tasdiq deb ataladigan) orqali amalga oshiriladigan ishonchli paket yetkazib berish.
Katta ma'lumotlarni samarali uzatish zarurati, ya'ni. protokol paketlarni keraksiz uzatishdan qochishi kerak
Yetkazib berishni tasdiqlash mexanizmini bekor qilish mumkin bo'lishi kerak ("sof" UDP protokoli sifatida ishlash qobiliyati)
Har bir xabarni tasdiqlash bilan buyruq rejimini amalga oshirish imkoniyati
Protokol orqali ma'lumotlarni uzatishning asosiy birligi xabar bo'lishi kerak
Ushbu talablar asosan maqolada tasvirlangan Ishonchli ma'lumotlar protokoli talablariga mos keladi RF 908 и RF 1151, va men ushbu protokolni ishlab chiqishda ushbu standartlarga tayandim.
Ushbu talablarni tushunish uchun ikkita tarmoq tugunlari o'rtasida TCP va UDP protokollari yordamida ma'lumotlarni uzatish vaqtini ko'rib chiqaylik. Ikkala holatda ham bitta paket yo'qoladi. TCP orqali interaktiv bo'lmagan ma'lumotlarni uzatish:
Diagrammadan ko'rinib turibdiki, paket yo'qolgan taqdirda TCP yo'qolgan paketni aniqlaydi va yo'qolgan segment raqamini so'rab jo'natuvchiga xabar beradi. UDP protokoli orqali ma'lumotlarni uzatish:
UDP yo'qotishlarni aniqlash choralarini ko'rmaydi. UDP protokolidagi uzatish xatolarini nazorat qilish to'liq dasturning javobgarligi.
TCP protokolidagi xatolarni aniqlash yakuniy tugun bilan ulanishni o'rnatish, ushbu ulanish holatini saqlash, har bir paket sarlavhasida yuborilgan baytlar sonini ko'rsatish va tasdiqlash raqamidan foydalangan holda kvitansiyalarni xabardor qilish orqali erishiladi.
Bundan tashqari, unumdorlikni yaxshilash uchun (ya'ni, tasdiqnoma olmasdan bir nechta segmentni yuborish) TCP protokoli uzatish oynasi deb ataladigan oynadan foydalanadi - segment jo'natuvchisi olishni kutayotgan ma'lumotlar baytlari soni.
TCP protokoli haqida ko'proq ma'lumot olish uchun qarang RF 793, UDP dan RF 768qaerda, aslida, ular belgilangan.
Yuqoridagilardan ko'rinib turibdiki, UDP (keyingi o'rinlarda deb yuritiladi) orqali ishonchli xabar yetkazib berish protokolini yaratish uchun. Ishonchli UDP), TCP ga o'xshash ma'lumotlarni uzatish mexanizmlarini amalga oshirish talab qilinadi. Aynan:
ulanish holatini saqlang
segment raqamlashdan foydalaning
maxsus tasdiqlash paketlaridan foydalaning
protokol o'tkazuvchanligini oshirish uchun soddalashtirilgan oyna mexanizmidan foydalaning
Bundan tashqari, sizga kerak:
ulanish uchun resurslarni ajratish uchun xabarning boshlanishini bildiradi
qabul qilingan xabarni yuqori oqim ilovasiga uzatish va protokol resurslarini chiqarish uchun xabar tugashini bildiradi
ulanishga xos protokolga etkazib berishni tasdiqlash mexanizmini "sof" UDP sifatida ishlashi uchun o'chirishga ruxsat bering
Ishonchli UDP sarlavhasi
Eslatib o'tamiz, UDP datagramma IP-datagrammasiga kiritilgan. Ishonchli UDP paketi UDP datagrammasiga mos ravishda "o'ralgan". Ishonchli UDP sarlavhasi inkapsulyatsiyasi:
Ishonchli UDP sarlavhasining tuzilishi juda oddiy:
Bayroqlar - paketlarni boshqarish bayroqlari
MessageType - ma'lum xabarlarga obuna bo'lish uchun yuqori oqim ilovalari tomonidan ishlatiladigan xabar turi
TransmissionId - uzatish raqami, qabul qiluvchining manzili va porti bilan birgalikda ulanishni noyob tarzda aniqlaydi.
PacketNumber - paket raqami
Variantlar - qo'shimcha protokol variantlari. Birinchi paketga kelsak, u xabar hajmini ko'rsatish uchun ishlatiladi
Bayroqlar quyidagicha:
FirstPacket - xabarning birinchi paketi
NoAsk - xabar tasdiqlash mexanizmini yoqishni talab qilmaydi
LastPacket - xabarning oxirgi paketi
RequestForPacket - tasdiqlash paketi yoki yo'qolgan paket uchun so'rov
Protokolning umumiy tamoyillari
Ishonchli UDP ikkita tugun o'rtasida kafolatlangan xabar uzatishga qaratilganligi sababli, u boshqa tomon bilan aloqa o'rnatish imkoniyatiga ega bo'lishi kerak. Ulanishni o'rnatish uchun jo'natuvchi FirstPacket bayrog'i bilan paketni yuboradi, unga javob ulanish o'rnatilganligini bildiradi. Barcha javob paketlari yoki boshqacha qilib aytganda, tasdiqlash paketlari, har doim PacketNumber maydonining qiymatini muvaffaqiyatli qabul qilingan paketlarning eng katta PacketNumber qiymatidan biriga o'rnatadi. Birinchi yuborilgan paket uchun Options maydoni xabarning o'lchamidir.
Xuddi shunday mexanizm ulanishni to'xtatish uchun ishlatiladi. LastPacket bayrog'i xabarning oxirgi paketiga o'rnatiladi. Javob paketida oxirgi paketning raqami + 1 ko'rsatilgan, bu qabul qiluvchi tomon uchun xabarning muvaffaqiyatli yetkazib berilishini anglatadi. Ulanishni o'rnatish va tugatish diagrammasi:
Ulanish o'rnatilganda ma'lumotlarni uzatish boshlanadi. Ma'lumotlar paketlar bloklarida uzatiladi. Har bir blok, oxirgisidan tashqari, belgilangan miqdordagi paketlarni o'z ichiga oladi. Bu qabul qilish/uzatuvchi oyna hajmiga teng. Ma'lumotlarning oxirgi blokida kamroq paketlar bo'lishi mumkin. Har bir blokni yuborgandan so'ng, jo'natuvchi tomon etkazib berishni tasdiqlash yoki yo'qolgan paketlarni qayta yetkazib berish so'rovini kutadi va javoblarni olish uchun qabul qilish/uzatish oynasini ochiq qoldiradi. Blokni yetkazib berishni tasdiqlashni olgandan so'ng, qabul qilish/uzatish oynasi siljiydi va keyingi ma'lumotlar bloki yuboriladi.
Qabul qiluvchi tomon paketlarni oladi. Har bir paket uzatish oynasiga to'g'ri kelishini tekshirish uchun tekshiriladi. Oynaga tushmaydigan paketlar va dublikatlar filtrlanadi. Chunki Agar oynaning o'lchami sobit bo'lsa va qabul qiluvchi va jo'natuvchi uchun bir xil bo'lsa, paketlar bloki yo'qolmagan holda etkazib berilganda, oyna keyingi ma'lumotlar blokining paketlarini qabul qilish uchun siljiydi va etkazib berishni tasdiqlaydi. yuborilgan. Agar oyna ish taymerida belgilangan muddatda to'ldirilmasa, u holda qaysi paketlar yetkazib berilmaganligi tekshiriladi va qayta yetkazib berish uchun so'rovlar yuboriladi. Qayta uzatish diagrammasi:
Taymerlar va protokol taymerlari
Ulanishni o'rnatib bo'lmaydigan bir nechta sabablar mavjud. Misol uchun, agar qabul qiluvchi tomon oflayn bo'lsa. Bunday holda, ulanishni o'rnatishga urinayotganda, ulanish vaqt tugashi bilan yopiladi. Ishonchli UDP ilovasi vaqt tugashini belgilash uchun ikkita taymerdan foydalanadi. Birinchisi, ishchi taymer masofaviy xostdan javob kutish uchun ishlatiladi. Agar u jo'natuvchi tomondan ishga tushsa, oxirgi yuborilgan paket qayta yuboriladi. Agar taymer qabul qiluvchida tugasa, yo'qolgan paketlar tekshiriladi va qayta yetkazib berish uchun so'rovlar yuboriladi.
Ikkinchi taymer tugunlar o'rtasida aloqa yo'q bo'lganda ulanishni yopish uchun kerak. Yuboruvchi tomon uchun u ish taymerining muddati tugagandan so'ng darhol boshlanadi va masofaviy tugundan javob kutadi. Belgilangan muddat ichida javob bo'lmasa, ulanish to'xtatiladi va resurslar chiqariladi. Qabul qiluvchi tomon uchun ulanishni yopish taymeri ish taymerining muddati ikki marta tugaganidan keyin ishga tushiriladi. Bu tasdiqlash paketini yo'qotishdan sug'urta qilish uchun kerak. Taymer muddati tugagach, ulanish ham tugatiladi va resurslar chiqariladi.
Ishonchli UDP uzatish holati diagrammasi
Protokol tamoyillari cheklangan holat mashinasida amalga oshiriladi, uning har bir holati paketlarni qayta ishlashning ma'lum bir mantiqiyligi uchun javobgardir.
Ishonchli UDP holati diagrammasi:
yopiq - aslida holat emas, bu avtomat uchun boshlang'ich va yakuniy nuqta. Davlat uchun yopiq uzatishni boshqarish bloki qabul qilinadi, u asinxron UDP serverini amalga oshirib, paketlarni tegishli ulanishlarga yo'naltiradi va holatni qayta ishlashni boshlaydi.
FirstPacketSending – xabar yuborilganda chiquvchi ulanishning dastlabki holati.
Bu holatda oddiy xabarlar uchun birinchi paket yuboriladi. Tasdiqlanmagan xabarlar uchun bu butun xabar yuboriladigan yagona holat.
SendingCycle – xabar paketlarini uzatish uchun asosiy holat.
Davlatdan unga o'tish FirstPacketSending xabarning birinchi paketi yuborilgandan keyin amalga oshiriladi. Aynan shu holatda qayta uzatish uchun barcha tasdiqlar va so'rovlar keladi. Undan chiqish ikki holatda mumkin - xabar muvaffaqiyatli yetkazib berilganda yoki vaqt tugashi bilan.
Birinchi paket qabul qilindi – xabarni qabul qiluvchining dastlabki holati.
U uzatish boshlanishining to'g'riligini tekshiradi, kerakli tuzilmalarni yaratadi va birinchi paketni olganligi to'g'risida tasdiqnoma yuboradi.
Bitta paketdan iborat bo'lgan va etkazib berishni tasdiqlovchi hujjatdan foydalanmasdan yuborilgan xabar uchun bu yagona holat. Bunday xabarni qayta ishlagandan so'ng, ulanish yopiladi.
O'rnatish – xabar paketlarini qabul qilishning asosiy holati.
U paketlarni vaqtincha saqlashga yozadi, paketlar yo‘qolganligini tekshiradi, paketlar bloki va butun xabarni yetkazib berish uchun bildirishnomalarni yuboradi va yo‘qolgan paketlarni qayta yetkazib berish uchun so‘rovlarni yuboradi. Agar butun xabar muvaffaqiyatli qabul qilingan bo'lsa, ulanish holatiga o'tadi Tugallandi, aks holda, kutish vaqti tugaydi.
Tugallandi – butun xabar muvaffaqiyatli qabul qilingan taqdirda ulanishni yopish.
Ushbu holat xabarni yig'ish uchun va jo'natuvchiga jo'natuvchiga yo'lda xabarni etkazib berishni tasdiqlash yo'qolgan holatlar uchun zarurdir. Bu holatdan kutish vaqti tugadi, lekin ulanish muvaffaqiyatli yopilgan deb hisoblanadi.
Kodga chuqurroq. uzatishni boshqarish bloki
Ishonchli UDP ning asosiy elementlaridan biri uzatishni boshqarish blokidir. Ushbu blokning vazifasi joriy ulanishlar va yordamchi elementlarni saqlash, kiruvchi paketlarni mos keladigan ulanishlarga taqsimlash, ulanishga paketlarni yuborish interfeysini ta'minlash va API protokolini amalga oshirishdir. Transmissiyani boshqarish bloki paketlarni UDP qatlamidan oladi va ularni qayta ishlash uchun davlat mashinasiga yuboradi. Paketlarni qabul qilish uchun u asinxron UDP serverini amalga oshiradi. ReliableUdpConnectionControlBlock sinfining ba'zi a'zolari:
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;
//...
}
Asinxron UDP serverini amalga oshirish:
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);
}
Har bir xabarni uzatish uchun ulanish haqidagi ma'lumotlarni o'z ichiga olgan tuzilma yaratiladi. Bunday tuzilma deyiladi ulanish yozuvi. ReliableUdpConnectionRecord sinfining ba'zi a'zolari:
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;
//...
}
Kodga chuqurroq. davlatlar
Davlatlar ishonchli UDP protokolining davlat mashinasini amalga oshiradilar, bu erda paketlarni asosiy qayta ishlash amalga oshiriladi. ReliableUdpState mavhum klassi holat uchun interfeysni taqdim etadi:
Protokolning butun mantig'i yuqorida keltirilgan sinflar tomonidan, masalan, ulanish yozuvidan ReliableUdp sarlavhasini yaratish kabi statik usullarni ta'minlovchi yordamchi sinf tomonidan amalga oshiriladi.
Keyinchalik, protokolning asosiy algoritmlarini aniqlaydigan interfeys usullarini amalga oshirishni batafsil ko'rib chiqamiz.
DisposeByTimeout usuli
DisposeByTimeout usuli vaqt tugashidan keyin ulanish resurslarini chiqarish va xabarni muvaffaqiyatli/muvaffaqiyatsiz yetkazib berish haqida signal berish uchun javobgardir. ReliableUdpState.DisposeByTimeout:
Bu faqat shtatda bekor qilingan Tugallandi. Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// сообщаем об успешном получении сообщения
SetAsCompleted(connectionRecord);
}
ProcessPackets usuli
ProcessPackets usuli paket yoki paketlarni qo'shimcha qayta ishlash uchun javobgardir. To'g'ridan-to'g'ri yoki paketli kutish taymer orqali chaqiriladi.
Holatida O'rnatish usul bekor qilingan va yo'qolgan paketlarni tekshirish va holatga o'tish uchun javobgardir Tugallandi, oxirgi paketni olgan va muvaffaqiyatli tekshiruvdan o'tgan taqdirda 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);
}
}
Holatida SendingCycle bu usul faqat taymerda chaqiriladi va oxirgi xabarni qayta yuborish, shuningdek ulanishni yopish taymerini yoqish uchun javobgardir. 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);
}
Holatida Tugallandi usul ishlaydigan taymerni to'xtatadi va xabarni abonentlarga yuboradi. Bajarildi.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// собираем сообщение и передаем его подписчикам
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
ReceivePacket usuli
Holatida Birinchi paket qabul qilindi Usulning asosiy vazifasi - birinchi xabar paketi haqiqatda interfeysga kelgan-kelmaganligini aniqlash, shuningdek, bitta paketdan iborat xabarni yig'ish. 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);
}
}
Holatida SendingCycle bu usul yetkazib berishni tasdiqlash va qayta yuborish so'rovlarini qabul qilish uchun bekor qilingan. 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));
}
Holatida O'rnatish ReceivePacket usulida kiruvchi paketlardan xabar yig'ishning asosiy ishi amalga oshiriladi. 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);
}
}
Holatida Tugallandi usulning yagona vazifasi - xabarni muvaffaqiyatli yetkazib berishni qayta tan olish. Bajarildi.Paketni qabul qilish:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// повторная отправка последнего пакета в связи с тем,
// что последний ack не дошел до отправителя
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Paketni yuborish usuli
Holatida FirstPacketSending bu usul ma'lumotlarning birinchi paketini yoki agar xabar yetkazib berishni tasdiqlashni talab qilmasa, butun xabarni yuboradi. 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);
}
Holatida SendingCycle bu usulda paketlar bloki yuboriladi. 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 );
}
}
Kodga chuqurroq. Aloqalarni yaratish va o'rnatish
Endi biz asosiy holatlarni va holatlarni boshqarishda qo‘llaniladigan usullarni ko‘rib chiqdik, keling, protokol qanday ishlashiga bir necha misollarni biroz batafsilroq ko‘rib chiqaylik. Oddiy sharoitlarda ma'lumotlarni uzatish diagrammasi:
Yaratishni batafsil ko'rib chiqing ulanish yozuvi birinchi paketni ulash va yuborish uchun. O'tkazish har doim xabar yuborish API-ni chaqiradigan dastur tomonidan amalga oshiriladi. Keyinchalik, uzatishni boshqarish blokining StartTransmission usuli chaqiriladi, bu yangi xabar uchun ma'lumotlarni uzatishni boshlaydi. Chiquvchi ulanishni yaratish:
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]);
}
Birinchi paketni yuborish (FirstPacketSending holati):
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);
}
Birinchi paketni yuborgandan so'ng, jo'natuvchi davlatga kiradi SendingCycle – paketni yetkazib berish tasdiqlanishini kuting.
Qabul qiluvchi tomon EndReceive usulidan foydalanib, yuborilgan paketni oladi, yangisini yaratadi ulanish yozuvi va bu paketni qayta ishlash uchun davlatning ReceivePacket usuliga oldindan ajratilgan sarlavha bilan uzatadi Birinchi paket qabul qilindi Qabul qiluvchi tomonda ulanishni yaratish:
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);
}
Birinchi paketni qabul qilish va tasdiqni yuborish (FirstPacketReceived holati):
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);
}
}
Kodga chuqurroq. Vaqt tugashi bilan ulanishni yopish
Vaqt tugashi bilan ishlash Ishonchli UDP ning muhim qismidir. Oraliq tugun ishlamay qolgan va ikkala yo'nalishda ham ma'lumotlarni yetkazib berish imkonsiz bo'lgan misolni ko'rib chiqing. Vaqt tugashi bilan ulanishni yopish diagrammasi:
Diagrammadan ko'rinib turibdiki, jo'natuvchining ish taymeri paketlar blokini yuborgandan so'ng darhol boshlanadi. Bu holatning SendPacket usulida sodir bo'ladi SendingCycle. Ish taymerini yoqish (SendingCycle holati):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// отправляем блок пакетов
// ...
// перезапускаем таймер после отправки
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
Taymer muddatlari ulanish yaratilganda o'rnatiladi. Standart ShortTimerPeriod 5 soniya. Misolda, u 1,5 soniyaga o'rnatiladi.
Kiruvchi ulanish uchun taymer oxirgi kiruvchi ma'lumotlar paketini olgandan keyin ishga tushadi, bu holatning ReceivePacket usulida sodir bo'ladi. O'rnatish Ish taymerini yoqish (yig'ish holati):
Ish taymerini kutish vaqtida kiruvchi ulanishga boshqa paketlar kelmadi. Taymer o'chdi va ProcessPackets usulini chaqirdi, bu erda yo'qolgan paketlar topildi va birinchi marta qayta yetkazib berish so'rovlari yuborildi. Qayta yetkazib berish so'rovlarini yuborish (yig'ish holati):
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 o'zgaruvchisi o'rnatilgan haqiqiy. Ushbu o'zgaruvchi ish taymerini qayta ishga tushirish uchun javobgardir.
Yuboruvchi tomonida ish taymeri ham ishga tushiriladi va oxirgi yuborilgan paket qayta yuboriladi. Ulanishni yopish taymerini yoqish (SendingCycle holati):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// отправляем повторно последний пакет
// ...
// включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
StartCloseWaitTimer(connectionRecord);
}
Shundan so'ng, chiquvchi ulanishda ulanishni yopish taymeri boshlanadi. 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);
}
Ulanishni yopish taymerining kutish vaqti sukut bo'yicha 30 soniya.
Qisqa vaqtdan so'ng, qabul qiluvchi tomonidagi ish taymeri yana yonadi, so'rovlar yana yuboriladi, shundan so'ng kiruvchi ulanish uchun ulanishni yopish taymerini ishga tushiradi.
Yopiq taymerlar ishga tushganda, ikkala ulanish yozuvlarining barcha resurslari chiqariladi. Yuboruvchi yuqori oqim ilovasiga yetkazib berishda xatolik haqida xabar beradi (Ishonchli UDP API-ga qarang). Ulanish yozuvlari resurslarini chiqarish:
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);
}
}
Kodga chuqurroq. Ma'lumotlar uzatishni tiklash
Paket yo'qolgan taqdirda ma'lumotlarni uzatishni tiklash diagrammasi:
Taymer tugashi bilan ulanishni yopishda allaqachon muhokama qilinganidek, ish taymerining muddati tugagach, qabul qiluvchi yo'qolgan paketlarni tekshiradi. Paket yo'qolgan taqdirda, qabul qiluvchiga etib bormagan paketlar sonining ro'yxati tuziladi. Bu raqamlar ma'lum bir ulanishning LostPackets massiviga kiritiladi va qayta yetkazib berish uchun so'rovlar yuboriladi. Paketlarni qayta etkazib berish uchun so'rovlarni yuborish (yig'ish holati):
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);
}
}
// ...
}
}
Yuboruvchi qayta yetkazib berish so'rovini qabul qiladi va etishmayotgan paketlarni jo'natadi. Shuni ta'kidlash kerakki, hozirda jo'natuvchi ulanishni yopish taymerini allaqachon ishga tushirgan va so'rov qabul qilinganda u qayta tiklanadi. Yo'qotilgan paketlarni qayta yuborish (SendingCycle holati):
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));
}
Qayta yuborilgan paket (diagrammada №3 paket) kiruvchi ulanish tomonidan qabul qilinadi. Qabul qilish oynasi to'lganligini va normal ma'lumotlarni uzatish tiklanganligini tekshirish uchun tekshiriladi. Qabul qilish oynasida urishlarni tekshirish (yig'ish holati):
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);
}
// ...
}
Ishonchli UDP API
Ma'lumotlarni uzatish protokoli bilan o'zaro ishlash uchun ochiq Ishonchli Udp klassi mavjud bo'lib, u uzatishni boshqarish bloki ustidagi o'ram hisoblanadi. Mana, sinfning eng muhim a'zolari:
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()
}
Xabarlar obuna orqali qabul qilinadi. Qayta qo'ng'iroq qilish usuli uchun delegat imzosi:
public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );
Xabar:
public class ReliableUdpMessage
{
// тип сообщения, простое перечисление
public ReliableUdpMessageTypes Type { get; private set; }
// данные сообщения
public byte[] Body { get; private set; }
// если установлено в true – механизм подтверждения доставки будет отключен
// для передачи конкретного сообщения
public bool NoAsk { get; private set; }
}
Muayyan xabar turiga va/yoki ma'lum bir jo'natuvchiga obuna bo'lish uchun ikkita ixtiyoriy parametr ishlatiladi: ReliableUdpMessageTypes messageType va IPEndPoint ipEndPoint.
Xabar turlari:
public enum ReliableUdpMessageTypes : short
{
// Любое
Any = 0,
// Запрос к STUN server
StunRequest = 1,
// Ответ от STUN server
StunResponse = 2,
// Передача файла
FileTransfer =3,
// ...
}
Xabar asinxron tarzda yuboriladi; buning uchun protokol asinxron dasturlash modelini amalga oshiradi:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Xabarni yuborish natijasi rost bo'ladi - agar xabar qabul qiluvchiga muvaffaqiyatli etib borgan bo'lsa va noto'g'ri bo'lsa - ulanish vaqt tugashi bilan yopilgan bo'lsa:
public bool EndSendMessage(IAsyncResult asyncResult)
xulosa
Ushbu maqolada ko'p narsa tasvirlanmagan. Mavzuni moslashtirish mexanizmlari, istisno va xatolarni qayta ishlash, asinxron xabar yuborish usullarini amalga oshirish. Ammo protokolning o'zagi, paketlarni qayta ishlash, ulanishni o'rnatish va taym-autlarni qayta ishlash mantig'ining tavsifi siz uchun tushunarli bo'lishi kerak.
Ishonchli yetkazib berish protokolining ko'rsatilgan versiyasi ilgari belgilangan talablarga javob beradigan darajada mustahkam va moslashuvchan. Ammo shuni qo'shimcha qilmoqchimanki, tasvirlangan dasturni yaxshilash mumkin. Masalan, o'tkazuvchanlikni oshirish va taymer davrlarini dinamik ravishda o'zgartirish uchun protokolga surma oynasi va RTT kabi mexanizmlar qo'shilishi mumkin, shuningdek ulanish tugunlari o'rtasida MTU ni aniqlash mexanizmini amalga oshirish foydali bo'ladi (lekin faqat katta xabarlar yuborilsa) .
E'tiboringiz uchun rahmat, fikr va mulohazalaringizni kutaman.
PS Tafsilotlarga qiziqqanlar yoki shunchaki protokolni sinab ko'rmoqchi bo'lganlar uchun GitHube-dagi loyihaga havola: Ishonchli UDP loyihasi
Yangilash: rahmat mayorovp и sidristij interfeysga vazifa qo'shish g'oyasi uchun. Kutubxonaning eski operatsion tizimlar bilan mosligi buzilmaydi, chunki 4-ramka XP va 2003 serverlarini qo'llab-quvvatlaydi.