Implémentation du protocole Reliable Udp pour .Net

Internet a beaucoup changĂ©. L'un des principaux protocoles Internet, le UDP, est utilisĂ© par les applications non seulement pour la diffusion de datagrammes et de messages en broadcast, mais aussi pour Ă©tablir des connexions « peer-to-peer » entre les nƓuds du rĂ©seau. En raison de sa simplicitĂ©, ce protocole a vu apparaĂźtre de nombreuses applications imprĂ©vues. Cependant, ses inconvĂ©nients, tels que l'absence de garantie de livraison, n'ont pas disparu. Cet article dĂ©crit la mise en Ɠuvre d'un protocole de livraison garantie sur UDP.
Contenu :Introduction
Exigences du protocole
En-tĂȘte Reliable UDP
Principes de fonctionnement du protocole
Timeouts et minuteries du protocole
Diagramme d'états de transmission de Reliable UDP
Plongée dans le code. Bloc de contrÎle de transmission
PlongĂ©e dans le code. États

Plongée dans le code. Création et établissement des connexions
Plongée dans le code. Fermeture de la connexion par timeout
Plongée dans le code. Récupération des données
API Reliable UDP
Conclusion
Liens utiles et articles

Introduction

L'architecture initiale de l'Internet supposait un espace d'adressage homogĂšne, oĂč chaque nƓud avait une adresse IP globale et unique, et pouvait communiquer directement avec d'autres nƓuds. Aujourd'hui, l'Internet a, en rĂ©alitĂ©, une architecture diffĂ©rente : un domaine d'adresses IP globales et de nombreux domaines d'adresses privĂ©es, cachĂ©s derriĂšre des dispositifs NAT.Dans une telle architecture, seuls les dispositifs situĂ©s dans l'espace d'adressage global peuvent interagir facilement avec d'autres sur le rĂ©seau, car ils possĂšdent une adresse IP unique et globale routable. Un nƓud situĂ© dans un rĂ©seau privĂ© peut se connecter Ă  d'autres nƓuds au sein de ce mĂȘme rĂ©seau, ainsi qu'Ă  d'autres nƓuds bien connus dans l'espace d'adressage global. Cette interaction est en grande partie rendue possible grĂące au mĂ©canisme de traduction d'adresses rĂ©seau. Les dispositifs NAT, tels que les routeurs Wi-Fi, crĂ©ent des enregistrements spĂ©ciaux dans les tables de traduction pour les connexions sortantes et modifient les adresses IP et les numĂ©ros de ports dans les paquets. Cela permet d'Ă©tablir une connexion sortante depuis le rĂ©seau privĂ© vers des nƓuds dans l'espace d'adressage global. Cependant, en mĂȘme temps, les dispositifs NAT bloquent gĂ©nĂ©ralement tout le trafic entrant, Ă  moins que des rĂšgles spĂ©cifiques ne soient Ă©tablies pour les connexions entrantes.

Cette architecture d'Internet est assez correcte pour l'interaction client-serveur, lorsque les clients peuvent se trouver dans des rĂ©seaux privĂ©s, tandis que les serveurs possĂšdent une adresse globale. Mais cela crĂ©e des difficultĂ©s pour la connexion directe entre deux nƓuds au sein de diffĂ©rents. rĂ©seaux privĂ©s. La connexion directe entre deux nƓuds est essentielle pour les applications « peer-to-peer », telles que la transmission vocale (Skype), l'accĂšs Ă  distance Ă  un ordinateur (TeamViewer) ou les jeux en ligne. L'une des mĂ©thodes les plus efficaces pour Ă©tablir une connexion peer-to-peer entre des dispositifs situĂ©s dans diffĂ©rents rĂ©seaux privĂ©s s'appelle le « hole punching ». Cette technique est le plus souvent utilisĂ©e avec des applications basĂ©es sur le protocole UDP.

Cependant, si votre application nécessite une livraison garantie des données, par exemple, si vous transférez des fichiers entre des ordinateurs, alors l'utilisation d'UDP entraßnera de nombreuses difficultés, car UDP n'est pas un protocole de livraison garantissant la livraison et ne garantit pas l'ordre des paquets, contrairement au protocole TCP.

Dans ce cas, pour garantir la livraison des paquets, il est nĂ©cessaire de mettre en Ɠuvre un protocole de niveau applicatif qui assure la fonctionnalitĂ© requise et fonctionne au-dessus de l'UDP.

Dans une telle situation, pour assurer la livraison garantie des paquets, un protocole de niveau applicatif doit ĂȘtre mis en Ɠuvre, garantissant la fonctionnalitĂ© nĂ©cessaire et fonctionnant par-dessus l'UDP.

Je tiens Ă  souligner qu'il existe une technique de TCP hole punching pour Ă©tablir des connexions TCP entre des nƓuds dans des rĂ©seaux privĂ©s diffĂ©rents, mais en raison du manque de support de nombreux dispositifs NAT, elle n'est gĂ©nĂ©ralement pas considĂ©rĂ©e comme un moyen principal de connexion de ces nƓuds.

Dans cet article, je vais aborder uniquement la mise en Ɠuvre du protocole de livraison garantie. La mise en Ɠuvre de la technique UDP hole punching sera dĂ©crite dans les articles suivants.

Exigences du protocole

  1. La livraison fiable des paquets est mise en Ɠuvre par un mĂ©canisme d'accusĂ© de rĂ©ception positif (dit positive acknowledgment).
  2. La nécessité d'une transmission efficace de grandes quantités de données, c'est-à-dire que le protocole doit éviter les retransmissions inutiles de paquets.
  3. Il doit ĂȘtre possible d'annuler le mĂ©canisme de confirmation de la livraison (capacitĂ© de fonctionner comme un protocole UDP « pur »).
  4. PossibilitĂ© de mettre en Ɠuvre un mode de commande avec confirmation de chaque message.
  5. L'unitĂ© de base de transmission de donnĂ©es doit ĂȘtre un message.

