Implementación del protocolo Reliable Udp para .Net

Internet ha cambiado hace mucho tiempo. Uno de los principales protocolos de Internet: las aplicaciones utilizan UDP no solo para entregar datagramas y transmisiones, sino también para proporcionar conexiones "peer-to-peer" entre los nodos de la red. Debido a su diseño simple, este protocolo tiene muchos usos previamente no planificados, sin embargo, las deficiencias del protocolo, como la falta de entrega garantizada, no han desaparecido por ningún lado. Este artículo describe la implementación del protocolo de entrega garantizada sobre UDP.
Contenido:Entrada
Requisitos del protocolo
Encabezado UDP confiable
Principios generales del protocolo
Tiempos de espera y temporizadores de protocolo
Diagrama de estado de transmisión UDP confiable
Más profundo en el código. unidad de control de transmisión
Más profundo en el código. estados

Más profundo en el código. Crear y establecer conexiones
Más profundo en el código. Cerrar la conexión en el tiempo de espera
Más profundo en el código. Restauración de la transferencia de datos
API UDP confiable
Conclusión
Enlaces y artículos útiles.

Entrada

La arquitectura original de Internet asumía un espacio de direcciones homogéneo en el que cada nodo tenía una dirección IP global y única y podía comunicarse directamente con otros nodos. Ahora Internet, de hecho, tiene una arquitectura diferente: un área de direcciones IP globales y muchas áreas con direcciones privadas ocultas detrás de dispositivos NAT.En esta arquitectura, solo los dispositivos en el espacio de direcciones global pueden comunicarse fácilmente con cualquier persona en la red porque tienen una dirección IP única y enrutable globalmente. Un nodo en una red privada puede conectarse a otros nodos en la misma red y también puede conectarse a otros nodos conocidos en el espacio de direcciones global. Esta interacción se logra en gran parte debido al mecanismo de traducción de direcciones de red. Los dispositivos NAT, como los enrutadores Wi-Fi, crean entradas especiales en la tabla de traducción para las conexiones salientes y modifican las direcciones IP y los números de puerto en los paquetes. Esto permite conexiones salientes desde la red privada a hosts en el espacio de direcciones global. Pero al mismo tiempo, los dispositivos NAT generalmente bloquean todo el tráfico entrante a menos que se establezcan reglas separadas para las conexiones entrantes.

Esta arquitectura de Internet es lo suficientemente correcta para la comunicación cliente-servidor, donde los clientes pueden estar en redes privadas y los servidores tienen una dirección global. Pero crea dificultades para la conexión directa de dos nodos entre diferente redes privadas. Una conexión directa entre dos nodos es importante para las aplicaciones de igual a igual, como la transmisión de voz (Skype), el acceso remoto a una computadora (TeamViewer) o los juegos en línea.

Uno de los métodos más efectivos para establecer una conexión punto a punto entre dispositivos en diferentes redes privadas se llama perforación. Esta técnica se usa más comúnmente con aplicaciones basadas en el protocolo UDP.

Pero si su aplicación necesita una entrega garantizada de datos, por ejemplo, transfiere archivos entre computadoras, entonces usar UDP tendrá muchas dificultades debido al hecho de que UDP no es un protocolo de entrega garantizada y no proporciona entrega de paquetes en orden, a diferencia de TCP. protocolo.

En este caso, para garantizar la entrega de paquetes garantizada, se requiere implementar un protocolo de capa de aplicación que proporcione la funcionalidad necesaria y funcione sobre UDP.

Quiero señalar de inmediato que existe una técnica de perforación de agujeros TCP para establecer conexiones TCP entre nodos en diferentes redes privadas, pero debido a la falta de soporte para muchos dispositivos NAT, generalmente no se considera como la forma principal de conectarse. tales nodos.

En el resto de este artículo, me centraré únicamente en la implementación del protocolo de entrega garantizada. La implementación de la técnica de perforación de agujeros UDP se describirá en los siguientes artículos.

Requisitos del protocolo

  1. Entrega de paquetes confiable implementada a través de un mecanismo de retroalimentación positiva (el llamado reconocimiento positivo)
  2. La necesidad de una transferencia eficiente de big data, es decir, el protocolo debe evitar la retransmisión innecesaria de paquetes
  3. Debería ser posible cancelar el mecanismo de confirmación de entrega (la capacidad de funcionar como un protocolo UDP "puro")
  4. Posibilidad de implementar el modo de comando, con confirmación de cada mensaje
  5. La unidad básica de transferencia de datos sobre el protocolo debe ser un mensaje.

Estos requisitos coinciden en gran medida con los requisitos del protocolo de datos confiables descritos en rfc 908 и rfc 1151, y me basé en esos estándares al desarrollar este protocolo.

Para comprender estos requisitos, veamos el momento de la transferencia de datos entre dos nodos de red que utilizan los protocolos TCP y UDP. Que en ambos casos tendremos un paquete perdido.
Transferencia de datos no interactivos sobre TCP:Implementación del protocolo Reliable Udp para .Net

Como puede ver en el diagrama, en caso de pérdida de un paquete, TCP detectará el paquete perdido y lo informará al remitente solicitando el número del segmento perdido.
Transferencia de datos a través del protocolo UDP:Implementación del protocolo Reliable Udp para .Net

UDP no realiza ningún paso de detección de pérdidas. El control de los errores de transmisión en el protocolo UDP es responsabilidad exclusiva de la aplicación.

La detección de errores en el protocolo TCP se logra estableciendo una conexión con un nodo final, almacenando el estado de esa conexión, indicando la cantidad de bytes enviados en cada encabezado de paquete y notificando los recibos usando un número de reconocimiento.

Además, para mejorar el rendimiento (es decir, enviar más de un segmento sin recibir un acuse de recibo), el protocolo TCP utiliza la llamada ventana de transmisión: la cantidad de bytes de datos que el remitente del segmento espera recibir.

Para obtener más información sobre el protocolo TCP, consulte rfc 793, de UDP a rfc 768donde, de hecho, se definen.

De lo anterior, está claro que para crear un protocolo confiable de entrega de mensajes sobre UDP (en lo sucesivo, UDP confiable), se requiere implementar mecanismos de transferencia de datos similares a TCP. A saber:

  • guardar estado de conexión
  • utilizar la numeración de segmentos
  • utilizar paquetes de confirmación especiales
  • use un mecanismo de ventana simplificado para aumentar el rendimiento del protocolo

Además, necesitas:

  • señalar el inicio de un mensaje, para asignar recursos para la conexión
  • señalar el final de un mensaje, para pasar el mensaje recibido a la aplicación ascendente y liberar los recursos del protocolo
  • permitir que el protocolo específico de la conexión deshabilite el mecanismo de confirmación de entrega para que funcione como UDP "puro"

Encabezado UDP confiable

Recuerde que un datagrama UDP está encapsulado en un datagrama IP. El paquete UDP confiable se "envuelve" apropiadamente en un datagrama UDP.
Encapsulación de encabezado UDP confiable:Implementación del protocolo Reliable Udp para .Net

La estructura del encabezado UDP Reliable es bastante simple:

Implementación del protocolo Reliable Udp para .Net

  • Banderas - banderas de control de paquetes
  • MessageType: tipo de mensaje utilizado por aplicaciones ascendentes para suscribirse a mensajes específicos
  • TransmissionId: el número de la transmisión, junto con la dirección y el puerto del destinatario, identifica de forma única la conexión
  • PacketNumber - número de paquete
  • Opciones: opciones de protocolo adicionales. En el caso del primer paquete, se utiliza para indicar el tamaño del mensaje.

Las banderas son las siguientes:

  • FirstPacket - el primer paquete del mensaje
  • NoAsk: el mensaje no requiere que se habilite un mecanismo de reconocimiento
  • LastPacket - el último paquete del mensaje
  • RequestForPacket - paquete de confirmación o solicitud de un paquete perdido

Principios generales del protocolo

Dado que Reliable UDP se centra en la transmisión de mensajes garantizada entre dos nodos, debe poder establecer una conexión con el otro lado. Para establecer una conexión, el remitente envía un paquete con la bandera FirstPacket, cuya respuesta significará que se ha establecido la conexión. Todos los paquetes de respuesta o, en otras palabras, los paquetes de reconocimiento, siempre establecen el valor del campo PacketNumber en uno más que el valor más grande de PacketNumber de los paquetes recibidos con éxito. El campo Opciones para el primer paquete enviado es el tamaño del mensaje.

Se utiliza un mecanismo similar para terminar una conexión. El indicador LastPacket se establece en el último paquete del mensaje. En el paquete de respuesta, se indica el número del último paquete + 1, lo que para el lado receptor significa la entrega exitosa del mensaje.
Diagrama de establecimiento y terminación de la conexión:Implementación del protocolo Reliable Udp para .Net

Cuando se establece la conexión, comienza la transferencia de datos. Los datos se transmiten en bloques de paquetes. Cada bloque, excepto el último, contiene un número fijo de paquetes. Es igual al tamaño de la ventana de recepción/transmisión. El último bloque de datos puede tener menos paquetes. Después de enviar cada bloque, el lado del envío espera una confirmación de entrega o una solicitud para volver a entregar los paquetes perdidos, dejando abierta la ventana de recepción/transmisión para recibir respuestas. Después de recibir la confirmación de la entrega del bloque, la ventana de recepción/transmisión cambia y se envía el siguiente bloque de datos.

El lado receptor recibe los paquetes. Cada paquete se verifica para ver si cae dentro de la ventana de transmisión. Los paquetes y duplicados que no caen en la ventana se filtran. Porque Si el tamaño de la ventana es fijo y es el mismo para el destinatario y el remitente, en el caso de que se entregue un bloque de paquetes sin pérdidas, la ventana se desplaza para recibir paquetes del siguiente bloque de datos y se envía una confirmación de entrega. enviado. Si la ventana no se llena dentro del período establecido por el temporizador de trabajo, se iniciará una verificación sobre qué paquetes no se han entregado y se enviarán solicitudes de reenvío.
Diagrama de retransmisión:Implementación del protocolo Reliable Udp para .Net

Tiempos de espera y temporizadores de protocolo

Hay varias razones por las que no se puede establecer una conexión. Por ejemplo, si la parte receptora está fuera de línea. En este caso, al intentar establecer una conexión, la conexión se cerrará por tiempo de espera. La implementación de Reliable UDP utiliza dos temporizadores para establecer tiempos de espera. El primero, el temporizador de trabajo, se usa para esperar una respuesta del host remoto. Si se activa en el lado del remitente, se reenvía el último paquete enviado. Si el temporizador expira en el destinatario, se realiza una verificación de paquetes perdidos y se envían solicitudes de reenvío.

El segundo temporizador es necesario para cerrar la conexión en caso de falta de comunicación entre los nodos. Para el lado del remitente, comienza inmediatamente después de que expira el temporizador de trabajo y espera una respuesta del nodo remoto. Si no hay respuesta dentro del período especificado, se termina la conexión y se liberan los recursos. Para el lado receptor, el temporizador de cierre de la conexión se inicia después de que el temporizador de trabajo expire dos veces. Esto es necesario para asegurarse contra la pérdida del paquete de confirmación. Cuando el temporizador expira, la conexión también finaliza y se liberan los recursos.

Diagrama de estado de transmisión UDP confiable

Los principios del protocolo se implementan en una máquina de estados finitos, cada uno de los cuales es responsable de una determinada lógica de procesamiento de paquetes.
Diagrama de estado UDP confiable:

Implementación del protocolo Reliable Udp para .Net

Cerradas - no es realmente un estado, es un punto de inicio y fin para el autómata. por estado Cerradas se recibe un bloque de control de transmisión que, implementando un servidor UDP asíncrono, reenvía paquetes a las conexiones apropiadas e inicia el procesamiento de estado.

PrimerPaqueteEnviando – el estado inicial en el que se encuentra la conexión de salida cuando se envía el mensaje.

En este estado, se envía el primer paquete para mensajes normales. Para mensajes sin confirmación de envío, este es el único estado donde se envía el mensaje completo.

Ciclo de envío – estado fundamental para la transmisión de paquetes de mensajes.

Transición a ella desde el estado. PrimerPaqueteEnviando se lleva a cabo después de que se ha enviado el primer paquete del mensaje. Es en este estado que vienen todos los acuses de recibo y solicitudes de retransmisiones. Salir de él es posible en dos casos: en caso de entrega exitosa del mensaje o por tiempo de espera.

PrimerPaqueteRecibido – el estado inicial del destinatario del mensaje.

Comprueba la corrección del comienzo de la transmisión, crea las estructuras necesarias y envía un acuse de recibo del primer paquete.

Para un mensaje que consta de un solo paquete y se envió sin usar prueba de entrega, este es el único estado. Después de procesar dicho mensaje, la conexión se cierra.

Ensamblar – estado básico para recibir paquetes de mensajes.

Escribe paquetes en el almacenamiento temporal, comprueba la pérdida de paquetes, envía acuses de recibo para la entrega de un bloque de paquetes y el mensaje completo, y envía solicitudes de reenvío de paquetes perdidos. En caso de recepción exitosa de todo el mensaje, la conexión pasa al estado Completado, de lo contrario, sale un tiempo de espera.

Completado – cierre de la conexión en caso de recepción satisfactoria de todo el mensaje.

Este estado es necesario para el montaje del mensaje y para el caso de que la confirmación de entrega del mensaje se perdiera en el camino hacia el remitente. Se sale de este estado por un tiempo de espera, pero la conexión se considera cerrada con éxito.

Más profundo en el código. unidad de control de transmisión

Uno de los elementos clave de Reliable UDP es el bloque de control de transmisión. La tarea de este bloque es almacenar conexiones actuales y elementos auxiliares, distribuir paquetes entrantes a las conexiones correspondientes, proporcionar una interfaz para enviar paquetes a una conexión e implementar el protocolo API. El bloque de control de transmisión recibe paquetes de la capa UDP y los reenvía a la máquina de estado para su procesamiento. Para recibir paquetes, implementa un servidor UDP asíncrono.
Algunos miembros de la 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 mensajes, se crea una estructura que contiene información sobre la conexión. Tal estructura se llama registro de conexión.
Algunos miembros de la 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ás profundo en el código. estados

Los estados implementan la máquina de estado del protocolo UDP Reliable, donde se lleva a cabo el procesamiento principal de los paquetes. La clase abstracta ReliableUdpState proporciona una interfaz para el estado:

Implementación del protocolo Reliable Udp para .Net

Toda la lógica del protocolo se implementa mediante las clases presentadas anteriormente, junto con una clase auxiliar que proporciona métodos estáticos, como, por ejemplo, construir el encabezado ReliableUdp a partir del registro de conexión.

A continuación, consideraremos en detalle la implementación de los métodos de interfaz que determinan los algoritmos básicos del protocolo.

Método DisposeByTimeout

El método DisposeByTimeout es responsable de liberar recursos de conexión después de un tiempo de espera y de señalar la entrega de mensajes correcta o incorrecta.
ReliableUdpState.DisposeByTimeout:

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

Solo se anula en el estado Completado.
Completado.DisposeByTimeout:

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

Método ProcessPackets

El método ProcessPackets es responsable del procesamiento adicional de un paquete o paquetes. Se llama directamente o a través de un temporizador de espera de paquetes.

En condicion Ensamblar el método se anula y es responsable de verificar los paquetes perdidos y hacer la transición al estado Completado, en caso de recibir el último paquete y pasar una verificación exitosa
Ensamblaje.de.paquetes.de.proceso:

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 condicion Ciclo de envío este método se llama solo en un temporizador y es responsable de reenviar el último mensaje, así como de habilitar el temporizador de cierre de la 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);
}

En condicion Completado el método detiene el temporizador de ejecución y envía el mensaje a los suscriptores.
Paquetes de proceso completados:

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

Recibir método de paquete

En condicion PrimerPaqueteRecibido la tarea principal del método es determinar si el primer paquete de mensajes realmente llegó a la interfaz y también recopilar un mensaje que consta de un solo paquete.
PrimerPaqueteRecibido.RecibirPaquete:

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 condicion Ciclo de envío este método se anula para aceptar acuses de recibo de entrega y solicitudes de retransmisión.
Ciclo de envío.Recepción de paquete:

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 condicion Ensamblar en el método ReceivePacket, se lleva a cabo el trabajo principal de ensamblar un mensaje a partir de paquetes entrantes.
Montaje.RecibirPaquete:

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 condicion Completado la única tarea del método es enviar un nuevo acuse de recibo de la entrega exitosa del mensaje.
Completado. Recibir paquete:

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

En condicion PrimerPaqueteEnviando este método envía el primer paquete de datos, o si el mensaje no requiere confirmación de entrega, el mensaje completo.
PrimerPaqueteEnviando.EnviarPaquete:

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 condicion Ciclo de envío en este método, se envía 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ás profundo en el código. Crear y establecer conexiones

Ahora que hemos visto los estados básicos y los métodos utilizados para manejar estados, analicemos algunos ejemplos de cómo funciona el protocolo con un poco más de detalle.
Diagrama de transmisión de datos en condiciones normales:Implementación del protocolo Reliable Udp para .Net

Considere en detalle la creación. registro de conexión para conectar y enviar el primer paquete. La transferencia siempre la inicia la aplicación que llama a la API de envío de mensajes. A continuación, se invoca el método StartTransmission del bloque de control de transmisión, que inicia la transmisión de datos para el nuevo mensaje.
Crear una conexión saliente:

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

Después de enviar el primer paquete, el remitente entra en el estado Ciclo de envío – esperar la confirmación de la entrega del paquete.
El lado receptor, utilizando el método EndReceive, recibe el paquete enviado, crea un nuevo registro de conexión y pasa este paquete, con un encabezado previamente analizado, al método ReceivePacket del estado para su procesamiento PrimerPaqueteRecibido
Crear una conexión en el 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);
}

Recibir el primer paquete y enviar 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ás profundo en el código. Cerrar la conexión en el tiempo de espera

El manejo del tiempo de espera es una parte importante de Reliable UDP. Considere un ejemplo en el que falló un nodo intermedio y la entrega de datos en ambas direcciones se volvió imposible.
Diagrama para cerrar una conexión por tiempo de espera:Implementación del protocolo Reliable Udp para .Net

Como puede verse en el diagrama, el temporizador de trabajo del remitente comienza inmediatamente después de enviar un bloque de paquetes. Esto sucede en el método SendPacket del estado. Ciclo de envío.
Habilitación del temporizador de trabajo (estado SendingCycle):

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

Los períodos del temporizador se establecen cuando se crea la conexión. El ShortTimerPeriod predeterminado es de 5 segundos. En el ejemplo, se establece en 1,5 segundos.

Para una conexión entrante, el temporizador comienza después de recibir el último paquete de datos entrante, esto sucede en el método ReceivePacket del estado Ensamblar
Habilitación del temporizador de trabajo (estado de ensamblaje):

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 llegaron más paquetes a la conexión entrante mientras esperaban el temporizador de trabajo. El temporizador se apagó y llamó al método ProcessPackets, donde se encontraron los paquetes perdidos y se enviaron solicitudes de reenvío por primera vez.
Envío de solicitudes de reenvío (estado de ensamblaje):

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 se establece en verdadero. Esta variable es responsable de reiniciar el temporizador de trabajo.

En el lado del remitente, el temporizador de trabajo también se activa y se reenvía el último paquete enviado.
Habilitación del temporizador de cierre de conexión (estado SendingCycle):

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

Después de eso, el temporizador de cierre de la conexión comienza en la conexión saliente.
ConfiableUdpState.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 período de tiempo de espera del temporizador de cierre de la conexión es de 30 segundos de forma predeterminada.

Después de un breve período de tiempo, el temporizador de trabajo en el lado del destinatario se dispara nuevamente, las solicitudes se envían nuevamente, luego de lo cual se inicia el temporizador de cierre de conexión para la conexión entrante.

Cuando se activan los temporizadores de cierre, se liberan todos los recursos de ambos registros de conexión. El remitente informa el error de entrega a la aplicación ascendente (ver API UDP confiable).
Liberación de recursos de registros 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ás profundo en el código. Restauración de la transferencia de datos

Diagrama de recuperación de transmisión de datos en caso de pérdida de paquetes:Implementación del protocolo Reliable Udp para .Net

Como ya se discutió en el cierre de la conexión en el tiempo de espera, cuando el temporizador de trabajo expira, el receptor buscará paquetes perdidos. En caso de pérdida de paquetes, se compilará una lista de la cantidad de paquetes que no llegaron al destinatario. Estos números se ingresan en la matriz LostPackets de una conexión específica y se envían solicitudes de reenvío.
Envío de solicitudes para volver a entregar paquetes (estado Ensamblado):

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 remitente aceptará la solicitud de reenvío y enviará los paquetes faltantes. Vale la pena señalar que en este momento el remitente ya inició el temporizador de cierre de la conexión y, cuando se recibe una solicitud, se reinicia.
Reenvío de 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));
}

El paquete de reenvío (paquete n.° 3 en el diagrama) es recibido por la conexión entrante. Se realiza una verificación para ver si la ventana de recepción está llena y se restaura la transmisión normal de datos.
Comprobación de aciertos en la ventana de recepción (estado de montaje):

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 confiable

Para interactuar con el protocolo de transferencia de datos, hay una clase Udp confiable abierta, que es un contenedor sobre el bloque de control de transferencia. Estos son los miembros más importantes de la 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()    
}

Los mensajes se reciben por suscripción. Firma del delegado para el método de devolución de llamada:

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

Mensaje:

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

Para suscribirse a un tipo de mensaje específico y/o un remitente específico, se utilizan dos parámetros opcionales: ReliableUdpMessageTypes messageType e IPEndPoint ipEndPoint.

Tipos de mensajes:

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

El mensaje se envía de forma asíncrona, para ello el protocolo implementa un modelo de programación asíncrona:

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

El resultado de enviar un mensaje será verdadero, si el mensaje llegó correctamente al destinatario y falso, si la conexión se cerró por tiempo de espera:

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusión

No se ha descrito mucho en este artículo. Mecanismos de coincidencia de hilos, manejo de excepciones y errores, implementación de métodos de envío de mensajes asíncronos. Pero el núcleo del protocolo, la descripción de la lógica para procesar paquetes, establecer una conexión y manejar los tiempos de espera, debe ser claro para usted.

La versión demostrada del protocolo de entrega confiable es lo suficientemente robusta y flexible para cumplir con los requisitos definidos anteriormente. Pero quiero agregar que la implementación descrita se puede mejorar. Por ejemplo, para aumentar el rendimiento y cambiar dinámicamente los períodos del temporizador, se pueden agregar al protocolo mecanismos como la ventana deslizante y RTT, también será útil implementar un mecanismo para determinar la MTU entre los nodos de conexión (pero solo si se envían mensajes grandes) .

Gracias por su atención, espero sus comentarios y comentarios.

PD Para aquellos que estén interesados ​​en los detalles o simplemente quieran probar el protocolo, el enlace al proyecto en GitHube:
Proyecto UDP confiable

Enlaces y artículos útiles.

  1. Especificación del protocolo TCP: en inglés и en Inglés
  2. Especificación del protocolo UDP: en inglés и en Inglés
  3. Discusión del protocolo RUDP: draft-ietf-sigtran-confiable-udp-00
  4. Protocolo de datos confiable: rfc 908 и rfc 1151
  5. Una implementación simple de confirmación de entrega sobre UDP: Tome el control total de su red con .NET y UDP
  6. Artículo que describe los mecanismos transversales de NAT: Comunicación de igual a igual a través de traductores de direcciones de red
  7. Implementación del modelo de programación asíncrona: Implementación del modelo de programación asincrónica CLR и Cómo implementar el patrón de diseño IAsyncResult
  8. Migración del modelo de programación asíncrona al patrón asíncrono basado en tareas (APM en TAP):
    Programación asíncrona TPL y .NET tradicional
    Interoperabilidad con otros patrones y tipos asincrónicos

Actualización: gracias mayorovp и sidristij por la idea de agregar una tarea a la interfaz. No se viola la compatibilidad de la biblioteca con sistemas operativos antiguos, porque El 4º marco es compatible con servidores XP y 2003.

Fuente: habr.com

Añadir un comentario