Внедряване на надеждния Udp протокол за .Net

Интернет се е променил отдавна. Един от основните протоколи на Интернет - UDP се използва от приложенията не само за доставяне на дейтаграми и излъчвания, но и за осигуряване на "peer-to-peer" връзки между мрежови възли. Поради простия си дизайн, този протокол има много непланирани преди това приложения, но недостатъците на протокола, като липсата на гарантирана доставка, не са изчезнали никъде. Тази статия описва прилагането на протокола за гарантирана доставка през UDP.
Съдържание:Влизане
Изисквания към протокола
Надеждна UDP заглавка
Общи принципи на протокола
Изчаквания и протоколни таймери
Диаграма на състоянието на надеждно UDP предаване
По-дълбоко в кода. блок за управление на трансмисията
По-дълбоко в кода. държави

По-дълбоко в кода. Създаване и установяване на връзки
По-дълбоко в кода. Затваряне на връзката при изчакване
По-дълбоко в кода. Възстановяване на трансфера на данни
Надежден UDP API
Заключение
Полезни връзки и статии

Влизане

Оригиналната архитектура на Интернет предполагаше хомогенно адресно пространство, в което всеки възел имаше глобален и уникален IP адрес и можеше да комуникира директно с други възли. Сега Интернет всъщност има различна архитектура - една област от ​глобални IP адреси и много области с частни адреси, скрити зад NAT устройства.В тази архитектура само устройства в глобалното адресно пространство могат лесно да комуникират с всеки в мрежата, тъй като имат уникален, глобално маршрутизиран IP адрес. Възел в частна мрежа може да се свързва с други възли в същата мрежа и може също да се свързва с други добре познати възли в глобалното адресно пространство. Това взаимодействие се постига до голяма степен благодарение на механизма за транслация на мрежови адреси. NAT устройства, като Wi-Fi рутери, създават специални записи в таблицата за превод за изходящи връзки и променят IP адреси и номера на портове в пакети. Това позволява изходящи връзки от частната мрежа към хостове в глобалното адресно пространство. Но в същото време NAT устройствата обикновено блокират целия входящ трафик, освен ако не са зададени отделни правила за входящи връзки.

Тази архитектура на Интернет е достатъчно правилна за комуникация клиент-сървър, където клиентите могат да бъдат в частни мрежи, а сървърите имат глобален адрес. Но създава трудности за директната връзка между два възела различни частни мрежи. Директната връзка между два възела е важна за peer-to-peer приложения като предаване на глас (Skype), получаване на отдалечен достъп до компютър (TeamViewer) или онлайн игри.

Един от най-ефективните методи за установяване на peer-to-peer връзка между устройства в различни частни мрежи се нарича пробиване на дупки. Тази техника се използва най-често с приложения, базирани на UDP протокола.

Но ако вашето приложение изисква гарантирана доставка на данни, например прехвърляте файлове между компютри, тогава използването на UDP ще има много трудности поради факта, че UDP не е протокол за гарантирана доставка и не осигурява доставка на пакети по ред, за разлика от TCP протокол.

В този случай, за да се осигури гарантирана доставка на пакети, е необходимо да се внедри протокол на приложния слой, който осигурява необходимата функционалност и работи върху UDP.

Искам веднага да отбележа, че има техника за пробиване на TCP дупки за установяване на TCP връзки между възли в различни частни мрежи, но поради липсата на поддръжка за нея от много NAT устройства, тя обикновено не се счита за основен начин за свързване такива възли.

За останалата част от тази статия ще се съсредоточа само върху прилагането на протокола за гарантирана доставка. Изпълнението на UDP техниката за пробиване на дупки ще бъде описано в следващите статии.

Изисквания към протокола

  1. Надеждна доставка на пакети, реализирана чрез механизъм за положителна обратна връзка (така нареченото положително потвърждение)
  2. Необходимостта от ефективен трансфер на големи данни, т.е. протоколът трябва да избягва ненужното препредаване на пакети
  3. Трябва да е възможно да се отмени механизмът за потвърждение на доставката (способността да функционира като "чист" UDP протокол)
  4. Възможност за реализиране на команден режим, с потвърждение на всяко съобщение
  5. Основната единица за пренос на данни по протокола трябва да бъде съобщение