Ces exigences coïncident en grande partie avec les exigences du protocole de données fiables, décrites dans rfc 908 et rfc 1151, et je me suis basé sur ces standards lors de la conception de ce protocole.

Pour comprendre ces exigences, examinons les diagrammes temporels de transmission des donnĂ©es entre deux nƓuds d'un rĂ©seau selon les protocoles TCP et UDP. Supposons que dans les deux cas, un paquet soit perdu.
Transmission de données non interactives via TCP :Implémentation du protocole Reliable Udp pour .Net

Comme le montre le diagramme, en cas de perte de paquets, TCP détectera le paquet perdu et en informera l'expéditeur, en demandant le numéro du segment perdu.
Transmission de données via le protocole UDP :Implémentation du protocole Reliable Udp pour .Net

UDP ne prend aucune mesure pour détecter les pertes. La vérification des erreurs de transmission dans le protocole UDP repose entiÚrement sur l'application.

La dĂ©tection des erreurs dans le protocole TCP est rĂ©alisĂ©e grĂące Ă  l'Ă©tablissement d'une connexion avec le nƓud final, en maintenant l'Ă©tat de cette connexion, en indiquant le numĂ©ro des octets envoyĂ©s dans chaque en-tĂȘte de paquet, et en envoyant des notifications de rĂ©ception par le biais du numĂ©ro d'accusĂ© de rĂ©ception « acknowledge number ».

