Implementació del protocol Reliable Udp per a .Net

Internet ha canviat fa molt de temps. Un dels principals protocols d'Internet - UDP és utilitzat per les aplicacions no només per lliurar datagrames i emissions, sinó també per proporcionar connexions "peer-to-peer" entre nodes de xarxa. A causa del seu disseny senzill, aquest protocol té molts usos abans no planificats, però, les mancances del protocol, com la manca d'entrega garantida, no han desaparegut enlloc. Aquest article descriu la implementació del protocol de lliurament garantit mitjançant UDP.
Contingut:Entrada
Requisits del protocol
Capçalera UDP fiable
Principis generals del protocol
Temps d'espera i temporitzadors de protocol
Diagrama d'estat de transmissió UDP fiable
Aprofundir en el codi. unitat de control de transmissió
Aprofundir en el codi. estats

Aprofundir en el codi. Creació i establiment de connexions
Aprofundir en el codi. Tancant la connexió en el temps d'espera
Aprofundir en el codi. Restauració de la transferència de dades
API UDP fiable
Conclusió
Enllaços i articles útils

Entrada

L'arquitectura original d'Internet suposava un espai d'adreces homogeni en el qual cada node tenia una adreça IP global i única i podia comunicar-se directament amb altres nodes. Ara Internet, de fet, té una arquitectura diferent: una àrea d'adreces IP globals i moltes àrees amb adreces privades amagades darrere dels dispositius NAT.En aquesta arquitectura, només els dispositius de l'espai d'adreces global poden comunicar-se fàcilment amb qualsevol persona de la xarxa perquè tenen una adreça IP única i encaminable globalment. Un node d'una xarxa privada es pot connectar a altres nodes de la mateixa xarxa i també es pot connectar a altres nodes coneguts de l'espai d'adreces global. Aquesta interacció s'aconsegueix en gran part gràcies al mecanisme de traducció d'adreces de xarxa. Els dispositius NAT, com ara els encaminadors Wi-Fi, creen entrades especials de la taula de traducció per a les connexions sortints i modifiquen les adreces IP i els números de port en paquets. Això permet connexions sortints de la xarxa privada als amfitrions de l'espai d'adreces global. Però al mateix temps, els dispositius NAT solen bloquejar tot el trànsit entrant tret que s'estableixin regles separades per a les connexions entrants.

Aquesta arquitectura d'Internet és prou correcta per a la comunicació client-servidor, on els clients poden estar en xarxes privades i els servidors tenen una adreça global. Però crea dificultats per a la connexió directa de dos nodes entre ells diversos xarxes privades. Una connexió directa entre dos nodes és important per a aplicacions peer-to-peer, com ara la transmissió de veu (Skype), l'accés remot a un ordinador (TeamViewer) o els jocs en línia.

Un dels mètodes més efectius per establir una connexió peer-to-peer entre dispositius de diferents xarxes privades s'anomena perforació. Aquesta tècnica s'utilitza més habitualment amb aplicacions basades en el protocol UDP.

Però si la vostra aplicació requereix un lliurament garantit de dades, per exemple, transferiu fitxers entre ordinadors, l'ús d'UDP tindrà moltes dificultats a causa del fet que UDP no és un protocol de lliurament garantit i no proporciona el lliurament de paquets en ordre, a diferència del TCP. protocol.

En aquest cas, per garantir el lliurament de paquets garantit, cal implementar un protocol de capa d'aplicació que proporcioni la funcionalitat necessària i funcioni a través d'UDP.

Vull assenyalar de seguida que hi ha una tècnica de perforació TCP per establir connexions TCP entre nodes de diferents xarxes privades, però a causa de la manca de suport per part de molts dispositius NAT, normalment no es considera com la forma principal de connectar-se. tals nodes.

Per a la resta d'aquest article, em centraré només en la implementació del protocol de lliurament garantit. La implementació de la tècnica de perforació UDP es descriurà als articles següents.

