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
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
- La livraison fiable des paquets est mise en Ćuvre par un mĂ©canisme d'accusĂ© de rĂ©ception positif (dit positive acknowledgment).
- 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.
- Il doit ĂȘtre possible d'annuler le mĂ©canisme de confirmation de la livraison (capacitĂ© de fonctionner comme un protocole UDP « pur »).
- PossibilitĂ© de mettre en Ćuvre un mode de commande avec confirmation de chaque message.
- 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 et , 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 :
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 :
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 , avec UDP dans , 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 :
La structure de l'en-tĂȘte Reliable UDP est assez simple :

- 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 :
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 :
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 :

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 :

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 :
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 :
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 :
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 :
Liens utiles et articles
- Spécification du protocole TCP : et
- Spécification du protocole UDP : et
- Discussion sur le protocole RUDP :
- Reliable Data Protocol : et
- Mise en Ćuvre simple de la confirmation de livraison par UDP :
- Article décrivant les mécanismes de contournement des NAT :
- Mise en Ćuvre du modĂšle de programmation asynchrone : et
- Transférer le modÚle de programmation asynchrone vers un modÚle asynchrone basé sur des tùches (APM en TAP) :
Mise à jour : Merci et 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