De plus, pour amĂ©liorer les performances (c'est-Ă -dire envoyer plusieurs segments sans attendre de confirmation), le protocole TCP utilise ce qu'on appelle une fenĂȘtre de transmission — le nombre d'octets de donnĂ©es que l'expĂ©diteur d'un segment s'attend Ă  recevoir.

Pour plus de dĂ©tails sur le protocole TCP, vous pouvez consulter rfc 793, avec UDP dans rfc 768, oĂč ils sont, en fait, dĂ©finis.

Comme il est Ă©vident d'aprĂšs ce qui prĂ©cĂšde, pour crĂ©er un protocole de livraison de messages fiable sur UDP (que nous appellerons dĂ©sormais Reliable UDP), il est nĂ©cessaire de mettre en Ɠuvre des mĂ©canismes de transmission de donnĂ©es similaires Ă  ceux de TCP. À savoir :

  • maintenir l'Ă©tat de la connexion
  • utiliser la numĂ©rotation des segments
  • utiliser des paquets de confirmation spĂ©ciaux
  • utiliser un mĂ©canisme de fenĂȘtre simplifiĂ© pour augmenter le dĂ©bit du protocole

De plus, il est nécessaire :

  • de signaler le dĂ©but du message, pour allouer des ressources Ă  la connexion
  • de signaler la fin du message, pour transmettre le message reçu Ă  l'application supĂ©rieure et libĂ©rer les ressources du protocole
  • de permettre au protocole, pour des connexions spĂ©cifiques, de dĂ©sactiver le mĂ©canisme de confirmation de livraison, afin de fonctionner comme un UDP « pur »

En-tĂȘte Reliable UDP

Rappelons que le datagramme UDP est encapsulé dans un datagramme IP. Le paquet Reliable UDP est donc « enveloppé » dans un datagramme UDP.
Encapsulation de l'en-tĂȘte Reliable UDP :ImplĂ©mentation du protocole Reliable Udp pour .Net

La structure de l'en-tĂȘte Reliable UDP est assez simple :

Implémentation du protocole Reliable Udp pour .Net

  • Flags – les drapeaux de contrĂŽle du paquet
  • MessageType – type de message, utilisĂ© par les applications supĂ©rieures pour s'abonner Ă  des messages spĂ©cifiques
  • TransmissionId — l'identifiant de transmission, qui, avec l'adresse et le port du destinataire, dĂ©finit de maniĂšre unique la connexion
  • PacketNumber – numĂ©ro de paquet
  • Options – options supplĂ©mentaires du protocole. Dans le cas du premier paquet, utilisĂ© pour indiquer la taille du message

Les drapeaux suivants existent :

  • FirstPacket — premier paquet du message
  • NoAsk — le message ne nĂ©cessite pas l'activation du mĂ©canisme de confirmation
  • LastPacket — dernier paquet du message
  • RequestForPacket — paquet de confirmation ou demande d'un paquet perdu

Principes de fonctionnement du protocole

Étant donnĂ© que Reliable UDP est orientĂ© vers la transmission garantie de messages entre deux nƓuds, il doit ĂȘtre capable d'Ă©tablir une connexion avec l'autre partie. Pour Ă©tablir la connexion, l'expĂ©diteur envoie un paquet avec le drapeau FirstPacket, auquel la rĂ©ponse indiquera l'Ă©tablissement de la connexion. Tous les paquets de rĂ©ponse, ou, autrement dit, les paquets de confirmation, affichent toujours la valeur du champ PacketNumber unitĂ© supĂ©rieure Ă  la valeur la plus Ă©levĂ©e de PacketNumber parmi les paquets reçus avec succĂšs. Dans le champ Options, la taille du message est enregistrĂ©e pour le premier paquet envoyĂ©.

Un mécanisme similaire est utilisé pour terminer la connexion. Dans le dernier paquet de messages, le drapeau LastPacket est défini. Dans le paquet de réponse, le numéro du dernier paquet + 1 est indiqué, ce qui signifie pour la partie réceptrice que le message a été livré avec succÚs.
Diagramme d'établissement et de terminaison de la connexion :Implémentation du protocole Reliable Udp pour .Net

Lorsque la connexion est Ă©tablie, le transfert de donnĂ©es commence. Les donnĂ©es sont transmises par blocs de paquets. Chaque bloc, sauf le dernier, contient un nombre fixe de paquets. Ce nombre est Ă©gal Ă  la taille de la fenĂȘtre de rĂ©ception/transmission. Le dernier bloc de donnĂ©es peut contenir moins de paquets. AprĂšs l'envoi de chaque bloc, la partie Ă©mettrice attend une confirmation de livraison ou une demande de renvoi des paquets perdus, laissant la fenĂȘtre de rĂ©ception/transmission ouverte pour recevoir des rĂ©ponses. AprĂšs rĂ©ception de la confirmation de livraison du bloc, la fenĂȘtre de rĂ©ception/transmission est dĂ©placĂ©e et le prochain bloc de donnĂ©es est envoyĂ©.

La partie rĂ©ceptrice accepte les paquets. Chaque paquet est vĂ©rifiĂ© pour sa conformitĂ© Ă  la fenĂȘtre de transmission. Les paquets ne correspondant pas Ă  la fenĂȘtre et les doublons sont rejetĂ©s. Comme la taille de la fenĂȘtre est strictement fixe et identique pour le rĂ©cepteur et l'Ă©metteur, en cas de livraison d'un bloc de paquets sans pertes, la fenĂȘtre est dĂ©placĂ©e pour recevoir les paquets du prochain bloc de donnĂ©es et une confirmation de livraison est envoyĂ©e. Si la fenĂȘtre n'est pas remplie dans le dĂ©lai fixĂ© par le minuteur de travail, une vĂ©rification sera lancĂ©e pour dĂ©terminer quels paquets n'ont pas Ă©tĂ© livrĂ©s et des demandes de renvoi seront envoyĂ©es.
Diagramme de retransmission :Implémentation du protocole Reliable Udp pour .Net

Timeouts et minuteries du protocole

Il existe plusieurs raisons pour lesquelles une connexion ne peut pas ĂȘtre Ă©tablie. Par exemple, si la partie rĂ©ceptrice est hors ligne. Dans ce cas, lors de la tentative d'Ă©tablissement de la connexion, celle-ci sera fermĂ©e en raison d'un dĂ©passement de dĂ©lai. Dans la mise en Ɠuvre de Reliable UDP, deux minuteurs sont utilisĂ©s pour Ă©tablir les dĂ©lais. Le premier, le minuteur de travail, est utilisĂ© pour attendre la rĂ©ponse de l'hĂŽte distant. S'il expire du cĂŽtĂ© de l'Ă©metteur, le dernier paquet envoyĂ© est renvoyĂ©. Si, cependant, le minuteur expire chez le rĂ©cepteur, une vĂ©rification des paquets perdus est effectuĂ©e et des demandes de renvoi sont envoyĂ©es.

Le second minuteur est nĂ©cessaire pour fermer la connexion en cas d'absence de communication entre les nƓuds. Pour le cĂŽtĂ© expĂ©diteur, il se dĂ©clenche immĂ©diatement aprĂšs le dĂ©clenchement du minuteur de travail et attend une rĂ©ponse du nƓud distant. En l'absence de rĂ©ponse dans le dĂ©lai imparti, la connexion est terminĂ©e et les ressources sont libĂ©rĂ©es. Du cĂŽtĂ© rĂ©cepteur, le minuteur de fermeture de connexion se dĂ©clenche aprĂšs le double dĂ©clenchement du minuteur de travail. Cela est nĂ©cessaire pour Ă©viter la perte du paquet de confirmation. Lorsque le minuteur se dĂ©clenche, la connexion est Ă©galement terminĂ©e et les ressources sont libĂ©rĂ©es.

Diagramme d'états de transmission de Reliable UDP

Les principes de fonctionnement du protocole sont réalisés dans un automate à états, chaque état ayant une logique de traitement spécifique des paquets.
Diagramme des états du Reliable UDP :

Implémentation du protocole Reliable Udp pour .Net

FermĂ© – n'est en rĂ©alitĂ© pas un Ă©tat, c'est un point de dĂ©part et de terminaison pour l'automate. L'Ă©tat FermĂ© est considĂ©rĂ© comme le bloc de gestion de transmission, qui, en rĂ©alisant un serveur UDP asynchrone, redirige les paquets vers les connexions appropriĂ©es et dĂ©marre le traitement des Ă©tats.

EnvoiPremierPaquet – Ă©tat initial dans lequel se trouve la connexion sortante lors de l'envoi d'un message.

Dans cet Ă©tat, le premier paquet pour les messages ordinaires est envoyĂ©. Pour les messages sans accusĂ© de rĂ©ception, c'est le seul Ă©tat – dans celui-ci, l'ensemble du message est envoyĂ©.

CycleEnvoi – Ă©tat principal pour la transmission des paquets du message.

La transition vers cet Ă©tat depuis l'Ă©tat EnvoiPremierPaquet se fait aprĂšs l'envoi du premier paquet de message. C'est prĂ©cisĂ©ment dans cet Ă©tat que parviennent tous les accusĂ©s de rĂ©ception et les demandes de retransmission. La sortie de cet Ă©tat est possible pour deux raisons – en cas de livraison rĂ©ussie du message ou Ă  cause d'un dĂ©lai d'attente.

PremierPaquetReçu – Ă©tat initial pour le destinataire du message.

Ici, la correction du début de la transmission est vérifiée, les structures nécessaires sont créées et un accusé de réception du premier paquet est envoyé.

Pour un message constituĂ© d'un seul paquet et envoyĂ© sans confirmation de livraison – c'est le seul Ă©tat. AprĂšs le traitement de ce type de message, la connexion est fermĂ©e.

Assemblage – Ă©tat principal pour la rĂ©ception des paquets du message.

Il enregistre les paquets dans un stockage temporaire, vérifie la perte de paquets, envoie des accusés de réception pour les blocs de paquets et les messages dans leur intégralité, et envoie des demandes de retransmission des paquets perdus. En cas de réception réussie de l'ensemble du message, la connexion passe à l'état Terminé, sinon, il sort par délai d'attente.

TerminĂ© – fermeture de la connexion en cas de rĂ©ception rĂ©ussie de l'ensemble du message.

Cet Ă©tat est nĂ©cessaire pour assembler le message et pour les cas oĂč l'accusĂ© de rĂ©ception du message a Ă©tĂ© perdu en route vers l'expĂ©diteur. La sortie de cet Ă©tat se fait par dĂ©lai d'attente, mais la connexion est considĂ©rĂ©e comme correctement fermĂ©e.

Plongée dans le code. Bloc de contrÎle de transmission

Un des Ă©lĂ©ments clĂ©s de Reliable UDP est le bloc de contrĂŽle de transmission. La tĂąche de ce bloc est de stocker les connexions en cours et les Ă©lĂ©ments auxiliaires, de distribuer les paquets entrants aux connexions appropriĂ©es, de fournir une interface pour envoyer des paquets Ă  la connexion et de mettre en Ɠuvre l'API du protocole. Le bloc de contrĂŽle de transmission reçoit des paquets du niveau UDP et les redirige vers le traitement dans l'automate Ă  Ă©tats. Un serveur UDP asynchrone est mis en Ɠuvre pour recevoir les paquets.
Quelques membres de la classe ReliableUdpConnectionControlBlock :

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // tableau de bytes pour la clé spécifiée. Utilisé pour assembler les messages entrants
  public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set; }
  // tableau de bytes pour la clé spécifiée. Utilisé pour envoyer des messages sortants.
  public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
  // enregistrement de la connexion pour la clé spécifiée.
  private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
  // liste des abonnés aux messages.
  private readonly List m_subscribers;
  // socket local
  private Socket m_socketIn;
  // port pour les messages entrants
  private int m_port;
  // adresse IP locale
  private IPAddress m_ipAddress;
  // point de terminaison local
  public IPEndPoint LocalEndpoint { get; private set; }
  // collection d'états préalablement initialisés
  // de l'automate
  public StatesCollection States { get; private set; }
  // générateur de nombres aléatoires. Utilisé pour créer TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;
  	//...
}

