Implantación do protocolo Reliable Udp para .Net

Internet cambiou hai moito tempo. Un dos principais protocolos de Internet - UDP é usado polas aplicacións non só para entregar datagramas e transmisións, senón tamén para proporcionar conexións "peer-to-peer" entre nós de rede. Debido ao seu deseño sinxelo, este protocolo ten moitos usos non previstos antes, non obstante, as deficiencias do protocolo, como a falta de garantía de entrega, non desapareceron por ningures. Este artigo describe a implementación do protocolo de entrega garantida a través de UDP.
Contido:Entrada
Requisitos do protocolo
Cabeceira UDP fiable
Principios xerais do protocolo
Tempos de espera e temporizadores de protocolo
Diagrama de estado de transmisión UDP fiable
Máis profundo no código. Unidade de control de transmisión
Máis profundo no código. estados

Máis profundo no código. Creación e establecemento de conexións
Máis profundo no código. Pechando a conexión no tempo de espera
Máis profundo no código. Restauración da transferencia de datos
API UDP fiable
Conclusión
Ligazóns e artigos útiles

Entrada

A arquitectura orixinal de Internet asumiu un espazo de enderezos homoxéneo no que cada nodo tiña un enderezo IP global e único e podía comunicarse directamente con outros nodos. Agora Internet, de feito, ten unha arquitectura diferente: unha área de enderezos IP globais e moitas áreas con enderezos privados escondidos detrás dos dispositivos NAT.Nesta arquitectura, só os dispositivos do espazo de enderezos global poden comunicarse facilmente con calquera persoa da rede porque teñen un enderezo IP único e enrutable globalmente. Un nodo dunha rede privada pode conectarse a outros nodos da mesma rede e tamén se pode conectar a outros nodos coñecidos no espazo de enderezos global. Esta interacción conséguese en gran parte debido ao mecanismo de tradución de enderezos de rede. Os dispositivos NAT, como os enrutadores Wi-Fi, crean entradas especiais de táboa de tradución para as conexións de saída e modifican os enderezos IP e os números de porto en paquetes. Isto permite conexións de saída desde a rede privada a hosts no espazo de enderezos global. Pero ao mesmo tempo, os dispositivos NAT adoitan bloquear todo o tráfico entrante a menos que se establezan regras separadas para as conexións entrantes.

Esta arquitectura de Internet é o suficientemente correcta para a comunicación cliente-servidor, onde os clientes poden estar en redes privadas e os servidores teñen un enderezo global. Pero crea dificultades para a conexión directa de dous nodos entre eles varios redes privadas. Unha conexión directa entre dous nodos é importante para aplicacións peer-to-peer como a transmisión de voz (Skype), o acceso remoto a un ordenador (TeamViewer) ou os xogos en liña.

Un dos métodos máis eficaces para establecer unha conexión peer-to-peer entre dispositivos en diferentes redes privadas chámase perforación. Esta técnica úsase máis habitualmente con aplicacións baseadas no protocolo UDP.

Pero se a súa aplicación precisa de entrega garantida de datos, por exemplo, transfire ficheiros entre ordenadores, entón o uso de UDP terá moitas dificultades debido ao feito de que UDP non é un protocolo de entrega garantido e non proporciona a entrega de paquetes en orde, a diferenza do TCP. protocolo.

Neste caso, para garantir a entrega de paquetes garantida, é necesario implementar un protocolo de capa de aplicación que proporcione a funcionalidade necesaria e funcione sobre UDP.

Quero notar de inmediato que existe unha técnica de perforación de buratos TCP para establecer conexións TCP entre nodos de diferentes redes privadas, pero debido á falta de compatibilidade con ela por parte de moitos dispositivos NAT, normalmente non se considera a forma principal de conectarse. tales nodos.

Para o resto deste artigo, centrareime só na implementación do protocolo de entrega garantida. A implementación da técnica de perforación UDP describirase nos seguintes artigos.

