Implémentation du protocole Reliable Udp pour .Net

Internet a changé depuis longtemps. L'un des principaux protocoles d'Internet - UDP est utilisé par les applications non seulement pour fournir des datagrammes et des diffusions, mais également pour fournir des connexions "peer-to-peer" entre les nœuds du réseau. En raison de sa conception simple, ce protocole a de nombreuses utilisations auparavant non planifiées, cependant, les lacunes du protocole, telles que l'absence de livraison garantie, n'ont disparu nulle part. Cet article décrit la mise en œuvre du protocole de livraison garantie sur UDP.
Table des matières:Entrée
Exigences du protocole
En-tête UDP fiable
Principes généraux du protocole
Délais d'attente et minuteries de protocole
Diagramme d'état de transmission UDP fiable
Plus profondément dans le code. unité de commande de transmission
Plus profondément dans le code. États

Plus profondément dans le code. Créer et établir des connexions
Plus profondément dans le code. Fermeture de la connexion à l'expiration du délai
Plus profondément dans le code. Restauration du transfert de données
API UDP fiable
Conclusion
Liens et articles utiles

Entrée

L'architecture originale d'Internet supposait un espace d'adressage homogène dans lequel chaque nœud avait une adresse IP globale et unique et pouvait communiquer directement avec d'autres nœuds. Aujourd'hui, Internet a en fait une architecture différente - une zone d'adresses IP globales et de nombreuses zones avec des adresses privées cachées derrière des appareils NAT.Dans cette architecture, seuls les périphériques de l'espace d'adressage global peuvent facilement communiquer avec n'importe qui sur le réseau, car ils disposent d'une adresse IP unique et globalement routable. Un nœud sur un réseau privé peut se connecter à d'autres nœuds sur le même réseau, et peut également se connecter à d'autres nœuds bien connus dans l'espace d'adressage global. Cette interaction est obtenue en grande partie grâce au mécanisme de traduction d'adresse réseau. Les périphériques NAT, tels que les routeurs Wi-Fi, créent des entrées de table de traduction spéciales pour les connexions sortantes et modifient les adresses IP et les numéros de port dans les paquets. Cela autorise les connexions sortantes du réseau privé vers les hôtes dans l'espace d'adressage global. Mais en même temps, les périphériques NAT bloquent généralement tout le trafic entrant à moins que des règles distinctes pour les connexions entrantes ne soient définies.

Cette architecture d'Internet est suffisamment correcte pour la communication client-serveur, où les clients peuvent être dans des réseaux privés et les serveurs ont une adresse globale. Mais cela crée des difficultés pour la connexion directe de deux nœuds entre différent réseaux privés. Une connexion directe entre deux nœuds est importante 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 appareils sur différents réseaux privés est appelée perforation. Cette technique est le plus souvent utilisée avec des applications basées sur le protocole UDP.

Mais si votre application a besoin d'une livraison garantie de données, par exemple, vous transférez des fichiers entre ordinateurs, l'utilisation d'UDP rencontrera de nombreuses difficultés du fait qu'UDP n'est pas un protocole de livraison garantie et ne fournit pas de livraison de paquets dans l'ordre, contrairement au TCP protocole.

Dans ce cas, pour assurer la livraison garantie des paquets, il est nécessaire d'implémenter un protocole de couche application qui fournit les fonctionnalités nécessaires et fonctionne sur UDP.

Je tiens à noter tout de suite qu'il existe une technique de perforation TCP pour établir des connexions TCP entre les nœuds de différents réseaux privés, mais en raison du manque de prise en charge par de nombreux périphériques NAT, elle n'est généralement pas considérée comme le principal moyen de se connecter. de tels nœuds.

Pour la suite de cet article, je me concentrerai uniquement sur la mise en œuvre du protocole de livraison garantie. La mise en œuvre de la technique de perforation UDP sera décrite dans les articles suivants.