Implémentation d'un serveur UDP asynchrone :

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // Création d'un nouveau tampon pour chaque socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // Transmission du tampon en tant que paramÚtre pour la méthode asynchrone
  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);
  // Le paquet a Ă©tĂ© reçu, prĂȘt Ă  en accepter un suivant        
  Receive();
  // Puisque la façon la plus simple de résoudre la question du tampon - obtenir une référence à celui-ci 
  // depuis IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // Obtention de l'en-tĂȘte du paquet        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // Un paquet incorrect a été reçu - on l'ignore
    return;
  }
  // Construction de la clé pour déterminer le record de connexion pour le paquet
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // Obtention du record de connexion existant ou création d'un nouveau
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // Démarrage du traitement du paquet dans l'automate final
  record.State.ReceivePacket(record, header, bytes);
}

Pour chaque transmission de message, une structure contenant des informations sur la connexion est créée. Cette structure s'appelle record de connexion.
Certains membres de la classe ReliableUdpConnectionRecord :

classe interne ReliableUdpConnectionRecord : IDisposable
{    
  // tableau d'octets avec le message    
  public byte[] IncomingStream { get; set; }
  // référence à l'état de l'automate
  public ReliableUdpState State { get; set; }    
  // paire qui identifie de maniĂšre unique l'enregistrement de connexion
  // dans le bloc de contrĂŽle de transmission     
  public Tuple Key { get; private set;}
  // limite infĂ©rieure de la fenĂȘtre de rĂ©ception    
  public int WindowLowerBound;
  // taille de la fenĂȘtre de transmission
  public readonly int WindowSize;     
  // numéro de paquet à envoyer
  public int SndNext;
  // nombre de paquets Ă  envoyer
  public int NumberOfPackets;
  // numéro de transmission (c'est la seconde partie de Tuple)
  // chaque message a son propre	
  public readonly Int32 TransmissionId;
  // point de terminaison IP distant – le rĂ©cepteur du message
  public readonly IPEndPoint RemoteClient;
  // taille du paquet, pour éviter la fragmentation au niveau IP
  // ne doit pas dĂ©passer MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // bloc de contrĂŽle de transmission
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // encapsule les résultats de l'opération asynchrone pour BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // ne pas envoyer de paquets d'accusé de réception
  public bool IsNoAnswerNeeded;
  // dernier paquet correctement reçu (toujours défini sur le plus grand numéro)
  public int RcvCurrent;
  // tableau des numéros de paquets perdus
  public int[] LostPackets { get; private set; }
  // le dernier paquet est-il arrivé. Utilisé comme bool.
  public int IsLastPacketReceived = 0;
  //...
}

PlongĂ©e dans le code. États