Requisitos do protocolo

  1. Entrega de paquetes fiable implementada mediante un mecanismo de retroalimentación positiva (o chamado recoñecemento positivo)
  2. A necesidade dunha transferencia eficiente de big data, é dicir. o protocolo debe evitar a retransmisión de paquetes innecesaria
  3. Debería ser posible cancelar o mecanismo de confirmación de entrega (a capacidade de funcionar como un protocolo UDP "puro").
  4. Capacidade de implementar o modo de comando, con confirmación de cada mensaxe
  5. A unidade básica de transferencia de datos a través do protocolo debe ser unha mensaxe

Estes requisitos coinciden en gran medida cos requisitos do protocolo de datos fiables descritos en Frase 908 и Frase 1151, e confiei neses estándares ao desenvolver este protocolo.

Para comprender estes requisitos, vexamos o momento da transferencia de datos entre dous nodos de rede usando os protocolos TCP e UDP. Que nos dous casos teremos un paquete perdido.
Transferencia de datos non interactivos a través de TCP:Implantación do protocolo Reliable Udp para .Net

Como podes ver no diagrama, en caso de perda de paquetes, TCP detectará o paquete perdido e informarao ao remitente pedindo o número do segmento perdido.
Transferencia de datos mediante protocolo UDP:Implantación do protocolo Reliable Udp para .Net

UDP non toma ningún paso de detección de perdas. O control dos erros de transmisión no protocolo UDP é totalmente responsabilidade da aplicación.

A detección de erros no protocolo TCP conséguese establecendo unha conexión cun nodo final, almacenando o estado desa conexión, indicando o número de bytes enviados en cada cabeceira de paquete e notificando os recibos mediante un número de acuse de recibo.

Ademais, para mellorar o rendemento (é dicir, enviar máis dun segmento sen recibir un acuse de recibo), o protocolo TCP usa a chamada xanela de transmisión, o número de bytes de datos que o remitente do segmento espera recibir.

Para obter máis información sobre o protocolo TCP, consulte Frase 793, de UDP a Frase 768onde, de feito, se definen.

Polo anterior, é evidente que para crear un protocolo de entrega de mensaxes fiable sobre UDP (en diante denominado como UDP fiable), é necesario implementar mecanismos de transferencia de datos similares ao TCP. A saber:

  • gardar o estado da conexión
  • utilizar a numeración de segmentos
  • use paquetes de confirmación especiais
  • utilizar un mecanismo de ventás simplificado para aumentar o rendemento do protocolo

Ademais, necesitas:

  • sinalar o inicio dunha mensaxe, para asignar recursos para a conexión
  • sinalar o final dunha mensaxe, para pasar a mensaxe recibida á aplicación e liberar recursos do protocolo
  • permitir que o protocolo específico da conexión desactive o mecanismo de confirmación de entrega para funcionar como UDP "puro".

Cabeceira UDP fiable

Lembre que un datagrama UDP está encapsulado nun datagrama IP. O paquete Reliable UDP está apropiadamente "envolto" nun datagrama UDP.
Encapsulación de cabeceira UDP fiable:Implantación do protocolo Reliable Udp para .Net

A estrutura da cabeceira UDP fiable é bastante sinxela:

Implantación do protocolo Reliable Udp para .Net

  • Bandeiras - bandeiras de control de paquetes
  • MessageType: tipo de mensaxe que usan as aplicacións anteriores para subscribirse a mensaxes específicas
  • TransmissionId: o número da transmisión, xunto co enderezo e o porto do destinatario, identifican de forma única a conexión
  • PacketNumber - número de paquete
  • Opcións: opcións de protocolo adicionais. No caso do primeiro paquete, utilízase para indicar o tamaño da mensaxe

As bandeiras son as seguintes:

  • FirstPacket - o primeiro paquete da mensaxe
  • NoAsk: a mensaxe non require que se active un mecanismo de recoñecemento
  • LastPacket - o último paquete da mensaxe
  • RequestForPacket: paquete de confirmación ou solicitude de paquete perdido

Principios xerais do protocolo

Dado que Reliable UDP está enfocado na transmisión de mensaxes garantida entre dous nodos, debe poder establecer unha conexión co outro lado. Para establecer unha conexión, o remitente envía un paquete coa marca FirstPacket, cuxa resposta significará que a conexión está establecida. Todos os paquetes de resposta, ou, noutras palabras, os paquetes de recoñecemento, sempre establecen o valor do campo PacketNumber en un máis que o maior valor PacketNumber dos paquetes recibidos con éxito. O campo Opcións para o primeiro paquete enviado é o tamaño da mensaxe.