Exigences du protocole

  1. Livraison de paquets fiable mise en œuvre via un mécanisme de rétroaction positive (le soi-disant accusé de réception positif)
  2. La nécessité d'un transfert efficace des mégadonnées, c'est-à-dire le protocole doit éviter les relais de paquets inutiles
  3. Il devrait être possible d'annuler le mécanisme de confirmation de livraison (la possibilité de fonctionner comme un protocole UDP "pur")
  4. Possibilité de mettre en œuvre le mode commande, avec confirmation de chaque message
  5. L'unité de base du transfert de données sur le protocole doit être un message

Ces exigences coïncident en grande partie avec les exigences du protocole de données fiables décrites dans rfc908 и rfc1151, et je me suis appuyé sur ces normes lors de l'élaboration de ce protocole.

Pour comprendre ces exigences, examinons la synchronisation du transfert de données entre deux nœuds de réseau à l'aide des protocoles TCP et UDP. Soit dans les deux cas nous aurons perdu un paquet.
Transfert de données non interactives via TCP :Implémentation du protocole Reliable Udp pour .Net

Comme vous pouvez le voir sur le diagramme, en cas de perte de paquet, TCP détectera le paquet perdu et le signalera à l'expéditeur en demandant le numéro du segment perdu.
Transfert de données via le protocole UDP :Implémentation du protocole Reliable Udp pour .Net

UDP ne prend aucune mesure de détection de perte. Le contrôle des erreurs de transmission dans le protocole UDP est entièrement de la responsabilité de l'application.

La détection d'erreurs dans le protocole TCP est réalisée en établissant une connexion avec un nœud d'extrémité, en stockant l'état de cette connexion, en indiquant le nombre d'octets envoyés dans chaque en-tête de paquet et en notifiant les réceptions à l'aide d'un numéro d'accusé de réception.

De plus, pour améliorer les performances (c'est-à-dire envoyer plus d'un segment sans recevoir d'accusé de réception), le protocole TCP utilise ce que l'on appelle la fenêtre de transmission - le nombre d'octets de données que l'expéditeur du segment s'attend à recevoir.

Pour plus d'informations sur le protocole TCP, voir rfc793, d'UDP à rfc768où, en fait, ils sont définis.

D'après ce qui précède, il est clair que pour créer un protocole de livraison de messages fiable sur UDP (ci-après dénommé UDP fiable), il est nécessaire d'implémenter des mécanismes de transfert de données similaires à TCP. À savoir:

  • enregistrer l'état de la connexion
  • utiliser la numérotation des segments
  • utiliser des packages de confirmation spéciaux
  • utiliser un mécanisme de fenêtrage simplifié pour augmenter le débit du protocole

De plus, vous avez besoin de :

  • signaler le début d'un message, pour allouer des ressources à la connexion
  • signaler la fin d'un message, pour transmettre le message reçu à l'application en amont et libérer les ressources du protocole
  • permettre au protocole spécifique à la connexion de désactiver le mécanisme de confirmation de livraison pour qu'il fonctionne comme UDP "pur"

En-tête UDP fiable

Rappelons qu'un datagramme UDP est encapsulé dans un datagramme IP. Le paquet UDP fiable est "enveloppé" de manière appropriée dans un datagramme UDP.
Encapsulation d'en-tête UDP fiable :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

  • Drapeaux - drapeaux de contrôle de paquet
  • MessageType - type de message utilisé par les applications en amont pour s'abonner à des messages spécifiques
  • TransmissionId - le numéro de la transmission, ainsi que l'adresse et le port du destinataire, identifient de manière unique la connexion
  • PacketNumber - numéro de paquet
  • Options - options de protocole supplémentaires. Dans le cas du premier paquet, il est utilisé pour indiquer la taille du message

Les drapeaux sont les suivants :

  • FirstPacket - le premier paquet du message
  • NoAsk - le message ne nécessite pas l'activation d'un mécanisme d'accusé de réception
  • LastPacket - le dernier paquet du message
  • RequestForPacket - paquet de confirmation ou demande de paquet perdu

Principes généraux du protocole

Étant donné que Reliable UDP se concentre sur la transmission garantie des messages entre deux nœuds, il doit être capable d'établir une connexion avec l'autre côté. Pour établir une connexion, l'expéditeur envoie un paquet avec l'indicateur FirstPacket, dont la réponse signifie que la connexion est établie. Tous les paquets de réponse, ou, en d'autres termes, les paquets d'accusé de réception, définissent toujours la valeur du champ PacketNumber sur un de plus que la plus grande valeur PacketNumber des paquets reçus avec succès. Le champ Options du premier paquet envoyé correspond à la taille du message.

Un mécanisme similaire est utilisé pour mettre fin à une connexion. L'indicateur LastPacket est défini sur le dernier paquet du message. Dans le paquet de réponse, le numéro du dernier paquet + 1 est indiqué, ce qui signifie pour le côté réception une livraison réussie du message.
Schéma d'établissement et de terminaison de 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. Elle est égale à la taille de la fenêtre de réception/transmission. Le dernier bloc de données peut avoir moins de paquets. Après l'envoi de chaque bloc, le côté émetteur attend une confirmation de livraison ou une demande de redistribution des paquets perdus, laissant la fenêtre de réception/transmission ouverte pour recevoir les réponses. Après avoir reçu la confirmation de la livraison du bloc, la fenêtre de réception/transmission se décale et le bloc de données suivant est envoyé.

Le côté réception reçoit les paquets. Chaque paquet est vérifié pour voir s'il tombe dans la fenêtre de transmission. Les paquets et les doublons qui ne tombent pas dans la fenêtre sont filtrés. Parce que Si la taille de la fenêtre est fixe et la même pour le destinataire et l'expéditeur, alors dans le cas d'un bloc de paquets livré sans perte, la fenêtre est décalée pour recevoir les paquets du bloc de données suivant et une confirmation de livraison est envoyé. Si la fenêtre ne se remplit pas dans le délai défini par le temporisateur de travail, une vérification sera lancée sur les paquets qui n'ont pas été livrés et des demandes de redistribution seront envoyées.
Diagramme de retransmission :Implémentation du protocole Reliable Udp pour .Net

Délais d'attente et minuteries de protocole

Il existe plusieurs raisons pour lesquelles une connexion ne peut pas être établie. Par exemple, si le destinataire est hors ligne. Dans ce cas, lors de la tentative d'établissement d'une connexion, la connexion sera fermée par timeout. L'implémentation UDP fiable utilise deux temporisateurs pour définir des délais d'attente. Le premier, le temporisateur de travail, est utilisé pour attendre une réponse de l'hôte distant. S'il se déclenche côté expéditeur, le dernier paquet envoyé est renvoyé. Si le temporisateur expire chez le destinataire, une vérification des paquets perdus est effectuée et des demandes de redistribution sont envoyées.

Le deuxième temporisateur est nécessaire pour fermer la connexion en cas d'absence de communication entre les nœuds. Pour le côté expéditeur, il démarre immédiatement après l'expiration du temporisateur de travail et attend une réponse du nœud distant. S'il n'y a pas de réponse dans le délai spécifié, la connexion est interrompue et les ressources sont libérées. Pour le côté réception, le temporisateur de fermeture de connexion est lancé après que le temporisateur de travail a expiré deux fois. Ceci est nécessaire pour s'assurer contre la perte du paquet de confirmation. Lorsque le temporisateur expire, la connexion est également interrompue et les ressources sont libérées.

Diagramme d'état de transmission UDP fiable

Les principes du protocole sont implémentés dans une machine à états finis, dont chaque état est responsable d'une certaine logique de traitement des paquets.
Diagramme d'état UDP fiable :

Implémentation du protocole Reliable Udp pour .Net

Fermé - n'est pas vraiment un état, c'est un point de départ et d'arrivée pour l'automate. Pour l'état Fermé un bloc de contrôle de transmission est reçu, qui, mettant en œuvre un serveur UDP asynchrone, transmet les paquets aux connexions appropriées et démarre le traitement d'état.

Envoi du premier paquet – l'état initial dans lequel se trouve la connexion sortante lorsque le message est envoyé.

Dans cet état, le premier paquet pour les messages normaux est envoyé. Pour les messages sans confirmation d'envoi, c'est le seul état où le message entier est envoyé.

CycleEnvoi – état fondamental pour la transmission des paquets de messages.

Transition vers elle de l'état Envoi du premier paquet effectué après l'envoi du premier paquet du message. C'est dans cet état que viennent tous les accusés de réception et demandes de retransmissions. La sortie de celui-ci est possible dans deux cas - en cas de livraison réussie du message ou par dépassement de délai.

Premier paquet reçu – l'état initial pour le destinataire du message.

Il vérifie l'exactitude du début de la transmission, crée les structures nécessaires et envoie un accusé de réception du premier paquet.

Pour un message qui se compose d'un seul paquet et a été envoyé sans utiliser de preuve de livraison, c'est le seul état. Après traitement d'un tel message, la connexion est fermée.

Montage – état de base pour la réception de paquets de messages.

Il écrit les paquets dans le stockage temporaire, vérifie la perte de paquets, envoie des accusés de réception pour la livraison d'un bloc de paquets et du message entier, et envoie des demandes de redistribution des paquets perdus. En cas de réception réussie de l'intégralité du message, la connexion passe à l'état Complété, sinon, un délai d'attente se termine.

Complété – fermeture de la connexion en cas de bonne réception de l'intégralité du message.

Cet état est nécessaire pour l'assemblage du message et pour le cas où la confirmation de livraison du message a été perdue sur le chemin de l'expéditeur. Cet état est quitté par un délai d'attente, mais la connexion est considérée comme fermée avec succès.

Plus profondément dans le code. unité de commande de transmission

L'un des éléments clés d'UDP fiable est le bloc de contrôle de transmission. La tâche de ce bloc est de stocker les connexions actuelles et les éléments auxiliaires, de distribuer les paquets entrants aux connexions correspondantes, de fournir une interface pour envoyer des paquets à une connexion et de mettre en œuvre le protocole API. Le bloc de contrôle de transmission reçoit des paquets de la couche UDP et les transmet à la machine d'état pour traitement. Pour recevoir des paquets, il implémente un serveur UDP asynchrone.
Certains membres de la classe ReliableUdpConnectionControlBlock :

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Implémentation d'un serveur UDP asynchrone :

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Pour chaque transfert de message, une structure est créée qui contient des informations sur la connexion. Une telle structure est appelée enregistrement de connexion.
Certains membres de la classe ReliableUdpConnectionRecord :

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

Plus profondément dans le code. États

Les états implémentent la machine d'état du protocole Reliable UDP, où le traitement principal des paquets a lieu. La classe abstraite ReliableUdpState fournit une interface pour l'état :

Implémentation du protocole Reliable Udp pour .Net

Toute la logique du protocole est implémentée par les classes présentées ci-dessus, ainsi qu'une classe auxiliaire qui fournit des méthodes statiques, comme, par exemple, la construction de l'en-tête ReliableUdp à partir de l'enregistrement de connexion.

Ensuite, nous examinerons en détail la mise en œuvre des méthodes d'interface qui déterminent les algorithmes de base du protocole.

Méthode DisposeByTimeoutDisposeByTimeout method

La méthode DisposeByTimeout est responsable de la libération des ressources de connexion après un délai d'attente et de la signalisation de la livraison réussie/échouée des messages.
ReliableUdpState.DisposeByTimeout :

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

Il n'est remplacé que dans l'état Complété.
Completed.DisposeByTimeout :

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

Méthode ProcessPackets

La méthode ProcessPackets est responsable du traitement supplémentaire d'un ou de plusieurs packages. Appelé directement ou via un temporisateur d'attente de paquets.

À la condition Montage la méthode est remplacée et est responsable de la vérification des paquets perdus et de la transition vers l'état Complété, en cas de réception du dernier paquet et de réussite d'un contrôle
Assembling.ProcessPackets :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

À la condition CycleEnvoi cette méthode est appelée uniquement sur un minuteur et est responsable du renvoi du dernier message, ainsi que de l'activation du minuteur de fermeture de connexion.
EnvoiCycle.ProcessPackets :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

À la condition Complété la méthode arrête le temporisateur en cours d'exécution et envoie le message aux abonnés.
Completed.ProcessPackets :

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

Méthode ReceivePacket

À la condition Premier paquet reçu la tâche principale de la méthode est de déterminer si le premier paquet de messages est effectivement arrivé à l'interface, et également de collecter un message constitué d'un seul paquet.
FirstPacketReceived.ReceivePacket :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

À la condition CycleEnvoi cette méthode est remplacée pour accepter les accusés de réception et les 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;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

À la condition Montage dans la méthode ReceivePacket, le travail principal d'assemblage d'un message à partir de paquets entrants a lieu.
Assembling.ReceivePacket :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

À la condition Complété la seule tâche du procédé est d'envoyer un nouvel accusé de réception de la livraison réussie du message.
Terminé.ReceivePacket :

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

Méthode d'envoi de paquets

À la condition Envoi du premier paquet cette méthode envoie le premier paquet de données ou, si le message ne nécessite pas de confirmation de livraison, le message entier.
FirstPacketSending.SendPacket :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

À la condition CycleEnvoi dans cette méthode, un bloc de paquets est envoyé.
SendingCycle.SendPacket :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Plus profondément dans le code. Créer et établir des connexions

Maintenant que nous avons vu les états de base et les méthodes utilisées pour gérer les états, décomposons quelques exemples de la façon dont le protocole fonctionne un peu plus en détail.
Diagramme de transmission de données dans des conditions normales :Implémentation du protocole Reliable Udp pour .Net

Considérez en détail la création enregistrement de connexion pour se connecter et envoyer le premier paquet. Le transfert est toujours initié par l'application qui appelle l'API d'envoi de message. Ensuite, la méthode StartTransmission du bloc de contrôle de transmission est invoquée, ce qui démarre la transmission des 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( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Envoi du premier paquet (état FirstPacketSending) :

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Après avoir envoyé le premier paquet, l'expéditeur entre dans l'état CycleEnvoi – attendre la confirmation de livraison du colis.
Le côté réception, en utilisant la méthode EndReceive, reçoit le paquet envoyé, crée un nouveau enregistrement de connexion et transmet ce paquet, avec un en-tête pré-parsé, à la méthode ReceivePacket de l'état pour le traitement Premier paquet reçu
Création d'une connexion côté réception :

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

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

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Plus profondément dans le code. Fermeture de la connexion à l'expiration du délai

La gestion du délai d'attente est une partie importante de Reliable UDP. Prenons un exemple dans lequel un nœud intermédiaire est tombé en panne et la livraison de données dans les deux sens est devenue impossible.
Schéma de fermeture d'une connexion par timeout :Implémentation du protocole Reliable Udp pour .Net

Comme on peut le voir sur le diagramme, le temporisateur de travail de l'expéditeur démarre immédiatement après l'envoi d'un bloc de paquets. Cela se produit dans la méthode SendPacket de l'état CycleEnvoi.
Activation de la minuterie de travail (état SendingCycle) :

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

Les périodes de minuterie sont définies lors de la création de la connexion. La valeur par défaut de ShortTimerPeriod est de 5 secondes. Dans l'exemple, il est réglé sur 1,5 seconde.

Pour une connexion entrante, le temporisateur démarre après la réception du dernier paquet de données entrant, cela se produit dans la méthode ReceivePacket de l'état Montage
Activation de la minuterie de travail (état d'assemblage) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Plus aucun paquet n'est arrivé sur la connexion entrante en attendant le minuteur de travail. Le minuteur s'est déclenché et a appelé la méthode ProcessPackets, où les paquets perdus ont été trouvés et les demandes de nouvelle livraison ont été envoyées pour la première fois.
Envoi de demandes de relivraison (état Assemblage) :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

La variable TimerSecondTry est définie sur oui. Cette variable est responsable du redémarrage de la minuterie de travail.

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

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

Après cela, le minuteur de fermeture de connexion démarre dans la connexion sortante.
FiableUdpState.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);
}

Le délai d'expiration du minuteur de fermeture de connexion est de 30 secondes par défaut.

Après un court laps de temps, le minuteur de travail du côté du destinataire se déclenche à nouveau, les demandes sont à nouveau envoyées, après quoi le minuteur de fermeture de la connexion démarre pour la connexion entrante

Lorsque les minuteurs de fermeture se déclenchent, toutes les ressources des deux enregistrements de connexion sont libérées. L'expéditeur signale l'échec de la livraison à l'application en amont (voir API UDP fiable).
Libération des ressources d'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);
  }
}

Plus profondément dans le code. Restauration du transfert de données

Schéma 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à discuté dans la fermeture de la connexion à l'expiration du délai, lorsque le temporisateur de travail expire, le récepteur vérifie les paquets perdus. En cas de perte de paquets, une liste du nombre de paquets qui n'ont pas atteint le destinataire sera compilée. Ces numéros sont entrés dans le tableau LostPackets d'une connexion spécifique et des demandes de redistribution sont envoyées.
Envoi de demandes de relivraison de colis (état Assemblage) :

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

L'expéditeur acceptera la demande de nouvelle livraison et enverra les paquets manquants. Il convient de noter qu'à ce moment, l'expéditeur a déjà démarré le minuteur de fermeture de connexion et, lorsqu'une demande est reçue, il est réinitialisé.
Renvoyer les paquets perdus (état SendingCycle) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Le paquet renvoyé (paquet #3 dans le diagramme) est reçu par la connexion entrante. Une vérification est effectuée pour voir si la fenêtre de réception est pleine et si la transmission normale des données est restaurée.
Vérification des occurrences dans la fenêtre de réception (état d'assemblage) :

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API UDP fiable

Pour interagir avec le protocole de transfert de données, il existe une classe Udp fiable ouverte, qui est un wrapper sur le bloc de contrôle de transfert. Voici les membres les plus importants de la classe :

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

Les messages sont reçus par abonnement. Signature déléguée pour la méthode de rappel :

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

Message:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  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 facultatifs sont utilisés : ReliableUdpMessageTypes messageType et IPEndPoint ipEndPoint.

Types de messages :

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

Le message est envoyé de manière asynchrone ; pour cela, le protocole implémente un modèle de programmation asynchrone :

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

Le résultat de l'envoi d'un message sera vrai - si le message a réussi à atteindre le destinataire et faux - si la connexion a été fermée par timeout :

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusion

Beaucoup n'a pas été décrit dans cet article. Mécanismes de correspondance de threads, gestion des exceptions et des erreurs, implémentation de méthodes d'envoi de messages asynchrones. Mais le cœur du protocole, la description de la logique de traitement des paquets, d'établissement d'une connexion et de gestion des délais d'attente, devrait être clair pour vous.

La version démontrée du protocole de livraison fiable est suffisamment robuste et flexible pour répondre aux exigences précédemment définies. Mais je tiens à ajouter que la mise en œuvre décrite peut être améliorée. Par exemple, pour augmenter le débit et modifier dynamiquement les périodes de temporisation, des mécanismes tels que la fenêtre glissante et le RTT peuvent être ajoutés au protocole, il sera également utile d'implémenter un mécanisme pour déterminer le MTU entre les nœuds de connexion (mais uniquement si des messages volumineux sont envoyés) .

Merci de votre attention, j'attends vos commentaires et remarques avec impatience.

PS Pour ceux qui sont intéressés par les détails ou qui veulent juste tester le protocole, le lien vers le projet sur GitHube :
Projet UDP fiable

Liens et articles utiles

  1. Spécification du protocole TCP : en anglais и en anglais
  2. Spécification du protocole UDP : en anglais и en anglais
  3. Discussion sur le protocole RUDP : brouillon-ietf-sigtran-fiable-udp-00
  4. Protocole de données fiable : rfc908 и rfc1151
  5. Une implémentation simple de la confirmation de livraison via UDP : Prenez le contrôle total de votre réseau avec .NET et UDP
  6. Article décrivant les mécanismes de traversée NAT : Communication d'égal à égal sur les traducteurs d'adresses réseau
  7. Implémentation du modèle de programmation asynchrone : Implémentation du modèle de programmation asynchrone CLR и Comment implémenter le modèle de conception IAsyncResult
  8. Portage du modèle de programmation asynchrone vers le modèle asynchrone basé sur les tâches (APM dans TAP) :
    TPL et programmation asynchrone .NET traditionnelle
    Interopérabilité avec d'autres modèles et types asynchrones

Mise à jour : merci maire и 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 violée, car Le 4ème framework prend en charge les serveurs XP et 2003.

Source: habr.com

Ajouter un commentaire