Les Ă©tats implĂ©mentent l'automate du protocole Reliable UDP, oĂč se dĂ©roule le traitement principal des paquets. La classe abstraite ReliableUdpState fournit une interface pour l'Ă©tat :

Implémentation du protocole Reliable Udp pour .Net

Toute la logique de fonctionnement du protocole est rĂ©alisĂ©e par les classes prĂ©sentĂ©es ci-dessus, en collaboration avec une classe d'assistance fournissant des mĂ©thodes statiques, telles que, par exemple, la construction de l'en-tĂȘte ReliableUdp Ă  partir de l'enregistrement de connexion.

Les mises en Ɠuvre des mĂ©thodes d'interface qui dĂ©finissent les principaux algorithmes de fonctionnement du protocole seront examinĂ©es en dĂ©tail.

Méthode DisposeByTimeout

La méthode DisposeByTimeout est responsable de la libération des ressources de connexion à l'expiration du délai d'attente et pour signaler la livraison réussie/échouée du message.
ReliableUdpState.DisposeByTimeout :

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

Il est redéfini uniquement dans l'état Terminé.
Completed.DisposeByTimeout :

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // nous signalons la réception réussie du message
  SetAsCompleted(connectionRecord);
}

Méthode ProcessPackets

La méthode ProcessPackets est responsable du traitement supplémentaire d'un paquet ou de plusieurs paquets. Elle est appelée directement ou via un minuteur d'attente des paquets.

Dans l'état Assemblage la méthode est redéfinie et est chargée de vérifier les paquets perdus et de passer à l'état Terminé, en cas de réception du dernier paquet et de vérification réussie
Assembling.ProcessPackets :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // il y a des paquets perdus, nous envoyons des demandes pour eux
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // nous réglons le minuteur une seconde fois, pour tenter de renvoyez
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // si aprĂšs deux tentatives de WaitForPacketTimer
    // nous n'avons pas réussi à obtenir les paquets - lançons le minuteur de fermeture de connexion
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // vérification réussie
  {
    // nous envoyons un accusé de réception de la réception du bloc de données
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // au lieu de mettre immĂ©diatement en Ɠuvre les ressources
    // nous lançons le minuteur, au cas oĂč
    // si le dernier ack n'atteint pas l'expéditeur et qu'il le demande à nouveau.
    // au dĂ©clenchement du minuteur - nous mettons en Ɠuvre les ressources
    // dans l'état Complété, la méthode du minuteur est redéfinie
    StartCloseWaitTimer(connectionRecord);
  }
  // c'est le cas oĂč l'ack du bloc de paquets a Ă©tĂ© perdu
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // nous lançons le minuteur de fermeture de connexion
    StartCloseWaitTimer(connectionRecord);
  }
}

Dans l'état CycleEnvoi cette méthode est appelée uniquement par le minuteur, et est responsable de la réexpédition du dernier message, ainsi que de l'activation du minuteur de fermeture de connexion.
SendingCycle.ProcessPackets :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  // Envoi du dernier paquet Ă  nouveau
  // (En cas de restauration de la connexion, le nƓud rĂ©cepteur renverra les demandes qui n'ont pas Ă©tĂ© reçues)
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // Démarre le timer CloseWait - pour attendre la restauration ou la clÎture de la connexion
  StartCloseWaitTimer(connectionRecord);
}

Dans l'Ă©tat TerminĂ© Cette mĂ©thode arrĂȘte le timer de travail et transmet le message aux abonnĂ©s.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // Assemble le message et le transmet aux abonnés
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Méthode ReceivePacket

Dans l'état PremierPaquetReçu La principale tùche de la méthode est de déterminer si le premier paquet du message a réellement été reçu sur l'interface, ainsi que de rassembler un message composé d'un seul paquet.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // nous rejetons le paquet
    return;
  // la combinaison des deux drapeaux - FirstPacket et LastPacket - indique que nous avons un seul message
  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))
    {
      // envoyer le paquet d'accusé de réception          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // par conception, tous les numéros de paquet commencent à 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // comptons le nombre de paquets qui doivent arriver
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // enregistrons le numéro du dernier paquet reçu (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // dĂ©calons la fenĂȘtre de rĂ©ception de 1
  connectionRecord.WindowLowerBound++;
  // changeons d'état
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // si le mécanisme d'accusé de réception n'est pas nécessaire
  // démarrons un minuteur qui libérera toutes les structures         
  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);
  }
}

Dans l'état CycleEnvoi Cette méthode est redéfinie pour recevoir des confirmations de livraison et des demandes de retransmission.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // calcul de la limite supĂ©rieure de la fenĂȘtre
  // on prend la limite de la fenĂȘtre + 1, pour obtenir les confirmations de livraison
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // vĂ©rification de l'appartenance Ă  la fenĂȘtre
  if (header.PacketNumber  windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // vérifier le dernier paquet :
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // transmission terminée
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // c'est une réponse au premier paquet avec confirmation
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // sans dĂ©calage de fenĂȘtre
    SendPacket(connectionRecord);
  }
  // confirmation de la réception du bloc de données
  else if (header.PacketNumber == windowHighestBound)
  {
    // dĂ©calage de la fenĂȘtre d rĂ©ception/envoi
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // réinitialisation du tableau de contrÎle de transmission
    connectionRecord.WindowControlArray.Nullify();
    // envoi du bloc de paquets
    SendPacket(connectionRecord);
  }
  // c'est une demande de retransmission - on envoie le paquet demandé
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Dans l'état Assemblage La méthode ReceivePacket effectue le travail principal de l'assemblage du message à partir des paquets reçus.
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // traitement des paquets avec le mécanisme de confirmation de livraison désactivé
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // réinitialiser le minuteur
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // enregistrer les données
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // si nous avons reçu un paquet avec le dernier indicateur - finaliser
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }
  // calcul de la limite supĂ©rieure de la fenĂȘtre
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // ignorer les paquets en dehors de la fenĂȘtre
  if (header.PacketNumber  (windowHighestBound))
    return;
  // ignorer les doublons
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // enregistrer les données 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // incrémenter le compteur de paquets        
  connectionRecord.PacketCounter++;
  // enregistrer le numĂ©ro de paquet actuel dans le tableau de contrĂŽle de fenĂȘtre        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // établir le plus grand paquet reçu        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // redémarrer les minuteurs        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // si le dernier paquet est arrivé
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // si nous avons reçu tous les paquets de la fenĂȘtre, alors rĂ©initialiser le compteur
  // et envoyer un paquet de confirmation
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // réinitialiser le compteur.      
    connectionRecord.PacketCounter = 0;
    // dĂ©caler la fenĂȘtre de transmission
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // réinitialisation du tableau de contrÎle de transmission
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // si le dernier paquet est déjà reçu        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // vérifier les paquets          
    ProcessPackets(connectionRecord);
  }
}

