El Internet ha cambiado desde hace tiempo. Uno de los principales protocolos de Internet, UDP, se utiliza en aplicaciones no solo para la entrega de datagramas y transmisiones por difusión, sino también para proporcionar conexiones 'peer-to-peer' entre nodos de la red. Debido a su diseño simple, este protocolo ha encontrado muchas aplicaciones no planificadas previamente; sin embargo, las desventajas del protocolo, como la falta de entrega garantizada, no han desaparecido. Este artículo describe la implementación de un protocolo de entrega garantizada sobre UDP.
Contenido:
Introducción
La arquitectura inicial de Internet suponí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. Actualmente, Internet, de hecho, tiene una arquitectura diferente: una zona 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 interactuar fácilmente con otros en la red, ya que poseen una dirección IP única y globalmente enrutada. Un nodo en una red privada puede conectarse a otros nodos en la misma red, así como a otros nodos bien conocidos en el espacio de direcciones global. Esta interacción se logra en gran medida gracias al mecanismo de traducción de direcciones de red. Los dispositivos NAT, por ejemplo, los enrutadores Wi-Fi, crean registros especiales en las tablas de traducción para las conexiones salientes y modifican las direcciones IP y los números de puerto en los paquetes. Esto permite establecer una conexión saliente desde la red privada a nodos en el espacio de direcciones global. Sin embargo, al mismo tiempo, los dispositivos NAT suelen bloquear todo el tráfico entrante a menos que se establezcan reglas específicas para las conexiones entrantes.
Esta arquitectura de Internet es bastante adecuada para la interacción cliente-servidor, donde los clientes pueden estar en redes privadas y los servidores tienen direcciones globales. Pero crea dificultades para la conexión directa entre dos nodos en diferentes redes privadas. diferentes redes privadas. La conexión directa entre dos nodos es importante para aplicaciones "peer-to-peer", como la transmisión de voz (Skype), el acceso remoto a un ordenador (TeamViewer) o los juegos en línea.
Uno de los métodos más eficaces para establecer una conexión peer-to-peer entre dispositivos en diferentes redes privadas se llama "hole punching". Esta técnica se utiliza con mayor frecuencia en aplicaciones basadas en el protocolo UDP.
Pero si su aplicación requiere la entrega garantizada de datos, por ejemplo, al transferir archivos entre ordenadores, entonces al utilizar UDP surgirán muchos problemas, ya que UDP no es un protocolo de entrega garantizada y no garantiza la entrega de paquetes en orden, a diferencia del protocolo TCP.
En este caso, para garantizar la entrega asegurada de paquetes, es necesario implementar un protocolo de nivel superior que proporcione la funcionalidad necesaria y que funcione sobre UDP.
Quiero señalar de inmediato que existe una técnica llamada TCP hole punching para establecer conexiones TCP entre nodos en diferentes redes privadas, pero debido a la falta de soporte en muchos dispositivos NAT, generalmente no se considera el método principal para conectar dichos nodos.
En este artículo, solo consideraré la implementación del protocolo de entrega garantizada. La implementación de la técnica UDP hole punching se describirá en artículos posteriores.
Requisitos del protocolo
- La entrega confiable de paquetes se implementa a través de un mecanismo de confirmación positiva (lo que se conoce como positive acknowledgment).
- Es necesario una transmisión eficiente de grandes volúmenes de datos, es decir, el protocolo debe evitar retransmisiones innecesarias de paquetes.
- Debe haber la posibilidad de desactivar el mecanismo de confirmación de entrega (capacidad de funcionar como un protocolo UDP "puro").
- Posibilidad de implementar un modo de comando, con confirmación de cada mensaje.
- La unidad básica de transmisión de datos del protocolo debe ser un mensaje.
Estos requisitos se alinean en gran medida con los requisitos del Reliable Data Protocol, descritos en. y , y me basé en estos estándares al desarrollar este protocolo.
Para entender estos requisitos, analicemos los diagramas temporales de transmisión de datos entre dos nodos de red a través de los protocolos TCP y UDP. Supongamos que en ambos casos se pierde un paquete.
Transmisión de datos no interactivos a través de TCP:
Como se puede ver en el diagrama, en caso de pérdida de paquetes, TCP detectará el paquete perdido y notificará al remitente, solicitando el número del segmento perdido.
Transmisión de datos a través del protocolo UDP:
UDP no toma ninguna medida para detectar pérdidas. El control de errores de la transmisión en el protocolo UDP recae completamente en la aplicación.
La detección de errores en el protocolo TCP se logra mediante el establecimiento de una conexión con el nodo final, manteniendo el estado de esta conexión, indicando el número de bytes enviados en cada encabezado de paquete y notificando las recepciones mediante el número de confirmación "acknowledge number".
Además, para mejorar el rendimiento (es decir, enviar más de un segmento sin recibir confirmación), el protocolo TCP utiliza una ventana de transmisión: el número de bytes de datos que el remitente del segmento espera recibir.
Para más detalles sobre el protocolo TCP, se puede consultar. , con UDP en , donde, de hecho, están definidos.
De lo anterior, queda claro que para crear un protocolo de entrega de mensajes confiable sobre UDP (en adelante llamaremos Reliable UDP), es necesario implementar mecanismos de transmisión de datos similares a los de TCP. Específicamente:
- mantener el estado de la conexión
- utilizar la numeración de segmentos
- usar paquetes de confirmación especiales
- utilizar un mecanismo de ventana simplificado para aumentar la capacidad del protocolo
Adicionalmente, se requiere:
- señalar el inicio del mensaje para reservar recursos para la conexión
- indicar el final del mensaje para transferir el mensaje recibido a la aplicación superior y liberar los recursos del protocolo
- permitir que el protocolo para conexiones específicas desactive el mecanismo de confirmación de entrega, para funcionar como un UDP 'puro'
Encabezado Reliable UDP
Recordemos que el datagrama UDP se encapsula en un datagrama IP. Por lo tanto, el paquete Reliable UDP se 'envuelve' en un datagrama UDP.
Encapsulación del encabezado Reliable UDP:
La estructura del encabezado Reliable UDP es bastante simple:

- Flags – banderas de control del paquete
- MessageType – tipo de mensaje, utilizado por aplicaciones superiores para suscribirse a mensajes específicos
- TransmissionId — identificador de transmisión, junto con la dirección y el puerto del destinatario, define de manera única la conexión
- PacketNumber – número de paquete
- Options – opciones adicionales del protocolo. En el caso del primer paquete se utiliza para indicar el tamaño del mensaje
Las banderas pueden ser las siguientes:
- FirstPacket — primer paquete del mensaje
- NoAsk — el mensaje no requiere activar el mecanismo de confirmación
- LastPacket — último paquete del mensaje
- RequestForPacket — paquete de confirmación o solicitud de paquete perdido
Principios generales de operación del protocolo
Dado que Reliable UDP está orientado a la transmisión garantizada de mensajes entre dos nodos, debe ser capaz de establecer una conexión con el otro lado. Para establecer la conexión, el lado emisor envía un paquete con la bandera FirstPacket, cuya respuesta indicará el establecimiento de la conexión. Todos los paquetes de respuesta, o de confirmación, siempre establecen el valor del campo PacketNumber en uno más que el número más alto de PacketNumber de los paquetes que han llegado con éxito. En el campo Options del primer paquete enviado se registra el tamaño del mensaje.
Para finalizar la conexión, se utiliza un mecanismo similar. En el último paquete de mensajes se establece la bandera LastPacket. En el paquete de respuesta se indica el número del último paquete + 1, lo que para la parte receptora significa que el mensaje ha sido entregado con éxito.
Diagrama de establecimiento y finalización de la conexión:
Una vez establecida la conexión, comienza la transmisión de datos. Los datos se transmiten en bloques de paquetes. Cada bloque, excepto el último, contiene un número fijo de paquetes, igual al tamaño de la ventana de recepción/transmisión. El último bloque de datos puede tener un número menor de paquetes. Tras el envío de cada bloque, la parte emisora espera la confirmación de entrega o la solicitud de reenvío de paquetes perdidos, manteniendo abierta la ventana de recepción/transmisión para recibir respuestas. Después de recibir la confirmación de entrega del bloque, la ventana de recepción/transmisión se desplaza y se envía el siguiente bloque de datos.
La parte receptora recibe los paquetes. Cada paquete se verifica para ver si cae dentro de la ventana de transmisión. Los paquetes que no entran en la ventana y los duplicados son descartados. Dado que el tamaño de la ventana es fijo y es el mismo tanto para el receptor como para el emisor, en caso de que un bloque de paquetes se entregue sin pérdidas, la ventana se desplaza para recibir los paquetes del siguiente bloque de datos y se envía la confirmación de entrega. Si la ventana no se llena dentro del período establecido por el temporizador de trabajo, se iniciará una verificación de qué paquetes no han sido entregados y se enviarán solicitudes de reenvío.
Diagrama de retransmisión:
Tiempos de espera y temporizadores del protocolo
Existen varias razones por las que no se puede establecer una conexión. Por ejemplo, si la parte receptora está fuera de línea. En ese caso, al intentar establecer una conexión, esta se cerrará por tiempo de espera. En la implementación de Reliable UDP se utilizan dos temporizadores para establecer los tiempos de espera. El primero, el temporizador de trabajo, se usa para esperar una respuesta del host remoto. Si se activa en la parte emisora, se reenvía el último paquete enviado. Si el temporizador se activa en el receptor, se realiza una verificación de los paquetes perdidos y se envían solicitudes de reenvío.
El segundo temporizador es necesario para cerrar la conexión en caso de que no haya comunicación entre los nodos. Para el lado del emisor, se activa inmediatamente después de que el temporizador de trabajo se dispare, y espera una respuesta del nodo remoto. Si no hay respuesta dentro del período establecido, la conexión se cierra y se liberan los recursos. Para el lado del receptor, el temporizador de cierre de conexión se activa después de la activación doble del temporizador de trabajo. Esto es necesario como una garantía contra la pérdida del paquete de confirmación. Cuando se activa el temporizador, la conexión también se cierre y se liberan los recursos.
Diagrama de estados de transferencia Reliable UDP
Los principios de funcionamiento del protocolo se implementan en una máquina de estados, donde cada estado se encarga de una lógica específica para el procesamiento de paquetes.
Diagrama de estados de Reliable UDP:

Cerrado – en realidad no es un estado, es el punto inicial y final para la máquina. Se considera como estado Cerrado el bloque de control de transmisión, que, al implementar un servidor UDP asíncrono, redirige los paquetes a las conexiones correspondientes y activa el procesamiento de estados.
EnviandoPrimerPaquete – estado inicial en el que se encuentra la conexión saliente al enviar un 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: aquí se envía todo el mensaje.
CicloDeEnvio – estado principal para la transmisión de paquetes del mensaje.
La transición a este estado desde EnviandoPrimerPaquete se produce después de enviar el primer paquete del mensaje. Es en este estado donde llegan todas las confirmaciones y solicitudes de reenvío. La salida de este estado es posible en dos casos: en caso de entrega exitosa del mensaje o por tiempo de espera.
PrimerPaqueteRecibido – estado inicial para el receptor del mensaje.
Aquí se verifica la corrección del inicio de la transmisión, se crean las estructuras necesarias y se envía una confirmación de recepción del primer paquete.
Para un mensaje que consiste en un solo paquete y se envía sin utilizar confirmación de entrega, este es el único estado. Después de procesar tal mensaje, la conexión se cierra.
Montaje – estado principal para la recepción de paquetes del mensaje.
En él se registra paquetes en un almacenamiento temporal, se verifica la ausencia de pérdida de paquetes, se envían confirmaciones de entrega del bloque de paquetes y del mensaje completo, y se envían solicitudes para la reentrega de paquetes perdidos. En caso de recibir correctamente todo el mensaje, la conexión pasa al estado Completed, de lo contrario, se realiza un cierre por tiempo de espera.
Completed – cierre de la conexión en caso de recibir correctamente todo el mensaje.
Este estado es necesario para ensamblar el mensaje y para el caso en que se perdió la confirmación de entrega del mensaje en el camino al remitente. La salida de este estado se realiza por tiempo de espera, pero la conexión se considera cerrada con éxito.
Más a fondo en el código. Bloque de control de transferencia
Uno de los elementos clave de Reliable UDP es el bloque de control de transmisión. La tarea de este bloque es almacenar las conexiones actuales y los elementos auxiliares, distribuir los paquetes recibidos a las conexiones correspondientes, proporcionar una interfaz para enviar paquetes a la conexión y realizar la API del protocolo. El bloque de control de transmisión recibe paquetes desde el nivel UDP y los redirige al autómata para su procesamiento. Para recibir paquetes, se implementó un servidor UDP asincrónico.
Algunos miembros de la clase ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// arreglo de bytes para la clave especificada. Se utiliza para ensamblar mensajes entrantes
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// arreglo de bytes para la clave especificada. Se utiliza para enviar mensajes salientes.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// registro de conexión para la clave especificada.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// lista de suscriptores a los mensajes.
private readonly List m_subscribers;
// socket local
private Socket m_socketIn;
// puerto para mensajes entrantes
private int m_port;
// dirección IP local
private IPAddress m_ipAddress;
// punto final local
public IPEndPoint LocalEndpoint { get; private set; }
// colección de estados del autómata previamente inicializados
public StatesCollection States { get; private set; }
// generador de números aleatorios. Se utiliza para crear TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementación de un servidor UDP asincrónico:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// Creamos un nuevo búfer para cada socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// Pasamos el búfer como parámetro al método asíncrono
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);
// Paquete recibido, listos para recibir el siguiente
Receive();
// Dado que la forma más simple de resolver la cuestión del búfer es obtener una referencia a él
// desde IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// Obtenemos el encabezado del paquete
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// Ha llegado un paquete incorrecto - lo descartamos
return;
}
// Construimos la clave para determinar el registro de conexión para el paquete
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// Obtenemos el registro de conexión existente o creamos uno nuevo
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// Procesamos el paquete en la máquina de estados
record.State.ReceivePacket(record, header, bytes);
}
Para cada transmisión de mensaje, se crea una estructura que contiene información sobre la conexión. Esta estructura se llama registro de conexión.
Algunos miembros de la clase ReliableUdpConnectionRecord:
clase interna ReliableUdpConnectionRecord : IDisposable
{
\/\/ matriz de bytes con el mensaje
public byte[] IncomingStream { get; set; }
\/\/ referencia al estado de la máquina de estados
public ReliableUdpState State { get; set; }
\/\/ par que identifica de manera única el registro de conexión
\/\/ en el bloque de control de transmisión
public Tuple Key { get; private set;}
\/\/ límite inferior de la ventana receptora
public int WindowLowerBound;
\/\/ tamaño de la ventana de transmisión
public readonly int WindowSize;
\/\/ número del paquete a enviar
public int SndNext;
\/\/ cantidad de paquetes a enviar
public int NumberOfPackets;
\/\/ número de transmisión (este es la segunda parte de Tuple)
\/\/ para cada mensaje hay uno
public readonly Int32 TransmissionId;
\/\/ endpoint IP remoto – el destinatario del mensaje
public readonly IPEndPoint RemoteClient;
\/\/ tamaño del paquete, para evitar fragmentación a nivel IP
\/\/ no debe superar el MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
public readonly int BufferSize;
\/\/ bloque de control de transmisión
public readonly ReliableUdpConnectionControlBlock Tcb;
\/\/ encapsula los resultados de la operación asincrónica para BeginSendMessage\/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
\/\/ no enviar paquetes de confirmación
public bool IsNoAnswerNeeded;
\/\/ último paquete recibido correctamente (siempre se establece en el número más alto)
public int RcvCurrent;
\/\/ matriz con los números de los paquetes perdidos
public int[] LostPackets { get; private set; }
\/\/ si el último paquete llegó. Usado como bool.
public int IsLastPacketReceived = 0;
\/\/...
}
Más a fondo en el código. Estados
Los estados implementan la máquina de estados del protocolo Reliable UDP, donde se procesa la lógica principal de los paquetes. La clase abstracta ReliableUdpState proporciona una interfaz para el estado:

Toda la lógica de operación del protocolo es implementada por las clases presentadas anteriormente, junto con una clase auxiliar que ofrece métodos estáticos, como, por ejemplo, la construcción del encabezado ReliableUdp a partir del registro de conexión.
A continuación, se revisarán en detalle las implementaciones de los métodos de la interfaz que definen los algoritmos principales de operación del protocolo.
Método DisposeByTimeout
El método DisposeByTimeout se encarga de liberar los recursos de la conexión tras la expiración del tiempo de espera y de señalar la entrega exitosa\/no exitosa del mensaje.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Se sobreescribe solo en el estado Completed.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// Notificamos sobre la recepción exitosa del mensaje
SetAsCompleted(connectionRecord);
}
Método ProcessPackets
El método ProcessPackets se encarga del procesamiento adicional de uno o más paquetes. Se llama directamente o a través de un temporizador de espera de paquetes.
En estado Montaje el método está sobreescrito y se encarga de verificar los paquetes perdidos y cambiar de estado Completed, en el caso de recibir el último paquete y pasar la verificación exitosa
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// hay paquetes perdidos, enviamos solicitudes para ellos
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// establecemos el temporizador por segunda vez, para un nuevo intento de transmisión
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// si después de dos intentos de WaitForPacketTimer
// no se lograron recibir paquetes - iniciamos el temporizador de cierre de conexión
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// verificación exitosa
{
// enviamos la confirmación de recepción del bloque de datos
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// en lugar de aplicar los recursos de inmediato
// iniciamos el temporizador, en caso de que
// si el último ack no llega al remitente y solicita de nuevo.
// al activarse el temporizador - aplicamos los recursos
// en estado Completed, el método del temporizador está sobreescrito
StartCloseWaitTimer(connectionRecord);
}
// este es el caso en que el ack del bloque de paquetes se ha perdido
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// iniciamos el temporizador de cierre de conexión
StartCloseWaitTimer(connectionRecord);
}
}
En estado CicloDeEnvio este método se llama solo por el temporizador y se encarga de reenviar el último mensaje, así como de activar el temporizador de cierre de conexión.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// reenviamos el último paquete
// (en caso de recuperación de la conexión, el nodo receptor volverá a enviar las solicitudes que no le llegaron)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// activamos el temporizador CloseWait – para esperar la recuperación de la conexión o su finalización
StartCloseWaitTimer(connectionRecord);
}
En estado Completed el método detiene el temporizador de trabajo y envía un mensaje a los suscriptores.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// recogemos el mensaje y lo enviamos a los suscriptores
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Método ReceivePacket
En estado PrimerPaqueteRecibido la tarea principal del método es determinar si realmente el primer paquete del mensaje ha llegado a la interfaz, así como recoger un mensaje que consiste en un solo paquete.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
return; // descartamos el paquete
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); // enviamos paquete de confirmación
}
SetAsCompleted(connectionRecord);
return;
}
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));
connectionRecord.RcvCurrent = header.PacketNumber;
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 estado CicloDeEnvio Este método está redefinido para recibir confirmaciones de entrega y 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;
// cálculo del límite superior de la ventana
// se toma el límite de la ventana + 1 para recibir confirmaciones de entrega
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// verificación de que está dentro de la ventana
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// verificar si es el último paquete:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// transmisión completa
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// esta es la respuesta al primer paquete con confirmación
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// sin desplazamiento de ventana
SendPacket(connectionRecord);
}
// ha llegado la confirmación de recepción del bloque de datos
else if (header.PacketNumber == windowHighestBound)
{
// desplazamos la ventana de recepción/transmisión
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// restablecemos el array de control de transmisión
connectionRecord.WindowControlArray.Nullify();
// enviamos el bloque de paquetes
SendPacket(connectionRecord);
}
// esta es una solicitud de retransmisión – enviamos el paquete requerido
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
En estado Montaje El método ReceivePacket realiza el trabajo principal de ensamblar el mensaje a partir de los paquetes entrantes.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// procesamiento de paquetes sin el mecanismo de confirmación de entrega
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// reiniciamos el temporizador
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// registramos los datos
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// si recibimos un paquete con la última bandera - finalizamos
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// cálculo del límite superior de la ventana
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// descartamos paquetes que no están en la ventana
if (header.PacketNumber (windowHighestBound))
return;
// descartamos duplicados
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// registramos los datos
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// incrementamos el contador de paquetes
connectionRecord.PacketCounter++;
// registramos en el array de control de la ventana el número actual del paquete
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// establecemos el paquete recibido más alto
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// reiniciamos temporizadores
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// si hemos recibido el último paquete
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// si hemos recibido todos los paquetes de la ventana, reiniciamos el contador
// y enviamos el paquete de confirmación
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// reiniciamos el contador.
connectionRecord.PacketCounter = 0;
// desplazamos la ventana de envío
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// reiniciamos el array de control de envío
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// si ya se ha recibido el último paquete
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// verificamos los paquetes
ProcessPackets(connectionRecord);
}
}
En estado Completed la única tarea del método es enviar una confirmación de entrega exitosa del mensaje.
Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// reenvío del último paquete debido a que
// el último ack no llegó al remitente
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Método SendPacket
En estado EnviandoPrimerPaquete este método envía el primer paquete de datos, o, si el mensaje no requiere confirmación de entrega, el mensaje completo.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// si no se necesita confirmación, enviamos todos los paquetes
// y liberamos recursos
if (connectionRecord.IsNoAnswerNeeded)
{
// Aquí se envía tal como está
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// creamos el encabezado del paquete y lo enviamos
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// aumentamos el contador
connectionRecord.SndNext++;
// desplazamos la ventana
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Iniciamos el temporizador
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
En estado CicloDeEnvio en este método se envía un bloque de paquetes.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// enviamos un bloque de paquetes
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++;
}
// en caso de una ventana de transmisión grande, reiniciamos el temporizador después del envío
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
Más a fondo en el código. Creación y establecimiento de conexiones
Ahora que hemos conocido los principales estados y métodos utilizados para manejar los estados, podemos analizar con más detalle algunos ejemplos del funcionamiento del protocolo.
Diagrama de transmisión de datos en condiciones normales:
Analicemos detalladamente la creación registro de conexión para conectar y enviar el primer paquete. El iniciador de la transmisión siempre es la aplicación que llama al método de API para enviar el mensaje. Luego se utiliza el método StartTransmission del bloque de control de transmisión, que inicia la transmisión de datos para el nuevo mensaje.
Creación de una conexión saliente:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "Debe usar el constructor con parámetros o iniciar el listener antes de enviar un mensaje" );
}
// iniciamos el procesamiento de paquetes entrantes
StartListener(LocalEndpoint);
}
// creamos una clave para el diccionario, basada en EndPoint y ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// generamos un número aleatorio transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// creamos una nueva entrada para la conexión y verificamos,
// si ya existe ese número en nuestros diccionarios
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// si existe – regeneramos un número aleatorio
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// si no se pudo de nuevo – generamos una excepción
throw new ArgumentException("El par TransmissionId & EndPoint ya existe en el diccionario");
}
// comenzamos el estado para el procesamiento
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Envio del primer paquete (estado FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// creamos el encabezado del paquete y lo enviamos
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// incrementamos el contador
connectionRecord.SndNext++;
// desplazamos la ventana
connectionRecord.WindowLowerBound++;
// cambiamos al estado SendingCycle
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Iniciamos el temporizador
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Después de enviar el primer paquete, el remitente pasa al estado CicloDeEnvio – esperar la confirmación de entrega del paquete.
La parte receptora, mediante el método EndReceive, recibe el paquete enviado, crea un nuevo registro de conexión y pasa este paquete, con el encabezado previamente analizado, al método ReceivePacket para su procesamiento. PrimerPaqueteRecibido
Creación de conexión en el lado receptor:
private void EndReceive(IAsyncResult ar)
{
// ...
// paquete recibido
// parseando el encabezado del paquete
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// llegó un paquete incorrecto - lo descartamos
return;
}
// construimos la clave para identificar el registro de conexión del paquete
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
// obtenemos el registro de conexión existente o creamos uno nuevo
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// lanzamos el paquete para su procesamiento en la máquina de estados
record.State.ReceivePacket(record, header, bytes);
}
Recepción del primer paquete y envío de confirmación (estado FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// descartamos el paquete
return;
// ...
// por diseño todos los números de paquete comienzan desde 0;
if (header.PacketNumber != 0)
return;
// inicializamos el arreglo para almacenar partes del mensaje
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// escribimos los datos del paquete en el arreglo
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// contamos la cantidad de paquetes que deben llegar
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length/(double)connectionRecord.BufferSize));
// registramos el número del último paquete recibido (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// luego desplazamos la ventana de recepción en 1
connectionRecord.WindowLowerBound++;
// cambiamos el estado
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*si no se requiere el mecanismo de confirmación*/)
// ...
else
{
// enviamos la confirmación
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Más a fondo en el código. Cierre de la conexión por tiempo de espera
El manejo de los tiempos de espera es una parte importante de Reliable UDP. Consideremos un ejemplo en el que hubo un fallo en un nodo intermedio y la entrega de datos se volvió imposible en ambas direcciones.
Diagrama de cierre de conexión por tiempo de espera:
Como se muestra en el diagrama, el temporizador de trabajo del remitente se activa inmediatamente después de enviar el bloque de paquetes. Esto ocurre en el método SendPacket del estado CicloDeEnvio.
Activación del temporizador de trabajo (estado SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// enviamos un bloque de paquetes
// ...
// reiniciamos el temporizador después del envío
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
Los períodos del temporizador se establecen al crear la conexión. Por defecto, ShortTimerPeriod es igual a 5 segundos. En el ejemplo, se establece en 1.5 segundos.
En la conexión entrante, el temporizador se activa después de recibir el último paquete de datos que llegó, esto ocurre en el método ReceivePacket del estado Montaje
Activación del temporizador de trabajo (estado Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// reiniciamos los temporizadores
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
En la conexión entrante, no llegaron más paquetes durante el tiempo de espera del temporizador de trabajo. El temporizador se activó y llamó al método ProcessPackets, en el que se detectaron paquetes perdidos y se enviaron solicitudes para reenvíos por primera vez.
Envío de solicitudes de reenvío (estado Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*verificación de paquetes perdidos */)
{
// enviamos solicitudes de reenvío
// establecemos el temporizador por segunda vez, para el intento de transmisión
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// si después de dos intentos de activación de WaitForPacketTimer
// no se lograron recibir paquetes - iniciamos el temporizador de cierre de conexión
StartCloseWaitTimer(connectionRecord);
}
else if (/*se recibió el último paquete y la verificación fue exitosa */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// si ack del bloque de paquetes se perdió
else
{
if (!connectionRecord.TimerSecondTry)
{
// reenviamos ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// iniciamos el temporizador de cierre de conexión
StartCloseWaitTimer(connectionRecord);
}
}
La variable TimerSecondTry se estableció en true. Esta variable es responsable del reinicio del temporizador de trabajo.
Del lado del remitente, el temporizador de trabajo también se activa y se reenvía el último paquete enviado.
Activación del temporizador de cierre de conexión (estado SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// reenviamos el último paquete
// ...
// activamos el temporizador CloseWait – para esperar la recuperación de la conexión o su finalización
StartCloseWaitTimer(connectionRecord);
}
Después de esto, se inicia un temporizador de cierre de conexión en la conexión saliente.
ReliableUdpState.StartCloseWaitTimer:
protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
else
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}
El período de espera del temporizador de cierre de conexión es de 30 segundos por defecto.
Después de un breve momento, se activa nuevamente el temporizador de trabajo en el lado del receptor, se envían nuevamente las solicitudes, tras lo cual se inicia el temporizador de cierre de conexión en la conexión entrante.
Al activarse los temporizadores de cierre, se liberan todos los recursos de ambos registros de conexión. El remitente informa de una entrega fallida a la aplicación superior (ver API Reliable UDP).
Liberación de recursos del registro 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 a fondo en el código. Recuperación de la transmisión de datos
Diagrama de recuperación de datos ante pérdida de paquetes:
Como ya se discutió en el cierre de la conexión por tiempo de espera, tras la expiración del temporizador de trabajo, el receptor realizará una verificación de los paquetes perdidos. En caso de que existan pérdidas, se elaborará una lista de números de paquetes que no llegaron al receptor. Tales números se registran en el arreglo LostPackets de la conexión específica y se envían solicitudes para la reentrega.
Envío de solicitudes para la reentrega de paquetes (estado Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
//...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// hay paquetes perdidos, se envían solicitudes para ellos
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// ...
}
}
El remitente aceptará la solicitud de reentrega y enviará los paquetes que faltan. Cabe destacar que en este momento el remitente ya tiene activado el temporizador de cierre de conexión y, al recibir la solicitud, se restablece.
Reenvío de paquetes perdidos (estado SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// reiniciar el temporizador de cierre de conexión
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// esta es una solicitud de retransmisión – enviamos el paquete requerido
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
El paquete retransmitido (packet#3 en el diagrama) es recibido por la conexión entrante. Se verifica la ocupación de la ventana de recepción y se reanuda la transmisión de datos normal.
Verificación de inclusión en la ventana de recepción (estado Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// incrementamos el contador de paquetes
connectionRecord.PacketCounter++;
// registramos en el arreglo de control de ventana el número actual del paquete
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// establecemos el mayor paquete recibido
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// reiniciamos los temporizadores
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// si hemos recibido todos los paquetes de la ventana, reiniciamos el contador
// y enviamos un paquete de confirmación
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// reiniciamos el contador.
connectionRecord.PacketCounter = 0;
// movemos la ventana de transmisión
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// reiniciar el arreglo de control de transmisión
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
API Reliable UDP
Para interactuar con el protocolo de transmisión de datos, existe una clase pública Reliable Udp, que actúa como un envoltorio sobre el bloque de control de transmisión. Estos son los miembros más importantes de la clase:
clase pública sellada ReliableUdp : IDisposable
{
// obtiene el punto final local
public IPEndPoint LocalEndpoint
// crea una instancia de ReliableUdp y comienza
// a escuchar paquetes entrantes en la dirección IP especificada
// y puerto. Un valor de 0 para el puerto significa usar
// un puerto asignado dinámicamente
public ReliableUdp(IPAddress localAddress, int port = 0)
// suscripción para recibir mensajes entrantes
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// cancelación de la suscripción para recibir mensajes
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// enviar mensaje asincrónicamente
// Nota: la compatibilidad con XP y Server 2003 se mantiene, ya que se utiliza .NET Framework 4.0
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// comenzar el envío asincrónico de un mensaje
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// obtener el resultado del envío asincrónico
public bool EndSendMessage(IAsyncResult asyncResult)
// liberar recursos
public void Dispose()
}
La recepción de mensajes se realiza por suscripción. La firma del delegado para el método de devolución de llamada:
public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );Mensaje:
public class ReliableUdpMessage
{
// tipo de mensaje, enumeración simple
public ReliableUdpMessageTypes Type { get; private set; }
// datos del mensaje
public byte[] Body { get; private set; }
// si se establece en verdadero, se desactivará el mecanismo de confirmación de entrega
// para la transmisión de un mensaje específico
public bool NoAsk { get; private set; }
}
Para suscribirse a un tipo específico de mensajes y/o a un remitente específico se utilizan dos parámetros opcionales: ReliableUdpMessageTypes messageType y IPEndPoint ipEndPoint.
Tipos de mensajes:
public enum ReliableUdpMessageTypes : short
{
// Cualquiera
Any = 0,
// Solicitud al servidor STUN
StunRequest = 1,
// Respuesta del servidor STUN
StunResponse = 2,
// Transferencia de archivo
FileTransfer = 3,
// ...
}
El envío de mensajes se realiza de manera asincrónica, para ello el protocolo implementa un modelo de programación asincrónico:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
El resultado del envío del mensaje será verdadero si el mensaje ha llegado correctamente al destinatario y falso si la conexión se cerró por tiempo de espera:
public bool EndSendMessage(IAsyncResult asyncResult)
Conclusión
Mucho no ha sido descrito en este artículo. Los mecanismos de concordancia de flujos, el manejo de excepciones y errores, así como la implementación de métodos asíncronos de envío de mensajes. Pero el núcleo del protocolo, la descripción de la lógica de procesamiento de paquetes, el establecimiento de conexiones y el manejo de tiempos de espera deben quedar claros para usted.
La versión del protocolo de entrega confiable demostrada es bastante robusta y flexible, y cumple con ciertos requisitos que se establecieron anteriormente. Sin embargo, quiero agregar que la implementación descrita puede ser mejorada. Por ejemplo, para aumentar la capacidad de envío y modificar dinámicamente los intervalos de los temporizadores, se pueden añadir mecanismos como la ventana deslizante y RTT; también sería útil implementar un mecanismo para determinar el MTU entre los nodos de conexión (pero solo en caso de enviar mensajes grandes).
Gracias por su atención, espero sus comentarios y observaciones.
P.D. Para aquellos interesados en los detalles o que simplemente quieran probar el protocolo, aquí está el enlace al proyecto en GitHub:
Enlaces útiles y artículos
- Especificación del protocolo TCP: y
- Especificación del protocolo UDP: y
- Discusión sobre el protocolo RUDP:
- Protocolo de datos confiables: y
- Implementación simple de confirmación de entrega sobre UDP:
- Artículo que describe mecanismos para superar NATs:
- Implementación del modelo de programación asíncrona: y
- La transición del modelo de programación asíncrona al patrón asíncrono basado en tareas (APM a TAP):
Actualización: Gracias y por la idea de añadir una tarea a la interfaz. La compatibilidad de la biblioteca con sistemas operativos antiguos se mantiene, ya que el marco 4 admite tanto XP como Server 2003.
Fuente: habr.com