Тези изисквания до голяма степен съвпадат с изискванията на протокола за надеждни данни, описани в RFC 908 и RFC 1151, и аз разчитах на тези стандарти, когато разработвах този протокол.

За да разберем тези изисквания, нека да разгледаме времето за прехвърляне на данни между два мрежови възела, използващи TCP и UDP протоколите. Нека и в двата случая ще имаме един загубен пакет.
Прехвърляне на неинтерактивни данни през TCP:Внедряване на надеждния Udp протокол за .Net

Както можете да видите от диаграмата, в случай на загуба на пакет, TCP ще открие изгубения пакет и ще го докладва на подателя, като поиска номера на изгубения сегмент.
Пренос на данни по UDP протокол:Внедряване на надеждния Udp протокол за .Net

UDP не предприема никакви стъпки за откриване на загуба. Контролът на грешките при предаване в UDP протокола е изцяло отговорност на приложението.

Откриването на грешки в TCP протокола се постига чрез установяване на връзка с краен възел, съхраняване на състоянието на тази връзка, посочване на броя байтове, изпратени във всеки хедър на пакет, и уведомяване за разписки с помощта на номер за потвърждение.

Освен това, за подобряване на производителността (т.е. изпращане на повече от един сегмент без получаване на потвърждение), TCP протоколът използва така наречения прозорец за предаване - броят байтове данни, които подателят на сегмента очаква да получи.

За повече информация относно TCP протокола вижте RFC 793, от UDP към RFC 768където всъщност са дефинирани.

От горното става ясно, че за да се създаде надежден протокол за доставка на съобщения през UDP (наричан по-нататък Надежден UDP), се изисква да се внедрят механизми за пренос на данни, подобни на TCP. а именно:

  • запазване на състоянието на връзката
  • използвайте номериране на сегменти
  • използвайте специални пакети за потвърждение
  • използвайте опростен механизъм за прозорци, за да увеличите пропускателната способност на протокола

Освен това се нуждаете от:

  • сигнализира за началото на съобщение, за да разпредели ресурси за връзката
  • сигнализира край на съобщение, за да предаде полученото съобщение на приложението нагоре по веригата и да освободи ресурсите на протокола
  • позволяват на специфичния за връзката протокол да деактивира механизма за потвърждение на доставката, за да функционира като "чист" UDP

Надеждна UDP заглавка

Спомнете си, че UDP дейтаграма е капсулирана в IP дейтаграма. Надеждният UDP пакет е подходящо "опакован" в UDP дейтаграма.
Надеждно UDP капсулиране на заглавката:Внедряване на надеждния Udp протокол за .Net

Структурата на надеждния UDP хедър е доста проста:

Внедряване на надеждния Udp протокол за .Net

  • Флагове - флагове за контрол на пакета
  • MessageType - тип съобщение, използвано от приложения нагоре по веригата за абониране за конкретни съобщения
  • TransmissionId - номерът на предаването, заедно с адреса и порта на получателя, уникално идентифицира връзката
  • PacketNumber - номер на пакет
  • Опции - допълнителни опции на протокола. В случай на първия пакет, той се използва за указване на размера на съобщението

Флаговете са както следва:

  • FirstPacket - първият пакет от съобщението
  • NoAsk - съобщението не изисква механизъм за потвърждение, за да бъде активиран
  • LastPacket - последният пакет от съобщението
  • RequestForPacket - пакет за потвърждение или заявка за изгубен пакет

Общи принципи на протокола

Тъй като Reliable UDP е фокусиран върху гарантирано предаване на съобщения между два възела, той трябва да може да установи връзка с другата страна. За да установи връзка, изпращачът изпраща пакет с флага FirstPacket, отговорът на който ще означава, че връзката е установена. Всички пакети с отговори или, с други думи, пакети за потвърждение, винаги задават стойността на полето PacketNumber на една повече от най-голямата стойност на PacketNumber на успешно получените пакети. Полето Опции за първия изпратен пакет е размерът на съобщението.