Dans l'état Terminé La seule tùche de la méthode est d'envoyer une nouvelle confirmation de la réception réussie du message.
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // renvoi du dernier paquet en raison de
  // le dernier ack n'est pas arrivé à l'expéditeur
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Méthode SendPacket

Dans l'Ă©tat EnvoiPremierPaquet Cette mĂ©thode envoie le premier paquet de donnĂ©es, ou, si le message ne nĂ©cessite pas de confirmation de livraison — tout le message.
FirstPacketSending.SendPacket :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // si la confirmation n'est pas nécessaire - envoyons tous les paquets
  // et libérons les ressources
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Ici, l'envoi se fait tel quel
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // crĂ©ons l'en-tĂȘte du paquet et l'envoyons
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // augmentons le compteur
  connectionRecord.SndNext++;
  // dĂ©calons la fenĂȘtre
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Démarrons le minuteur
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Dans l'état CycleEnvoi Dans cette méthode, un bloc de paquets est envoyé.
SendingCycle.SendPacket :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // envoi d'un bloc de paquets      
  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 cas de grande fenĂȘtre de transmission, redĂ©marrons le minuteur aprĂšs l'envoi
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Plongée dans le code. Création et établissement des connexions

Maintenant que nous avons fait connaissance avec les principaux états et méthodes utilisés pour gérer les états, nous pouvons examiner en détail quelques exemples de fonctionnement du protocole.
Diagramme de transmission des données dans des conditions normales :Implémentation du protocole Reliable Udp pour .Net

Examinons en détail la création record de connexion pour établir une connexion et envoyer le premier paquet. L'initiateur de la transmission est toujours l'application qui appelle la méthode API pour envoyer le message. Ensuite, la méthode StartTransmission du bloc de contrÎle de transmission est activée, lançant la transmission de données pour le nouveau message.
Création d'une connexion sortante :

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "Vous devez utiliser un constructeur avec des paramÚtres ou démarrer l'écoute avant d'envoyer un message" );
    }
    // démarrer le traitement des paquets entrants
    StartListener(LocalEndpoint);
  }
  // créer une clé pour le dictionnaire, basée sur EndPoint et ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // générer un numéro aléatoire transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // créer un nouvel enregistrement pour la connexion et vérifier, 
  // si un tel numéro existe déjà dans nos dictionnaires
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // si cela existe – rĂ©gĂ©nĂ©rer le numĂ©ro alĂ©atoire 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // si encore Ă©chouĂ© – gĂ©nĂ©rer une exception
      throw new ArgumentException("La paire TransmissionId & EndPoint existe déjà dans le dictionnaire");
  }
  // état lancé pour traitement         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Envoi du premier paquet (état FirstPacketSending) :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // crĂ©er l'en-tĂȘte du paquet et l'envoyer 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // augmenter le compteur
  connectionRecord.SndNext++;
  // dĂ©caler la fenĂȘtre
  connectionRecord.WindowLowerBound++;
  // passer à l'état SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Démarrer le minuteur
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

AprĂšs l'envoi du premier paquet, l'expĂ©diteur passe Ă  l'Ă©tat CycleEnvoi – attendre la confirmation de la livraison du paquet.
Le cĂŽtĂ© rĂ©cepteur, Ă  l'aide de la mĂ©thode EndReceive, accepte le paquet envoyĂ©, crĂ©e un nouveau record de connexion et transmet ce paquet, avec l'en-tĂȘte prĂ©alablement analysĂ©, au traitement de la mĂ©thode ReceivePacket de l'Ă©tat PremierPaquetReçu
Création d'une connexion cÎté réception :

private void EndReceive(IAsyncResult ar)
{
  // ...
  // paquet reçu
  // analyse de l'en-tĂȘte du paquet
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {
    // un paquet incorrect est arrivé - nous le rejetons
    return;
  }
  // construit la clé pour déterminer l'enregistrement de connexion pour le paquet
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // récupÚre l'enregistrement de connexion existant ou en crée un nouveau
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // lance le traitement du paquet dans la machine d'état
  record.State.ReceivePacket(record, header, bytes);
}