Requisits del protocol

  1. Lliurament de paquets fiable implementat mitjançant un mecanisme de retroalimentació positiva (l'anomenat reconeixement positiu)
  2. La necessitat d'una transferència eficient de big data, és a dir. el protocol ha d'evitar la retransmissió de paquets innecessària
  3. Hauria de ser possible cancel·lar el mecanisme de confirmació de lliurament (la capacitat de funcionar com un protocol UDP "pur").
  4. Possibilitat d'implementar el mode d'ordres, amb confirmació de cada missatge
  5. La unitat bàsica de transferència de dades a través del protocol ha de ser un missatge

Aquests requisits coincideixen en gran mesura amb els requisits del protocol de dades fiables descrits a 908 и 1151, i em vaig basar en aquests estàndards per desenvolupar aquest protocol.

Per entendre aquests requisits, mirem el temps de transferència de dades entre dos nodes de xarxa mitjançant els protocols TCP i UDP. Que en tots dos casos tindrem un paquet perdut.
Transferència de dades no interactives a través de TCP:Implementació del protocol Reliable Udp per a .Net

Com es pot veure al diagrama, en cas de pèrdua de paquet, TCP detectarà el paquet perdut i l'informarà al remitent demanant el número del segment perdut.
Transferència de dades mitjançant protocol UDP:Implementació del protocol Reliable Udp per a .Net

UDP no fa cap mesura de detecció de pèrdues. El control dels errors de transmissió en el protocol UDP és totalment responsabilitat de l'aplicació.

La detecció d'errors en el protocol TCP s'aconsegueix establint una connexió amb un node final, emmagatzemant l'estat d'aquesta connexió, indicant el nombre de bytes enviats a cada capçalera de paquet i notificant els rebuts mitjançant un número de confirmació.

A més, per millorar el rendiment (és a dir, enviar més d'un segment sense rebre un reconeixement), el protocol TCP utilitza l'anomenada finestra de transmissió, el nombre de bytes de dades que l'emissor del segment espera rebre.

Per obtenir més informació sobre el protocol TCP, vegeu 793, d'UDP a 768on, de fet, es defineixen.

A partir de l'anterior, és evident que per crear un protocol de lliurament de missatges fiable a través d'UDP (d'ara endavant anomenat UDP fiable), és necessari implementar mecanismes de transferència de dades similars a TCP. És a dir:

  • desar l'estat de connexió
  • utilitzar la numeració de segments
  • utilitzar paquets de confirmació especials
  • utilitzar un mecanisme de finestres simplificat per augmentar el rendiment del protocol

A més, necessiteu:

  • senyalar l'inici d'un missatge, per assignar recursos per a la connexió
  • senyaleu el final d'un missatge, per passar el missatge rebut a l'aplicació amunt i alliberar els recursos del protocol
  • permetre que el protocol específic de connexió desactivi el mecanisme de confirmació de lliurament per funcionar com a UDP "pur".

Capçalera UDP fiable

Recordeu que un datagrama UDP està encapsulat en un datagrama IP. El paquet UDP fiable s'"embolica" adequadament en un datagrama UDP.
Encapsulació de capçalera UDP fiable:Implementació del protocol Reliable Udp per a .Net

L'estructura de la capçalera UDP fiable és bastant simple:

Implementació del protocol Reliable Udp per a .Net

  • Banderes: banderes de control de paquets
  • MessageType: tipus de missatge utilitzat per les aplicacions aigües amunt per subscriure's a missatges específics
  • TransmissionId: el número de la transmissió, juntament amb l'adreça i el port del destinatari, identifica de manera única la connexió
  • PacketNumber - número de paquet
  • Opcions: opcions de protocol addicionals. En el cas del primer paquet, s'utilitza per indicar la mida del missatge

Les banderes són les següents:

  • FirstPacket: el primer paquet del missatge
  • NoAsk: el missatge no requereix que s'hagi activat un mecanisme de reconeixement
  • LastPacket: l'últim paquet del missatge
  • RequestForPacket: paquet de confirmació o sol·licitud d'un paquet perdut

Principis generals del protocol

Com que Reliable UDP se centra en la transmissió de missatges garantida entre dos nodes, ha de poder establir una connexió amb l'altre costat. Per establir una connexió, el remitent envia un paquet amb el senyalador FirstPacket, la resposta a la qual significarà que la connexió s'ha establert. Tots els paquets de resposta, o, en altres paraules, els paquets de reconeixement, sempre estableixen el valor del camp PacketNumber en un més que el valor PacketNumber més gran dels paquets rebuts correctament. El camp Opcions del primer paquet enviat és la mida del missatge.

S'utilitza un mecanisme similar per finalitzar una connexió. La marca LastPacket s'estableix a l'últim paquet del missatge. En el paquet de resposta, s'indica el número de l'últim paquet + 1, que per a la part receptora significa lliurament satisfactori del missatge.
Esquema d'establiment i terminació de la connexió:Implementació del protocol Reliable Udp per a .Net

Quan s'estableix la connexió, comença la transferència de dades. Les dades es transmeten en blocs de paquets. Cada bloc, excepte l'últim, conté un nombre fix de paquets. És igual a la mida de la finestra de recepció/transmissió. L'últim bloc de dades pot tenir menys paquets. Després d'enviar cada bloc, la part remitent espera una confirmació de lliurament o una sol·licitud per tornar a lliurar els paquets perduts, deixant oberta la finestra de recepció/transmissió per rebre respostes. Després de rebre la confirmació del lliurament del bloc, la finestra de recepció/transmissió canvia i s'envia el següent bloc de dades.

El costat receptor rep els paquets. Cada paquet es revisa per veure si es troba dins de la finestra de transmissió. Els paquets i duplicats que no cauen a la finestra es filtren. Perquè Si la mida de la finestra és fixa i la mateixa per al destinatari i l'emissor, en el cas d'un bloc de paquets que s'entrega sense pèrdua, la finestra es desplaça per rebre els paquets del següent bloc de dades i es mostra una confirmació de lliurament. enviat. Si la finestra no s'omple dins el termini fixat pel temporitzador de treball, s'iniciarà una comprovació de quins paquets no s'han lliurat i s'enviaran les sol·licituds de reentrega.
Diagrama de retransmissió:Implementació del protocol Reliable Udp per a .Net

Temps d'espera i temporitzadors de protocol

Hi ha diversos motius pels quals no es pot establir una connexió. Per exemple, si la part receptora està fora de línia. En aquest cas, quan s'intenta establir una connexió, la connexió es tancarà per temps d'espera. La implementació de Reliable UDP utilitza dos temporitzadors per establir temps d'espera. El primer, el temporitzador de treball, s'utilitza per esperar una resposta de l'amfitrió remot. Si es dispara al costat del remitent, l'últim paquet enviat es torna a enviar. Si el temporitzador caduca al destinatari, es realitza una comprovació de paquets perduts i s'envien peticions de tornada.

El segon temporitzador és necessari per tancar la connexió en cas de manca de comunicació entre els nodes. Per al remitent, comença immediatament després que el temporitzador de treball expiri i espera una resposta del node remot. Si no hi ha resposta dins del període especificat, la connexió s'acaba i s'alliberen recursos. Per al costat receptor, el temporitzador de tancament de la connexió s'inicia després que el temporitzador de treball caduqui dues vegades. Això és necessari per assegurar-se contra la pèrdua del paquet de confirmació. Quan el temporitzador expira, la connexió també s'acaba i s'alliberen recursos.

Diagrama d'estat de transmissió UDP fiable

Els principis del protocol s'implementen en una màquina d'estats finits, cada estat de la qual és responsable d'una determinada lògica de processament de paquets.
Diagrama d'estat UDP fiable:

Implementació del protocol Reliable Udp per a .Net

Tancat - no és realment un estat, és un punt d'inici i final per a l'autòmat. Per estat Tancat es rep un bloc de control de transmissió que, implementant un servidor UDP asíncron, reenvia els paquets a les connexions adequades i inicia el processament de l'estat.

Enviament del primer paquet – l'estat inicial en què es troba la connexió de sortida quan s'envia el missatge.

En aquest estat, s'envia el primer paquet per als missatges normals. Per als missatges sense confirmació d'enviament, aquest és l'únic estat on s'envia tot el missatge.

SendingCycle – estat fonamental per a la transmissió de paquets de missatges.

Transició a ella des de l'estat Enviament del primer paquet realitzat després que s'hagi enviat el primer paquet del missatge. És en aquest estat on arriben tots els reconeixements i peticions de retransmissions. La sortida és possible en dos casos: en cas de lliurament satisfactori del missatge o en cas d'espera.

Primer paquet rebut – l'estat inicial del destinatari del missatge.

Comprova la correcció de l'inici de la transmissió, crea les estructures necessàries i envia un justificant de recepció del primer paquet.

Per a un missatge que consta d'un sol paquet i s'ha enviat sense utilitzar prova de lliurament, aquest és l'únic estat. Després de processar aquest missatge, la connexió es tanca.

Muntatge – estat bàsic per rebre paquets de missatges.

Escriu paquets a l'emmagatzematge temporal, comprova si hi ha pèrdues de paquets, envia justificants per al lliurament d'un bloc de paquets i tot el missatge i envia sol·licituds per tornar a lliurar els paquets perduts. En cas de rebre correctament tot el missatge, la connexió passa a l'estat Completed, en cas contrari, surt un temps d'espera.

Completed – tancar la connexió en cas de rebre correctament tot el missatge.

Aquest estat és necessari per al muntatge del missatge i per al cas en què la confirmació de lliurament del missatge s'ha perdut en el camí cap al remitent. S'ha sortit d'aquest estat amb un temps d'espera, però la connexió es considera tancada correctament.

Aprofundir en el codi. unitat de control de transmissió

Un dels elements clau de Reliable UDP és el bloc de control de transmissió. La tasca d'aquest bloc és emmagatzemar les connexions actuals i els elements auxiliars, distribuir els paquets entrants a les connexions corresponents, proporcionar una interfície per enviar paquets a una connexió i implementar el protocol API. El bloc de control de transmissió rep paquets de la capa UDP i els reenvia a la màquina d'estats per processar-los. Per rebre paquets, implementa un servidor UDP asíncron.
Alguns membres de la classe 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;    	
  //...
}

Implementació del servidor UDP asíncron:

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

Per a cada transferència de missatges, es crea una estructura que conté informació sobre la connexió. Aquesta estructura s'anomena registre de connexió.
Alguns membres de la classe 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;
  //...
}

Aprofundir en el codi. estats

Els estats implementen la màquina d'estats del protocol Reliable UDP, on té lloc el processament principal dels paquets. La classe abstracta ReliableUdpState proporciona una interfície per a l'estat:

Implementació del protocol Reliable Udp per a .Net

Tota la lògica del protocol està implementada per les classes presentades anteriorment, juntament amb una classe auxiliar que proporciona mètodes estàtics, com, per exemple, la construcció de la capçalera ReliableUdp a partir del registre de connexió.

A continuació, considerarem en detall la implementació dels mètodes d'interfície que determinen els algorismes bàsics del protocol.

Mètode DisposeByTimeout

El mètode DisposeByTimeout és responsable d'alliberar els recursos de connexió després d'un temps d'espera i de senyalitzar l'enviament de missatges amb èxit o sense èxit.
ReliableUdpState.DisposeByTimeout:

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

Només està anul·lat a l'estat Completed.
Completed.DisposeByTimeout:

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

Mètode ProcessPackets

El mètode ProcessPackets és responsable del processament addicional d'un paquet o paquets. Trucada directament o mitjançant un temporitzador d'espera de paquets.

En condicions Muntatge el mètode està anul·lat i s'encarrega de comprovar els paquets perduts i de fer la transició a l'estat Completed, en cas de rebre l'últim paquet i passar una verificació correcta
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);
  }
}

En condicions SendingCycle aquest mètode només es crida amb un temporitzador i s'encarrega de tornar a enviar l'últim missatge, així com d'habilitar el temporitzador de tancament de la connexió.
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);
}

En condicions Completed el mètode atura el temporitzador en execució i envia el missatge als subscriptors.
Completed.ProcessPackets:

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

Mètode ReceivePacket

En condicions Primer paquet rebut la tasca principal del mètode és determinar si el primer paquet de missatges va arribar realment a la interfície, i també recollir un missatge que consta d'un sol paquet.
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);
  }
}

En condicions SendingCycle aquest mètode s'invalida per acceptar els reconeixements de lliurament i les sol·licituds de retransmissió.
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));
}

En condicions Muntatge en el mètode ReceivePacket, té lloc el treball principal d'ensamblar un missatge a partir dels paquets entrants.
Muntatge.Paquet de recepció:

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

En condicions Completed l'única tasca del mètode és enviar un reconeixement de l'entrega correcta del missatge.
Completed.ReceivePacket:

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

Mètode d'enviament de paquets

En condicions Enviament del primer paquet aquest mètode envia el primer paquet de dades, o si el missatge no requereix confirmació de lliurament, tot el missatge.
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);
}

En condicions SendingCycle en aquest mètode, s'envia un bloc de paquets.
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 );
  }
}

Aprofundir en el codi. Creació i establiment de connexions

Ara que hem vist els estats bàsics i els mètodes utilitzats per gestionar els estats, desglossem uns quants exemples de com funciona el protocol amb una mica més de detall.
Diagrama de transmissió de dades en condicions normals:Implementació del protocol Reliable Udp per a .Net

Considereu en detall la creació registre de connexió per connectar i enviar el primer paquet. La transferència sempre l'inicia l'aplicació que crida a l'API d'enviament de missatges. A continuació, s'invoca el mètode StartTransmission del bloc de control de transmissió, que inicia la transmissió de dades per al missatge nou.
Creació d'una connexió de sortida:

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

Enviament del primer paquet (estat 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);
}

Després d'enviar el primer paquet, el remitent entra a l'estat SendingCycle - Espereu la confirmació del lliurament del paquet.
El costat receptor, utilitzant el mètode EndReceive, rep el paquet enviat i en crea un nou registre de connexió i passa aquest paquet, amb una capçalera analitzada prèviament, al mètode ReceivePacket de l'estat per processar-lo Primer paquet rebut
Creació d'una connexió al costat receptor:

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

Recepció del primer paquet i enviament d'un reconeixement (estat 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);
  }
}

Aprofundir en el codi. Tancant la connexió en el temps d'espera

La gestió del temps d'espera és una part important de Reliable UDP. Penseu en un exemple en què un node intermedi va fallar i el lliurament de dades en ambdues direccions es va fer impossible.
Diagrama per tancar una connexió per temps d'espera:Implementació del protocol Reliable Udp per a .Net

Com es pot veure al diagrama, el temporitzador de treball del remitent comença immediatament després d'enviar un bloc de paquets. Això passa en el mètode SendPacket de l'estat SendingCycle.
Habilitació del temporitzador de treball (estat SendingCycle):

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

Els períodes de temporitzadors s'estableixen quan es crea la connexió. El ShortTimerPeriod predeterminat és de 5 segons. A l'exemple, s'estableix en 1,5 segons.

Per a una connexió entrant, el temporitzador s'inicia després de rebre l'últim paquet de dades entrant, això passa amb el mètode ReceivePacket de l'estat Muntatge
Habilitació del temporitzador de treball (estat de muntatge):

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

No van arribar més paquets a la connexió entrant mentre s'esperava el temporitzador de treball. El temporitzador es va apagar i va cridar el mètode ProcessPackets, on es van trobar els paquets perduts i es van enviar per primera vegada les sol·licituds de reentrega.
Enviament de sol·licituds de reentrega (estat de muntatge):

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

La variable TimerSecondTry està establerta a veritable. Aquesta variable s'encarrega de reiniciar el temporitzador de treball.

Per part del remitent, també s'activa el temporitzador de treball i es torna a enviar l'últim paquet enviat.
Activació del temporitzador de tancament de la connexió (estat SendingCycle):

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

Després d'això, el temporitzador de tancament de la connexió s'inicia a la connexió de sortida.
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);
}

El temps d'espera del temporitzador de tancament de la connexió és de 30 segons de manera predeterminada.

Després d'un breu temps, el temporitzador de treball del costat del destinatari es torna a disparar, les sol·licituds es tornen a enviar, després del qual s'inicia el temporitzador de tancament de la connexió per a la connexió entrant.

Quan s'activen els temporitzadors de tancament, s'alliberen tots els recursos dels dos registres de connexió. El remitent informa de l'error de lliurament a l'aplicació amunt (vegeu API UDP fiable).
Alliberació de recursos de registre de connexió:

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

Aprofundir en el codi. Restauració de la transferència de dades

Diagrama de recuperació de transmissió de dades en cas de pèrdua de paquets:Implementació del protocol Reliable Udp per a .Net

Com ja s'ha comentat en tancar la connexió en el temps d'espera, quan el temporitzador de treball expiri, el receptor comprovarà si hi ha paquets perduts. En cas de pèrdua de paquets, s'elaborarà una llista del nombre de paquets que no han arribat al destinatari. Aquests números s'introdueixen a la matriu LostPackets d'una connexió específica i s'envien sol·licituds de reentrega.
Enviament de sol·licituds per tornar a lliurar paquets (estat de muntatge):

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

El remitent acceptarà la sol·licitud de reentrega i enviarà els paquets que falten. Val a dir que en aquest moment l'emissor ja ha iniciat el temporitzador de tancament de la connexió i, quan es rep una sol·licitud, es reinicia.
Tornant a enviar paquets perduts (estat SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

El paquet de reenviament (paquet #3 al diagrama) el rep la connexió entrant. Es fa una comprovació per veure si la finestra de recepció està plena i es restableix la transmissió normal de dades.
Comprovació de visites a la finestra de recepció (estat de muntatge):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API UDP fiable

Per interactuar amb el protocol de transferència de dades, hi ha una classe Udp fiable oberta, que és un embolcall sobre el bloc de control de transferència. Aquests són els membres més importants de la classe:

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

Els missatges es reben per subscripció. Signatura del delegat per al mètode de devolució de trucada:

public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );

Missatge:

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

Per subscriure's a un tipus de missatge específic i/o a un remitent específic, s'utilitzen dos paràmetres opcionals: ReliableUdpMessageTypes messageType i IPEndPoint ipEndPoint.

Tipus de missatges:

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

El missatge s'envia de manera asíncrona; per a això, el protocol implementa un model de programació asíncrona:

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

El resultat de l'enviament d'un missatge serà cert, si el missatge ha arribat correctament al destinatari i fals, si la connexió s'ha tancat en el temps d'espera:

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusió

No s'ha descrit gaire en aquest article. Mecanismes de concordança de fils, maneig d'excepcions i errors, implementació de mètodes d'enviament de missatges asíncrons. Però el nucli del protocol, la descripció de la lògica per processar paquets, establir una connexió i gestionar els temps d'espera, haurien de ser clars.

La versió demostrada del protocol de lliurament fiable és prou robust i flexible per satisfer els requisits definits anteriorment. Però vull afegir que la implementació descrita es pot millorar. Per exemple, per augmentar el rendiment i canviar dinàmicament els períodes de temporitzador, es poden afegir mecanismes com la finestra lliscant i el RTT al protocol, també serà útil implementar un mecanisme per determinar MTU entre nodes de connexió (però només si s'envien missatges grans). .

Gràcies per la vostra atenció, espero els vostres comentaris i comentaris.

PS Per a aquells que estiguin interessats en els detalls o simplement vulguin provar el protocol, l'enllaç al projecte a GitHube:
Projecte UDP fiable

Enllaços i articles útils

  1. Especificació del protocol TCP: en anglès и en rus
  2. Especificació del protocol UDP: en anglès и en rus
  3. Discussió del protocol RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Protocol de dades fiables: 908 и 1151
  5. Una implementació senzilla de la confirmació de lliurament a través d'UDP: Preneu el control total de les vostres xarxes amb .NET i UDP
  6. Article que descriu els mecanismes de travessa NAT: Comunicació peer-to-peer a través dels traductors d'adreces de xarxa
  7. Implementació del model de programació asíncrona: Implementació del model de programació asíncrona CLR и Com implementar el patró de disseny IAsyncResult
  8. Portar el model de programació asíncrona al patró asíncron basat en tasques (APM a TAP):
    Programació asíncrona TPL i .NET tradicional
    Interoperabilitat amb altres patrons i tipus asíncrons

Actualització: Gràcies majorovp и sidristij per la idea d'afegir una tasca a la interfície. La compatibilitat de la biblioteca amb sistemes operatius antics no es viola, perquè El quart marc admet tant el servidor XP com el 4.

Font: www.habr.com

Afegeix comentari