Un mecanismo similar úsase para finalizar unha conexión. A marca LastPacket está establecida no último paquete da mensaxe. No paquete de resposta, indícase o número do último paquete + 1, o que para o lado receptor significa a entrega exitosa da mensaxe.
Esquema de establecemento e terminación da conexión:Implantación do protocolo Reliable Udp para .Net

Cando se establece a conexión, comeza a transferencia de datos. Os datos transmítense en bloques de paquetes. Cada bloque, excepto o último, contén un número fixo de paquetes. É igual ao tamaño da xanela de recepción/transmisión. O último bloque de datos pode ter menos paquetes. Despois de enviar cada bloque, o lado remitente espera unha confirmación de entrega ou unha solicitude para volver entregar os paquetes perdidos, deixando aberta a xanela de recepción/transmisión para recibir respostas. Despois de recibir a confirmación da entrega do bloque, a xanela de recepción/transmisión cambia e envíase o seguinte bloque de datos.

O lado receptor recibe os paquetes. Cada paquete é verificado para ver se cae dentro da xanela de transmisión. Os paquetes e duplicados que non caen na xanela son filtrados. Porque Se o tamaño da xanela é fixo e o mesmo para o destinatario e o remitente, entón no caso de que un bloque de paquetes se entregue sen perda, a xanela desprázase para recibir os paquetes do seguinte bloque de datos e unha confirmación de entrega. enviado. Se a xanela non se enche no prazo fixado polo temporizador de traballo, iniciarase unha comprobación dos paquetes que non foron entregados e enviaranse solicitudes de reentrega.
Diagrama de retransmisión:Implantación do protocolo Reliable Udp para .Net

Tempos de espera e temporizadores de protocolo

Hai varias razóns polas que non se pode establecer unha conexión. Por exemplo, se o destinatario está fóra de liña. Neste caso, ao tentar establecer unha conexión, a conexión pecharase no tempo de espera. A implementación de Reliable UDP usa dous temporizadores para establecer os tempos de espera. O primeiro, o temporizador de traballo, úsase para esperar unha resposta do host remoto. Se se dispara no lado do remitente, entón o último paquete enviado será enviado de novo. Se o temporizador caduca no destinatario, realízase unha comprobación de paquetes perdidos e envíanse solicitudes de reenvío.

O segundo temporizador é necesario para pechar a conexión en caso de falta de comunicación entre os nodos. Para o lado do remitente, comeza inmediatamente despois de que caduque o temporizador de traballo e agarda unha resposta do nodo remoto. Se non hai resposta no período especificado, a conexión finaliza e liberaranse os recursos. Para o lado receptor, o temporizador de peche da conexión iníciase despois de que o temporizador de traballo caduque dúas veces. Isto é necesario para asegurarse contra a perda do paquete de confirmación. Cando caduca o temporizador, a conexión tamén se finaliza e os recursos son liberados.

Diagrama de estado de transmisión UDP fiable

Os principios do protocolo están implementados nunha máquina de estados finitos, cada un dos cales é responsable dunha determinada lóxica de procesamento de paquetes.
Diagrama de estado UDP fiable:

Implantación do protocolo Reliable Udp para .Net

Pechado - non é realmente un estado, é un punto de inicio e fin para o autómata. Para o estado Pechado recíbese un bloque de control de transmisión que, implementando un servidor UDP asíncrono, reenvía os paquetes ás conexións adecuadas e inicia o procesamento do estado.

Primeiro envío de paquetes – o estado inicial no que se atopa a conexión de saída cando se envía a mensaxe.

Neste estado, envíase o primeiro paquete para as mensaxes normais. Para as mensaxes sen confirmación de envío, este é o único estado onde se envía a mensaxe completa.

Ciclo de envío – estado fundamental para a transmisión de paquetes de mensaxes.

Transición a ela dende o Estado Primeiro envío de paquetes realizada despois de enviar o primeiro paquete da mensaxe. Neste estado chegan todos os recoñecementos e solicitudes de retransmisións. É posible saír dela en dous casos: en caso de entrega satisfactoria da mensaxe ou en caso de espera.

Primeiro paquete recibido – o estado inicial do destinatario da mensaxe.

Comproba a corrección do inicio da transmisión, crea as estruturas necesarias e envía un acuse de recibo do primeiro paquete.

Para unha mensaxe que consta dun único paquete e que se enviou sen utilizar unha proba de entrega, este é o único estado. Despois de procesar tal mensaxe, a conexión péchase.

Montaxe – estado básico para recibir paquetes de mensaxes.

Escribe paquetes no almacenamento temporal, comproba a perda de paquetes, envía acuses de recibo para a entrega dun bloque de paquetes e a mensaxe completa e envía solicitudes de reenvío de paquetes perdidos. No caso de recibir con éxito a mensaxe completa, a conexión pasa ao estado Feito, se non, sae un tempo de espera.

Feito – pechar a conexión no caso de recibir con éxito a mensaxe completa.

Este estado é necesario para a montaxe da mensaxe e para o caso de perderse a confirmación de entrega da mensaxe no camiño cara ao remitente. Saíse deste estado por un tempo de espera, pero a conexión considérase pechada correctamente.

Máis profundo no código. Unidade de control de transmisión

Un dos elementos clave de Reliable UDP é o bloque de control de transmisión. A tarefa deste bloque é almacenar as conexións actuais e os elementos auxiliares, distribuír os paquetes entrantes ás conexións correspondentes, proporcionar unha interface para enviar paquetes a unha conexión e implementar a API do protocolo. O bloque de control de transmisión recibe paquetes da capa UDP e envíaos á máquina de estado para procesalos. Para recibir paquetes, implementa un servidor UDP asíncrono.
Algúns membros da clase 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ón de servidor UDP asíncrono:

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

Para cada transferencia de mensaxes, créase unha estrutura que contén información sobre a conexión. A tal estrutura chámase rexistro de conexión.
Algúns membros da clase 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;
  //...
}

Máis profundo no código. estados

Os estados implementan a máquina de estados do protocolo Reliable UDP, onde ten lugar o procesamento principal dos paquetes. A clase abstracta ReliableUdpState proporciona unha interface para o estado:

Implantación do protocolo Reliable Udp para .Net

Toda a lóxica do protocolo está implementada polas clases presentadas anteriormente, xunto cunha clase auxiliar que proporciona métodos estáticos, como, por exemplo, construír a cabeceira ReliableUdp a partir do rexistro de conexión.

A continuación, consideraremos en detalle a implementación dos métodos de interface que determinan os algoritmos básicos do protocolo.

Método DisposeByTimeout

O método DisposeByTimeout é o responsable de liberar os recursos de conexión despois dun tempo de espera e de sinalar a entrega de mensaxes exitosa ou non.
ReliableUdpState.DisposeByTimeout:

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

Só se anula no estado Feito.
Completado.DisposeByTimeout:

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

Método ProcessPackets

O método ProcessPackets é responsable do procesamento adicional dun ou dos paquetes. Chamado directamente ou mediante un temporizador de espera de paquetes.

Capaz Montaxe o método é anulado e encárgase de comprobar os paquetes perdidos e facer a transición ao estado Feito, en caso de recibir o último paquete e pasar unha comprobación exitosa
Paquetes de proceso de montaxe:

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

Capaz Ciclo de envío este método chámase só nun temporizador, e encárgase de reenviar a última mensaxe, así como de activar o temporizador de peche da conexión.
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);
}

Capaz Feito o método detén o temporizador en execución e envía a mensaxe aos subscritores.
Completado.ProcessPackets:

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

Método ReceivePacket

Capaz Primeiro paquete recibido a principal tarefa do método é determinar se o primeiro paquete de mensaxes chegou realmente á interface e tamén recoller unha mensaxe consistente nun único paquete.
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);
  }
}

Capaz Ciclo de envío este método se anula para aceptar acuses de recibo de entrega e solicitudes de retransmisión.
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));
}

Capaz Montaxe no método ReceivePacket, ten lugar o traballo principal de ensamblar unha mensaxe a partir de paquetes entrantes.
Montaxe.Paquete de recepción:

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

Capaz Feito a única tarefa do método é enviar un novo recoñecemento da entrega exitosa da mensaxe.
Completed.ReceivePacket:

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

Método de envío de paquetes

Capaz Primeiro envío de paquetes este método envía o primeiro paquete de datos ou, se a mensaxe non require confirmación de entrega, a mensaxe completa.
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);
}

Capaz Ciclo de envío neste método, envíase un bloque de paquetes.
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 );
  }
}

Máis profundo no código. Creación e establecemento de conexións

Agora que vimos os estados básicos e os métodos utilizados para manexar os estados, imos desglosar algúns exemplos de como funciona o protocolo cun pouco máis de detalle.
Diagrama de transmisión de datos en condicións normais:Implantación do protocolo Reliable Udp para .Net

Considere en detalle a creación rexistro de conexión para conectarse e enviar o primeiro paquete. A transferencia sempre é iniciada pola aplicación que chama á API de envío de mensaxes. A continuación, invócase o método StartTransmission do bloque de control de transmisión, que inicia a transmisión de datos para a nova mensaxe.
Creando unha conexión de saída:

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

Envío do primeiro paquete (estado 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);
}

Despois de enviar o primeiro paquete, o remitente entra no estado Ciclo de envío - agarde a confirmación da entrega do paquete.
O lado receptor, usando o método EndReceive, recibe o paquete enviado, crea un novo rexistro de conexión e pasa este paquete, cunha cabeceira previamente analizada, ao método ReceivePacket do estado para procesar Primeiro paquete recibido
Creando unha conexión no lado 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);
}

Recibindo o primeiro paquete e enviando un acuse de recibo (estado 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);
  }
}

Máis profundo no código. Pechando a conexión no tempo de espera

O manexo do tempo de espera é unha parte importante de Reliable UDP. Considere un exemplo no que un nodo intermedio fallou e a entrega de datos en ambas direccións se fixo imposible.
Diagrama para pechar unha conexión por tempo de espera:Implantación do protocolo Reliable Udp para .Net

Como se pode ver no diagrama, o temporizador de traballo do remitente comeza inmediatamente despois de enviar un bloque de paquetes. Isto ocorre no método SendPacket do estado Ciclo de envío.
Activando o temporizador de traballo (estado SendingCycle):

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

Os períodos do temporizador establécense cando se crea a conexión. O ShortTimerPeriod predeterminado é de 5 segundos. No exemplo, establécese en 1,5 segundos.

Para unha conexión entrante, o temporizador comeza despois de recibir o último paquete de datos entrante, isto ocorre no método ReceivePacket do estado Montaxe
Activación do temporizador de traballo (estado de montaxe):

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

Non chegaron máis paquetes á conexión entrante mentres se esperaba o temporizador de traballo. O temporizador apagouse e chamou ao método ProcessPackets, onde se atoparon os paquetes perdidos e as solicitudes de reenvío foron enviadas por primeira vez.
Envío de solicitudes de reentrega (estado de montaxe):

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

A variable TimerSecondTry está definida como certo. Esta variable encárgase de reiniciar o temporizador de traballo.

Por parte do remitente, o temporizador de traballo tamén se activa e o último paquete enviado é reenviado.
Activando o temporizador de peche da conexión (estado SendingCycle):

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

Despois diso, o temporizador de peche da conexión comeza na conexión de saída.
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);
}

O tempo de espera do temporizador de peche da conexión é de 30 segundos por defecto.

Despois dun curto período de tempo, o temporizador de traballo no lado do destinatario volve dispararse, as solicitudes envíanse de novo, despois de que comeza o temporizador de peche da conexión para a conexión entrante

Cando se disparan os temporizadores de peche, todos os recursos de ambos os rexistros de conexión son liberados. O remitente informa do fallo de entrega á aplicación upstream (consulte API UDP fiable).
Liberando recursos de rexistro de conexión:

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

Máis profundo no código. Restauración da transferencia de datos

Diagrama de recuperación de transmisión de datos en caso de perda de paquetes:Implantación do protocolo Reliable Udp para .Net

Como xa se comentou ao pechar a conexión no tempo de espera, cando caduque o temporizador de traballo, o receptor comprobará se hai paquetes perdidos. En caso de perda de paquetes, compilarase unha lista do número de paquetes que non chegaron ao destinatario. Estes números introdúcense na matriz LostPackets dunha conexión específica e envíanse solicitudes de reentrega.
Envío de solicitudes para volver entregar paquetes (estado de montaxe):

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

O remitente aceptará a solicitude de reentrega e enviará os paquetes que faltan. Cabe sinalar que neste momento o remitente xa iniciou o temporizador de peche da conexión e, cando se recibe unha solicitude, restablece.
Reenviando paquetes perdidos (estado 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));
}

O paquete reenviado (paquete #3 no diagrama) é recibido pola conexión entrante. Realízase unha comprobación para ver se a xanela de recepción está chea e se restablece a transmisión normal de datos.
Comprobando acertos na xanela de recepción (estado de montaxe):

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

Para interactuar co protocolo de transferencia de datos, hai unha clase Reliable Udp aberta, que é un envoltorio sobre o bloque de control de transferencia. Aquí están os membros máis importantes da clase:

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

As mensaxes recíbense mediante subscrición. Sinatura do delegado para o método de devolución de chamada:

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

Mensaxe:

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

Para subscribirse a un tipo de mensaxe específico e/ou a un remitente específico, utilízanse dous parámetros opcionais: ReliableUdpMessageTypes messageType e IPEndPoint ipEndPoint.

Tipos de mensaxes:

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

A mensaxe envíase de forma asíncrona; para iso, o protocolo implementa un modelo de programación asíncrona:

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

O resultado do envío dunha mensaxe será verdadeiro (se a mensaxe chegou con éxito ao destinatario e falso) se a conexión se pechou por tempo de espera:

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusión

Non se describiu moito neste artigo. Mecanismos de correspondencia de fíos, tratamento de excepcións e erros, implementación de métodos de envío de mensaxes asíncronos. Pero o núcleo do protocolo, a descrición da lóxica para procesar paquetes, establecer unha conexión e xestionar os tempos de espera, debería estar claro para ti.

A versión demostrada do protocolo de entrega fiable é o suficientemente robusta e flexible como para cumprir cos requisitos previamente definidos. Pero quero engadir que a implementación descrita pode mellorarse. Por exemplo, para aumentar o rendemento e cambiar dinámicamente os períodos do temporizador, pódense engadir ao protocolo mecanismos como xanela deslizante e RTT, tamén será útil implementar un mecanismo para determinar MTU entre nodos de conexión (pero só se se envían mensaxes grandes). .

Grazas pola vosa atención, espero os vosos comentarios e comentarios.

PS Para aqueles que estean interesados ​​nos detalles ou só queiran probar o protocolo, a ligazón ao proxecto en GitHube:
Proxecto UDP fiable

Ligazóns e artigos útiles

  1. Especificación do protocolo TCP: en inglés и en ruso
  2. Especificación do protocolo UDP: en inglés и en ruso
  3. Discusión do protocolo RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Protocolo de datos fiables: Frase 908 и Frase 1151
  5. Unha implementación sinxela de confirmación de entrega sobre UDP: Toma o control total das túas redes con .NET e UDP
  6. Artigo que describe os mecanismos de atravesamento de NAT: Comunicación punto a punto a través dos tradutores de enderezos de rede
  7. Implementación do modelo de programación asíncrona: Implementación do modelo de programación asíncrona CLR и Como implementar o patrón de deseño IAsyncResult
  8. Portando o modelo de programación asíncrona ao patrón asincrónico baseado en tarefas (APM en TAP):
    Programación asíncrona TPL e .NET tradicional
    Interoperabilidade con outros patróns e tipos asíncronos

Actualización: Grazas maiorovp и sidristij pola idea de engadir unha tarefa á interface. Non se viola a compatibilidade da biblioteca con sistemas operativos antigos, porque O cuarto marco admite servidores XP e 4.

Fonte: www.habr.com

Engadir un comentario