Réception du premier paquet et envoi de l'accusé de réception (état FirstPacketReceived) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // rejetons le paquet
    return;
  // ...
  // par conception tous les numéros de paquet commencent à 0;
  if (header.PacketNumber != 0)
    return;
  // initialise le tableau pour stocker les parties du message
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // enregistre les données du paquet dans le tableau
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // calcule le nombre de paquets qui doivent arriver
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
  // enregistre le numéro du dernier paquet reçu (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // aprĂšs cela, dĂ©place la fenĂȘtre de rĂ©ception de 1
  connectionRecord.WindowLowerBound++;
  // passe à l'état suivant
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  if (/*si le mécanisme de confirmation n'est pas requis*/)
  // ...
  else
  {
    // envoie l'accusé de réception
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Plongée dans le code. Fermeture de la connexion par timeout

Le traitement des dĂ©lais d'expiration est une partie importante de Reliable UDP. ConsidĂ©rons un exemple dans lequel un nƓud intermĂ©diaire a Ă©chouĂ© et la livraison des donnĂ©es dans les deux directions est devenue impossible.
Diagramme de fermeture de connexion par délai d'expiration :Implémentation du protocole Reliable Udp pour .Net

Comme le montre le diagramme, le minuteur de travail de l'expéditeur s'active immédiatement aprÚs l'envoi du bloc de paquets. Cela se produit dans la méthode SendPacket de l'état CycleEnvoi.
Activation du minuteur de travail (état SendingCycle) :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // envoie un bloc de paquets   
  // ...   
  // redémarre le minuteur aprÚs l'envoi
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
}

Les périodes du minuteur sont définies lors de l'établissement de la connexion. Par défaut, ShortTimerPeriod est égal à 5 secondes. Dans l'exemple, il est fixé à 1,5 seconde.

Pour une connexion entrante, le minuteur se déclenche aprÚs la réception du dernier paquet de données reçu, ce qui se produit dans la méthode ReceivePacket de l'état. Assemblage
Activation du minuteur de travail (état Assembling) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // redémarre les minuteurs        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Dans la connexion entrante, aucun paquet n'est arrivé pendant la période d'attente du minuteur de travail. Le minuteur a été déclenché et a appelé la méthode ProcessPackets, dans laquelle des paquets perdus ont été détectés et des demandes de retransmission ont été envoyées pour la premiÚre fois.
Envoi de demandes de retransmission (état Assembling) :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/* vérification des paquets perdus */)
  {
    // envoi des demandes de retransmission
    // réinitialisation du minuteur pour une deuxiÚme tentative d'envoi
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // si aprĂšs deux tentatives de WaitForPacketTimer 
    // les paquets n'ont pas été reçus - démarre le minuteur de fermeture de connexion
    StartCloseWaitTimer(connectionRecord);
  }
  else if (/* dernier paquet reçu et vérification réussie */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // si l'ack sur le bloc de paquets a été perdu
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // renvoyer l'ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // démarre le minuteur de fermeture de connexion
    StartCloseWaitTimer(connectionRecord);
  }
}

La variable TimerSecondTry s'est établie à true. Cette variable est responsable du redémarrage du minuteur de travail.

Du cÎté de l'expéditeur, le minuteur de travail se déclenche également et le dernier paquet envoyé est renvoyé.
Activation du minuteur de fermeture de connexion (état SendingCycle) :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // renvoyer le dernier paquet 
  // ...        
  // dĂ©marrer le minuteur CloseWait – pour attendre la rĂ©cupĂ©ration de la connexion ou sa fermeture
  StartCloseWaitTimer(connectionRecord);
}

AprÚs quoi un minuteur de fermeture de connexion est lancé dans la connexion sortante.
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);
}

La période d'attente du minuteur de fermeture de connexion est par défaut de 30 secondes.

AprÚs un court laps de temps, le minuteur de travail cÎté récepteur se déclenche à nouveau, des demandes sont envoyées, aprÚs quoi le minuteur de fermeture de connexion de la connexion entrante est lancé.

Lors de l'activation des minuteurs de fermeture, toutes les ressources des deux enregistrements de connexion sont libérées. L'expéditeur signale une livraison échouée à l'application supérieure (voir l'API Reliable UDP).
Libération des ressources de l'enregistrement de connexion :

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

Plongée dans le code. Récupération des données

Diagramme de récupération de transmission de données en cas de perte de paquet :Implémentation du protocole Reliable Udp pour .Net

Comme déjà abordé dans la fermeture de la connexion par délai d'attente, à l'expiration du minuteur de travail, le récepteur effectuera une vérification des paquets perdus. En cas de pertes de paquets, une liste des numéros de paquets non reçus sera établie. Ces numéros sont ajoutés au tableau LostPackets de la connexion spécifique et des demandes de retransmission sont envoyées.
Envoi de demandes de retransmission de paquets (état Assembling) :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // il y a des paquets perdus, nous envoyons des demandes pour eux
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

L'expéditeur recevra la demande de retransmission et renverra les paquets manquants. Il convient de noter qu'à ce moment-là, l'expéditeur a déjà lancé le minuteur de fermeture de connexion et, lors de la réception de la demande, il est réinitialisé.
Renvoi des paquets perdus (état SendingCycle) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // réinitialiser le minuteur de fermeture de la connexion 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // c'est une demande de retransmission – nous envoyons le paquet requis          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Le paquet retransmis (packet#3 sur le diagramme) est reçu par la connexion entrante. Une vĂ©rification est effectuĂ©e pour remplir la fenĂȘtre de rĂ©ception et le transfert de donnĂ©es normal reprend.
VĂ©rification de l'entrĂ©e dans la fenĂȘtre de rĂ©ception (Ă©tat Assembling) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // augmentons le compteur de paquets        
  connectionRecord.PacketCounter++;
  // enregistrons dans le tableau de contrĂŽle de la fenĂȘtre le numĂ©ro actuel du paquet        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // définissons le plus grand paquet reçu        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // redémarrons les minuteurs        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // si tous les paquets de la fenĂȘtre sont reçus, rĂ©initialisons le compteur
  // et envoyons le paquet d'accusé de réception
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // réinitialisons le compteur.      
    connectionRecord.PacketCounter = 0;
    // dĂ©calage de la fenĂȘtre de transmission
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // réinitialisation du tableau de contrÎle de la transmission
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API Reliable UDP