Подобен механизъм се използва за прекратяване на връзка. Флагът LastPacket е зададен на последния пакет от съобщението. В отговорния пакет се посочва номерът на последния пакет + 1, което за приемащата страна означава успешно предаване на съобщението.
Диаграма за установяване и прекратяване на връзката:Внедряване на надеждния Udp протокол за .Net

Когато връзката се установи, започва прехвърлянето на данни. Данните се предават на блокове от пакети. Всеки блок, с изключение на последния, съдържа фиксиран брой пакети. Той е равен на размера на прозореца за получаване/предаване. Последният блок от данни може да има по-малко пакети. След изпращане на всеки блок, изпращащата страна чака потвърждение за доставка или заявка за повторно доставяне на изгубени пакети, оставяйки прозореца за получаване/предаване отворен за получаване на отговори. След получаване на потвърждение за доставка на блок, прозорецът за получаване/предаване се измества и се изпраща следващият блок от данни.

Получаващата страна получава пакетите. Всеки пакет се проверява, за да се види дали попада в рамките на прозореца за предаване. Пакетите и дубликатите, които не попадат в прозореца, се филтрират. защото Ако размерът на прозореца е фиксиран и еднакъв за получателя и подателя, тогава в случай на доставка на блок от пакети без загуба, прозорецът се измества, за да получи пакети от следващия блок от данни и се получава потвърждение за доставка изпратено. Ако прозорецът не се запълни в рамките на периода, зададен от работния таймер, ще започне проверка кои пакети не са доставени и ще бъдат изпратени заявки за повторно доставяне.
Диаграма на препредаване:Внедряване на надеждния Udp протокол за .Net

Изчаквания и протоколни таймери

Има няколко причини, поради които не може да се установи връзка. Например, ако приемащата страна е офлайн. В този случай, когато се опитвате да установите връзка, връзката ще бъде затворена след изчакване. Надеждното изпълнение на UDP използва два таймера за задаване на изчакване. Първият, работният таймер, се използва за изчакване на отговор от отдалечения хост. Ако се задейства от страна на изпращача, последният изпратен пакет се изпраща повторно. Ако таймерът изтече при получателя, тогава се извършва проверка за изгубени пакети и се изпращат заявки за повторно доставяне.

Вторият таймер е необходим за затваряне на връзката в случай на липса на комуникация между възлите. За страната на подателя, той стартира веднага след изтичане на работния таймер и чака отговор от отдалечения възел. Ако няма отговор в посочения период, връзката се прекъсва и ресурсите се освобождават. За приемащата страна таймерът за затваряне на връзката се стартира, след като работният таймер изтече два пъти. Това е необходимо, за да се застраховате срещу загуба на пакета за потвърждение. Когато таймерът изтече, връзката също се прекъсва и ресурсите се освобождават.

Диаграма на състоянието на надеждно UDP предаване

Принципите на протокола се изпълняват в краен автомат, всяко състояние на който отговаря за определена логика на обработка на пакети.
Надеждна UDP диаграма на състоянието:

Внедряване на надеждния Udp протокол за .Net

Затворен - всъщност не е състояние, това е начална и крайна точка за автомата. За държавата Затворен получава се контролен блок за предаване, който, реализирайки асинхронен UDP сървър, препраща пакети към съответните връзки и започва обработка на състоянието.

FirstPacketSending – първоначалното състояние, в което е изходящата връзка при изпращане на съобщението.

В това състояние се изпраща първият пакет за нормални съобщения. За съобщения без потвърждение за изпращане това е единственото състояние, в което се изпраща цялото съобщение.

Цикъл на изпращане – основно състояние за предаване на пакети съобщения.

Преход към него от държавата FirstPacketSending извършва се след изпращане на първия пакет от съобщението. Именно в това състояние идват всички потвърждения и искания за препредаване. Изходът от него е възможен в два случая - при успешно доставяне на съобщението или при таймаут.

FirstPacketReceived – първоначалното състояние за получателя на съобщението.

Той проверява правилността на началото на предаването, създава необходимите структури и изпраща потвърждение за получаване на първия пакет.

За съобщение, което се състои от един пакет и е изпратено без използване на доказателство за доставка, това е единственото състояние. След обработка на такова съобщение връзката се затваря.

Монтаж – основно състояние за получаване на пакети съобщения.

Той записва пакети във временно хранилище, проверява за загуба на пакети, изпраща потвърждения за доставката на блок от пакети и цялото съобщение и изпраща заявки за повторно доставяне на изгубени пакети. В случай на успешно получаване на цялото съобщение, връзката преминава в състояние XNUMXавършен, в противен случай изтича изчакване.

XNUMXавършен – затваряне на връзката при успешно получаване на цялото съобщение.

Това състояние е необходимо за сглобяването на съобщението и за случая, когато потвърждението за доставка на съобщението е изгубено по пътя към подателя. Това състояние се излиза от изчакване, но връзката се счита за успешно затворена.

По-дълбоко в кода. блок за управление на трансмисията

Един от ключовите елементи на Reliable UDP е блокът за управление на предаването. Задачата на този блок е да съхранява текущи връзки и спомагателни елементи, да разпределя входящите пакети към съответните връзки, да предоставя интерфейс за изпращане на пакети към връзка и да внедрява API на протокола. Блокът за управление на предаването получава пакети от UDP слоя и ги препраща към крайната машина за обработка. За да получава пакети, той внедрява асинхронен UDP сървър.
Някои членове на класа ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Внедряване на асинхронен UDP сървър:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения 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);
}

За всяко прехвърляне на съобщение се създава структура, която съдържа информация за връзката. Такава структура се нарича запис на връзката.
Някои членове на класа ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

По-дълбоко в кода. държави

Държавите внедряват държавната машина на протокола Reliable UDP, където се извършва основната обработка на пакетите. Абстрактният клас ReliableUdpState осигурява интерфейс за състоянието:

Внедряване на надеждния Udp протокол за .Net

Цялата логика на протокола се изпълнява от класовете, представени по-горе, заедно с спомагателен клас, който предоставя статични методи, като например конструиране на заглавката ReliableUdp от записа на връзката.

След това ще разгледаме подробно изпълнението на интерфейсните методи, които определят основните алгоритми на протокола.

Метод DisposeByTimeout

Методът DisposeByTimeout е отговорен за освобождаването на ресурсите за връзка след изчакване и за сигнализирането на успешно/неуспешно доставяне на съобщение.
ReliableUdpState.DisposeByTimeout:

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;      
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

Отменя се само в държавата XNUMXавършен.
Завършено. DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

Метод ProcessPackets

Методът ProcessPackets отговаря за допълнителната обработка на пакет или пакети. Извиква се директно или чрез таймер за изчакване на пакети.

Способен на Монтаж методът се отменя и отговаря за проверката за изгубени пакети и преминаването към състоянието XNUMXавършен, в случай на получаване на последния пакет и преминаване на успешна проверка
Сглобяване.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Способен на Цикъл на изпращане този метод се извиква само на таймер и отговаря за повторното изпращане на последното съобщение, както и за активирането на таймера за затваряне на връзката.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Способен на XNUMXавършен методът спира работещия таймер и изпраща съобщението до абонатите.
Завършено.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Метод ReceivePacket

Способен на FirstPacketReceived основната задача на метода е да определи дали първият пакет със съобщения действително е пристигнал на интерфейса, както и да събере съобщение, състоящо се от един пакет.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // 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);
  }
}

Способен на Цикъл на изпращане този метод се отменя, за да приема потвърждения за доставка и заявки за повторно предаване.
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));
}

Способен на Монтаж в метода ReceivePacket се извършва основната работа по сглобяването на съобщение от входящите пакети.
Сглобяване.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);
  }
}

Способен на XNUMXавършен единствената задача на метода е да изпрати повторно потвърждение за успешното доставяне на съобщението.
Завършен. ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Метод за изпращане на пакет

Способен на FirstPacketSending този метод изпраща първия пакет данни или, ако съобщението не изисква потвърждение за доставка, цялото съобщение.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Способен на Цикъл на изпращане при този метод се изпраща блок от пакети.
SendingCycle.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 );
  }
}

По-дълбоко в кода. Създаване и установяване на връзки

Сега, след като видяхме основните състояния и методите, използвани за обработка на състояния, нека разбием няколко примера за това как работи протоколът малко по-подробно.
Диаграма на предаване на данни при нормални условия:Внедряване на надеждния Udp протокол за .Net

Разгледайте подробно създаването запис на връзката за свързване и изпращане на първия пакет. Прехвърлянето винаги се инициира от приложението, което извиква API за изпращане на съобщение. След това се извиква методът StartTransmission на контролния блок за предаване, който стартира предаването на данни за новото съобщение.
Създаване на изходяща връзка:

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]);
}

Изпращане на първия пакет (състояние FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

След изпращане на първия пакет изпращачът влиза в състоянието Цикъл на изпращане – изчакайте потвърждение за доставка на пакета.
Получаващата страна, използвайки метода EndReceive, получава изпратения пакет, създава нов запис на връзката и предава този пакет с предварително анализирана заглавка към метода ReceivePacket на състоянието за обработка FirstPacketReceived
Създаване на връзка от приемащата страна:

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);
}

Получаване на първия пакет и изпращане на потвърждение (състояние FirstPacketReceived):

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);
  }
}

По-дълбоко в кода. Затваряне на връзката при изчакване

Обработката на изчакване е важна част от надеждния UDP. Помислете за пример, в който междинен възел се повреди и доставката на данни в двете посоки стана невъзможна.
Диаграма за затваряне на връзка чрез таймаут:Внедряване на надеждния Udp протокол за .Net

Както се вижда от диаграмата, работният таймер на изпращача започва веднага след изпращането на блок от пакети. Това се случва в метода SendPacket на състоянието Цикъл на изпращане.
Активиране на работния таймер (състояние на цикъл на изпращане):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

Периодите на таймера се задават при създаване на връзката. По подразбиране ShortTimerPeriod е 5 секунди. В примера е зададено на 1,5 секунди.

За входяща връзка таймерът започва след получаване на последния входящ пакет данни, това се случва в метода ReceivePacket на състоянието Монтаж
Активиране на работния таймер (състояние на сглобяване):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Няма повече пакети, пристигнали при входящата връзка, докато чакате работния таймер. Таймерът изгасна и извика метода ProcessPackets, където изгубените пакети бяха открити и заявките за повторно доставяне бяха изпратени за първи път.
Изпращане на заявки за повторно доставяне (състояние на сглобяване):

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 е зададена на вярно. Тази променлива отговаря за рестартирането на работния таймер.

От страна на изпращача работният таймер също се задейства и последният изпратен пакет се изпраща повторно.
Активиране на таймера за затваряне на връзката (състояние на цикъл на изпращане):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

След това таймерът за затваряне на връзката започва в изходящата връзка.
ReliableUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

Периодът на изчакване на таймера за затваряне на връзката е 30 секунди по подразбиране.

След кратко време работещият таймер от страната на получателя се задейства отново, отново се изпращат заявки, след което стартира таймерът за затваряне на връзката за входящата връзка

Когато се активират таймерите за затваряне, всички ресурси на двата записа за връзка се освобождават. Подателят съобщава за неуспешната доставка на приложението нагоре по веригата (вижте Надежден UDP API).
Освобождаване на ресурси за запис на връзка:

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);
  }
}

По-дълбоко в кода. Възстановяване на трансфера на данни

Диаграма за възстановяване на предаване на данни в случай на загуба на пакет:Внедряване на надеждния Udp протокол за .Net

Както вече беше обсъдено при затваряне на връзката при изчакване, когато работният таймер изтече, приемникът ще провери за изгубени пакети. В случай на загуба на пакети ще бъде съставен списък с броя на пакетите, които не са достигнали до получателя. Тези номера се въвеждат в масива LostPackets на конкретна връзка и се изпращат заявки за повторно доставяне.
Изпращане на заявки за повторно доставяне на пакети (състояние на сглобяване):

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);
      }
    }
    // ...
  }
}

Подателят ще приеме заявката за повторно доставяне и ще изпрати липсващите пакети. Струва си да се отбележи, че в този момент подателят вече е стартирал таймера за затваряне на връзката и когато се получи заявка, той се нулира.
Повторно изпращане на изгубени пакети (състояние на цикъл на изпращане):

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));
}

Повторно изпратеният пакет (пакет #3 в диаграмата) се получава от входящата връзка. Прави се проверка дали прозорецът за получаване е пълен и нормалното предаване на данни е възстановено.
Проверка за попадения в прозореца за получаване (състояние на сглобяване):

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);
  }
  // ...
}

Надежден UDP API

За взаимодействие с протокола за пренос на данни има отворен клас Reliable Udp, който е обвивка над контролния блок за прехвърляне. Ето най-важните членове на класа:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<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 );

Съобщение:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

За да се абонирате за конкретен тип съобщение и/или конкретен подател, се използват два незадължителни параметъра: ReliableUdpMessageTypes messageType и IPEndPoint ipEndPoint.

Типове съобщения:

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

Съобщението се изпраща асинхронно; за това протоколът прилага модел на асинхронно програмиране:

public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)

Резултатът от изпращането на съобщение ще бъде вярно - ако съобщението е достигнало успешно до получателя и невярно - ако връзката е затворена след изчакване:

public bool EndSendMessage(IAsyncResult asyncResult)

Заключение

Много неща не са описани в тази статия. Механизми за съвпадение на нишки, обработка на изключения и грешки, внедряване на методи за асинхронно изпращане на съобщения. Но същността на протокола, описанието на логиката за обработка на пакети, установяване на връзка и обработка на изчакване, трябва да ви е ясно.

Демонстрираната версия на надеждния протокол за доставка е достатъчно стабилна и гъвкава, за да отговори на предварително дефинираните изисквания. Но искам да добавя, че описаното изпълнение може да бъде подобрено. Например, за увеличаване на пропускателната способност и динамична промяна на периодите на таймера, механизми като плъзгащ се прозорец и RTT могат да бъдат добавени към протокола, също така ще бъде полезно да се приложи механизъм за определяне на MTU между възлите на връзката (но само ако се изпращат големи съобщения) .

Благодаря ви за вниманието, очаквам вашите коментари и коментари.

PS За тези, които се интересуват от подробности или просто искат да тестват протокола, връзката към проекта в GitHube:
Надежден UDP проект

Полезни връзки и статии

  1. Спецификация на TCP протокола: на английски език и на руски език
  2. Спецификация на UDP протокола: на английски език и на руски език
  3. Обсъждане на протокола RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Надежден протокол за данни: RFC 908 и RFC 1151
  5. Проста реализация на потвърждение за доставка през UDP: Поемете пълен контрол над вашата мрежа с .NET и UDP
  6. Статия, описваща механизмите за преминаване на NAT: Peer-to-Peer комуникация в преводачи на мрежови адреси
  7. Реализация на модела на асинхронно програмиране: Внедряване на модела за асинхронно програмиране на CLR и Как да внедрим шаблона за проектиране IAsyncResult
  8. Пренасяне на модела на асинхронно програмиране към базирания на задачи асинхронен модел (APM в TAP):
    TPL и традиционно .NET асинхронно програмиране
    Взаимодействие с други асинхронни модели и типове

Актуализация: Благодаря ви майоровп и сидристий за идеята за добавяне на задача към интерфейса. Съвместимостта на библиотеката със стари операционни системи не е нарушена, т.к Четвъртата рамка поддържа както XP, така и 4 сървър.

Източник: www.habr.com

Добавяне на нов коментар