Pour interagir avec le protocole de transmission de données, il existe une classe ouverte Reliable Udp, qui est un wrapper autour du bloc de contrÎle de transmission. Voici les membres les plus importants de la classe :

classe publique scellée ReliableUdp : IDisposable
{
  // récupÚre le point de terminaison local
  public IPEndPoint LocalEndpoint    
  // crée une instance de ReliableUdp et commence
  // à écouter les paquets entrants à l'adresse IP spécifiée
  // et au port. Une valeur de 0 pour le port signifie utiliser
  // un port attribué dynamiquement
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // abonnement pour recevoir des messages entrants
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // dĂ©sabonnement pour arrĂȘter de recevoir des messages
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // envoyer un message de maniĂšre asynchrone 
  // Remarque : la compatibilité avec XP et Server 2003 est maintenue, car .NET Framework 4.0 est utilisé
  public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // commencer l'envoi asynchrone d'un message
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // obtenir le résultat de l'envoi asynchrone
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // libérer les ressources
  public void Dispose()    
}

La réception d'un message se fait par abonnement. La signature du délégué pour la méthode de rappel :

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

Message :

public class ReliableUdpMessage
{
  // type de message, simple énumération
  public ReliableUdpMessageTypes Type { get; private set; }
  // données du message
  public byte[] Body { get; private set; }
  // si dĂ©fini Ă  true – le mĂ©canisme de confirmation de livraison sera dĂ©sactivĂ©
  // pour l'envoi d'un message spécifique
  public bool NoAsk { get; private set; }
}

Pour s'abonner à un type de message spécifique et/ou à un expéditeur spécifique, deux paramÚtres optionnels sont utilisés : ReliableUdpMessageTypes messageType et IPEndPoint ipEndPoint.

Types de messages :

public enum ReliableUdpMessageTypes : short
{ 
  // Tout
  Any = 0,
  // RequĂȘte au serveur STUN 
  StunRequest = 1,
  // Réponse du serveur STUN
  StunResponse = 2,
  // Transmission de fichier
  FileTransfer = 3,
  // ...
}

L'envoi d'un message se fait de maniÚre asynchrone, pour cela, un modÚle de programmation asynchrone est implémenté dans le protocole :

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

Le rĂ©sultat de l'envoi du message sera true – si le message a Ă©tĂ© correctement reçu par le destinataire et false – si la connexion a Ă©tĂ© fermĂ©e en raison d'un dĂ©lai d'attente :

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusion

Beaucoup de choses n'ont pas Ă©tĂ© dĂ©crites dans cet article. Les mĂ©canismes de coordination des flux, le traitement des exceptions et des erreurs, et la mise en Ɠuvre de mĂ©thodes asynchrones pour l'envoi de messages. Cependant, le noyau du protocole, la description de la logique de traitement des paquets, l'Ă©tablissement de connexions et le traitement des dĂ©lais d'expiration doivent ĂȘtre clarifiĂ©s pour vous.

La version du protocole de livraison fiable dĂ©montrĂ©e est suffisamment stable et flexible, rĂ©pondant ainsi Ă  certaines exigences dĂ©finies prĂ©cĂ©demment. Cependant, je tiens Ă  ajouter que l'implĂ©mentation dĂ©crite peut ĂȘtre amĂ©liorĂ©e. Par exemple, pour augmenter la bande passante et modifier dynamiquement les pĂ©riodes des minuteries, des mĂ©canismes tels que la fenĂȘtre glissante et le RTT peuvent ĂȘtre ajoutĂ©s au protocole. Il serait Ă©galement utile de mettre en Ɠuvre un mĂ©canisme de dĂ©termination de la MTU entre les nƓuds de connexion (mais seulement en cas d'envoi de gros messages).

Merci de votre attention, j'attends vos commentaires et suggestions.

P.S. Pour ceux qui s'intéressent aux détails ou souhaitent simplement tester le protocole, voici le lien vers le projet sur GitHub :
Projet Reliable UDP

Liens utiles et articles

  1. Spécification du protocole TCP : en anglais et en russe
  2. Spécification du protocole UDP : en anglais et en russe
  3. Discussion sur le protocole RUDP : draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol : rfc 908 et rfc 1151
  5. Mise en Ɠuvre simple de la confirmation de livraison par UDP : Prenez le contrĂŽle total de vos rĂ©seaux avec .NET et UDP
  6. Article décrivant les mécanismes de contournement des NAT : Communication entre pairs à travers des traducteurs d'adresses réseau
  7. Mise en Ɠuvre du modĂšle de programmation asynchrone : ImplĂ©mentation du modĂšle de programmation asynchrone CLR et Comment implĂ©menter le modĂšle de conception IAsyncResult
  8. Transférer le modÚle de programmation asynchrone vers un modÚle asynchrone basé sur des tùches (APM en TAP) :
    TPL et programmation asynchrone traditionnelle .NET
    Interopérabilité avec d'autres modÚles et types asynchrones

Mise à jour : Merci mayorovp et sidristij pour l'idée d'ajouter une tùche à l'interface. La compatibilité de la bibliothÚque avec les anciens systÚmes d'exploitation n'est pas compromise, car le framework 4 prend en charge XP et le serveur 2003.

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster