Implementação do protocolo Reliable Udp para .Net

A Internet mudou há muito tempo. Um dos principais protocolos da Internet - o UDP é usado por aplicativos não apenas para entregar datagramas e transmissões, mas também para fornecer conexões "ponto a ponto" entre os nós da rede. Devido ao seu design simples, este protocolo tem muitos usos não planejados anteriormente, no entanto, as deficiências do protocolo, como a falta de entrega garantida, não desapareceram em nenhum lugar. Este artigo descreve a implementação do protocolo de entrega garantida em UDP.
Conteúdo:Entrada
Requisitos de protocolo
Cabeçalho UDP confiável
Princípios gerais do protocolo
Timeouts e temporizadores de protocolo
Diagrama de estado de transmissão UDP confiável
Mais profundo no código. unidade de controle de transmissão
Mais profundo no código. estados

Mais profundo no código. Criando e Estabelecendo Conexões
Mais profundo no código. Fechando a conexão no tempo limite
Mais profundo no código. Restaurando transferência de dados
API UDP confiável
Conclusão
Links e artigos úteis

Entrada

A arquitetura original da Internet assumia um espaço de endereçamento homogêneo no qual cada nodo tinha um endereço IP global e único e podia se comunicar diretamente com outros nodos. Agora a Internet, de fato, tem uma arquitetura diferente - uma área de endereços IP globais e muitas áreas com endereços privados escondidos atrás de dispositivos NAT.Nessa arquitetura, apenas os dispositivos no espaço de endereço global podem se comunicar facilmente com qualquer pessoa na rede porque possuem um endereço IP exclusivo e globalmente roteável. Um nó em uma rede privada pode se conectar a outros nós na mesma rede e também pode se conectar a outros nós conhecidos no espaço de endereço global. Essa interação é alcançada em grande parte devido ao mecanismo de tradução de endereço de rede. Dispositivos NAT, como roteadores Wi-Fi, criam entradas de tabela de conversão especiais para conexões de saída e modificam endereços IP e números de porta em pacotes. Isso permite conexões de saída da rede privada para hosts no espaço de endereço global. Mas, ao mesmo tempo, os dispositivos NAT geralmente bloqueiam todo o tráfego de entrada, a menos que sejam definidas regras separadas para conexões de entrada.

Essa arquitetura da Internet é correta o suficiente para a comunicação cliente-servidor, onde os clientes podem estar em redes privadas e os servidores têm um endereço global. Mas cria dificuldades para a conexão direta de dois nós entre diferente redes privadas. Uma conexão direta entre dois nós é importante para aplicativos ponto a ponto, como transmissão de voz (Skype), obtenção de acesso remoto a um computador (TeamViewer) ou jogos online.

Um dos métodos mais eficazes para estabelecer uma conexão ponto a ponto entre dispositivos em diferentes redes privadas é chamado de perfuração. Essa técnica é mais comumente usada com aplicativos baseados no protocolo UDP.

Mas se o seu aplicativo precisa de entrega garantida de dados, por exemplo, você transfere arquivos entre computadores, usar o UDP terá muitas dificuldades devido ao fato de que o UDP não é um protocolo de entrega garantida e não fornece entrega de pacotes em ordem, ao contrário do TCP protocolo.

Nesse caso, para garantir a entrega garantida de pacotes, é necessário implementar um protocolo de camada de aplicação que forneça a funcionalidade necessária e funcione sobre UDP.

Quero observar desde já que existe uma técnica de perfuração TCP para estabelecer conexões TCP entre nós em diferentes redes privadas, mas devido à falta de suporte para ela por muitos dispositivos NAT, geralmente não é considerada a principal forma de conexão tais nós.

No restante deste artigo, focarei apenas na implementação do protocolo de entrega garantida. A implementação da técnica de perfuração UDP será descrita nos artigos a seguir.

Requisitos de protocolo

  1. Entrega confiável de pacotes implementada por meio de um mecanismo de feedback positivo (o chamado reconhecimento positivo)
  2. A necessidade de transferência eficiente de big data, ou seja, o protocolo deve evitar a retransmissão desnecessária de pacotes
  3. Deve ser possível cancelar o mecanismo de confirmação de entrega (a capacidade de funcionar como um protocolo UDP "puro")
  4. Capacidade de implementar o modo de comando, com confirmação de cada mensagem
  5. A unidade básica de transferência de dados pelo protocolo deve ser uma mensagem

Esses requisitos coincidem amplamente com os requisitos do Protocolo de Dados Confiáveis ​​descritos em rf 908 и rf 1151, e confiei nesses padrões ao desenvolver este protocolo.

Para entender esses requisitos, vejamos o tempo de transferência de dados entre dois nós de rede usando os protocolos TCP e UDP. Deixe em ambos os casos teremos um pacote perdido.
Transferência de dados não interativos por TCP:Implementação do protocolo Reliable Udp para .Net

Como você pode ver no diagrama, em caso de perda de pacote, o TCP detectará o pacote perdido e o reportará ao remetente solicitando o número do segmento perdido.
Transferência de dados via protocolo UDP:Implementação do protocolo Reliable Udp para .Net

O UDP não executa nenhuma etapa de detecção de perda. O controle de erros de transmissão no protocolo UDP é de inteira responsabilidade do aplicativo.

A detecção de erros no protocolo TCP é obtida estabelecendo uma conexão com um nó final, armazenando o estado dessa conexão, indicando o número de bytes enviados em cada cabeçalho de pacote e notificando os recebimentos usando um número de confirmação.

Além disso, para melhorar o desempenho (ou seja, enviar mais de um segmento sem receber uma confirmação), o protocolo TCP usa a chamada janela de transmissão - o número de bytes de dados que o remetente do segmento espera receber.

Para obter mais informações sobre o protocolo TCP, consulte rf 793, de UDP para rf 768onde, de fato, eles são definidos.

Pelo exposto, fica claro que, para criar um protocolo confiável de entrega de mensagens sobre UDP (doravante denominado UDP confiável), é necessário implementar mecanismos de transferência de dados semelhantes ao TCP. Nomeadamente:

  • salvar estado de conexão
  • usar numeração de segmento
  • usar pacotes especiais de confirmação
  • usar um mecanismo de janela simplificado para aumentar a taxa de transferência do protocolo

Além disso, você precisa:

  • sinalizar o início de uma mensagem, para alocar recursos para a conexão
  • sinalizar o fim de uma mensagem, passar a mensagem recebida para o aplicativo upstream e liberar recursos do protocolo
  • permitir que o protocolo específico da conexão desabilite o mecanismo de confirmação de entrega para funcionar como UDP "puro"

Cabeçalho UDP confiável

Lembre-se de que um datagrama UDP é encapsulado em um datagrama IP. O pacote UDP confiável é adequadamente "empacotado" em um datagrama UDP.
Encapsulamento de cabeçalho UDP confiável:Implementação do protocolo Reliable Udp para .Net

A estrutura do cabeçalho UDP confiável é bastante simples:

Implementação do protocolo Reliable Udp para .Net

  • Flags - sinalizadores de controle de pacote
  • MessageType - tipo de mensagem usado por aplicativos upstream para assinar mensagens específicas
  • TransmissionId - o número da transmissão, juntamente com o endereço e a porta do destinatário, identifica exclusivamente a conexão
  • PacketNumber - número do pacote
  • Opções - opções de protocolo adicionais. No caso do primeiro pacote, é usado para indicar o tamanho da mensagem

As bandeiras são as seguintes:

  • FirstPacket - o primeiro pacote da mensagem
  • NoAsk - a mensagem não requer um mecanismo de reconhecimento para ser ativado
  • LastPacket - o último pacote da mensagem
  • RequestForPacket - pacote de confirmação ou solicitação de pacote perdido

Princípios gerais do protocolo

Como o UDP confiável é focado na transmissão garantida de mensagens entre dois nós, ele deve ser capaz de estabelecer uma conexão com o outro lado. Para estabelecer uma conexão, o remetente envia um pacote com o sinalizador FirstPacket, cuja resposta significará que a conexão foi estabelecida. Todos os pacotes de resposta, ou, em outras palavras, pacotes de confirmação, sempre definem o valor do campo PacketNumber para um a mais do que o maior valor de PacketNumber de pacotes recebidos com sucesso. O campo Opções para o primeiro pacote enviado é o tamanho da mensagem.

Um mecanismo semelhante é usado para encerrar uma conexão. O sinalizador LastPacket é definido no último pacote da mensagem. No pacote de resposta, é indicado o número do último pacote + 1, o que para o lado receptor significa a entrega bem-sucedida da mensagem.
Diagrama de estabelecimento e terminação da conexão:Implementação do protocolo Reliable Udp para .Net

Quando a conexão é estabelecida, a transferência de dados começa. Os dados são transmitidos em blocos de pacotes. Cada bloco, exceto o último, contém um número fixo de pacotes. É igual ao tamanho da janela de recepção/transmissão. O último bloco de dados pode ter menos pacotes. Após o envio de cada bloco, o lado remetente aguarda uma confirmação de entrega ou um pedido de reentrega de pacotes perdidos, deixando a janela receber/transmitir aberta para receber as respostas. Depois de receber a confirmação da entrega do bloco, a janela de recebimento/transmissão muda e o próximo bloco de dados é enviado.

O lado receptor recebe os pacotes. Cada pacote é verificado para ver se ele cai dentro da janela de transmissão. Pacotes e duplicatas que não caem na janela são filtrados. Porque Se o tamanho da janela for fixo e igual para o destinatário e o remetente, no caso de um bloco de pacotes ser entregue sem perdas, a janela é deslocada para receber pacotes do próximo bloco de dados e uma confirmação de entrega é enviado. Se a janela não for preenchida dentro do período definido pelo timer de trabalho, será iniciada uma verificação de quais pacotes não foram entregues e serão enviadas solicitações de reenvio.
Diagrama de retransmissão:Implementação do protocolo Reliable Udp para .Net

Timeouts e temporizadores de protocolo

Existem várias razões pelas quais uma conexão não pode ser estabelecida. Por exemplo, se a parte receptora estiver offline. Neste caso, ao tentar estabelecer uma conexão, a conexão será encerrada por timeout. A implementação do UDP confiável usa dois temporizadores para definir tempos limite. O primeiro, o timer de trabalho, é usado para aguardar uma resposta do host remoto. Se disparar no lado do remetente, o último pacote enviado será reenviado. Se o cronômetro expirar no destinatário, uma verificação de pacotes perdidos será executada e as solicitações de reentrega serão enviadas.

O segundo temporizador é necessário para fechar a conexão em caso de falta de comunicação entre os nós. Para o lado do remetente, ele inicia imediatamente após o término do temporizador de trabalho e aguarda uma resposta do nó remoto. Se não houver resposta no período especificado, a conexão é encerrada e os recursos são liberados. Para o lado receptor, o cronômetro de fechamento da conexão é iniciado após o cronômetro de trabalho expirar duas vezes. Isso é necessário para evitar a perda do pacote de confirmação. Quando o cronômetro expira, a conexão também é encerrada e os recursos são liberados.

Diagrama de estado de transmissão UDP confiável

Os princípios do protocolo são implementados em uma máquina de estados finitos, sendo que cada estado é responsável por uma certa lógica de processamento de pacotes.
Diagrama de estado UDP confiável:

Implementação do protocolo Reliable Udp para .Net

Fechadas - não é realmente um estado, é um ponto inicial e final para o autômato. Para o estado Fechadas é recebido um bloco de controle de transmissão que, implementando um servidor UDP assíncrono, encaminha os pacotes para as conexões apropriadas e inicia o processamento do estado.

PrimeiroPacoteEnviando – o estado inicial em que a conexão de saída está quando a mensagem é enviada.

Nesse estado, o primeiro pacote de mensagens normais é enviado. Para mensagens sem confirmação de envio, este é o único estado onde toda a mensagem é enviada.

Ciclo de Envio – estado fundamental para a transmissão de pacotes de mensagens.

Transição para ele do estado PrimeiroPacoteEnviando realizada após o envio do primeiro pacote da mensagem. É nesse estado que chegam todas as confirmações e solicitações de retransmissões. A saída é possível em dois casos - no caso de entrega bem-sucedida da mensagem ou por tempo limite.

PrimeiroPacoteRecebido – o estado inicial do destinatário da mensagem.

Ele verifica a exatidão do início da transmissão, cria as estruturas necessárias e envia um aviso de recebimento do primeiro pacote.

Para uma mensagem que consiste em um único pacote e foi enviada sem o uso de comprovante de entrega, esse é o único estado. Depois de processar essa mensagem, a conexão é encerrada.

montagem – estado básico para receber pacotes de mensagens.

Ele grava pacotes no armazenamento temporário, verifica a perda de pacotes, envia confirmações para a entrega de um bloco de pacotes e da mensagem inteira e envia solicitações para reenvio de pacotes perdidos. Em caso de recebimento bem-sucedido de toda a mensagem, a conexão passa para o estado Efetuado, caso contrário, um tempo limite será encerrado.

Efetuado – fechando a conexão em caso de recebimento bem-sucedido de toda a mensagem.

Este estado é necessário para a montagem da mensagem e para o caso em que a confirmação de entrega da mensagem foi perdida no caminho para o remetente. Este estado é encerrado por um tempo limite, mas a conexão é considerada fechada com sucesso.

Mais profundo no código. unidade de controle de transmissão

Um dos elementos-chave do UDP confiável é o bloco de controle de transmissão. A tarefa deste bloco é armazenar conexões atuais e elementos auxiliares, distribuir pacotes de entrada para as conexões correspondentes, fornecer uma interface para enviar pacotes para uma conexão e implementar a API do protocolo. O bloco de controle de transmissão recebe pacotes da camada UDP e os encaminha para a máquina de estado para processamento. Para receber pacotes, implementa um servidor UDP assíncrono.
Alguns membros da 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;    	
  //...
}

Implementação do servidor UDP assíncrono:

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

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

Para cada transferência de mensagem, é criada uma estrutura que contém informações sobre a conexão. Tal estrutura é chamada registro de conexão.
Alguns membros da 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;
  //...
}

Mais profundo no código. estados

Os estados implementam a máquina de estados do protocolo UDP confiável, onde ocorre o processamento principal dos pacotes. A classe abstrata ReliableUdpState fornece uma interface para o estado:

Implementação do protocolo Reliable Udp para .Net

Toda a lógica do protocolo é implementada pelas classes apresentadas acima, juntamente com uma classe auxiliar que fornece métodos estáticos, como, por exemplo, construir o cabeçalho ReliableUdp a partir do registro da conexão.

A seguir, consideraremos em detalhes a implementação dos métodos de interface que determinam os algoritmos básicos do protocolo.

Método DisposeByTimeout

O método DisposeByTimeout é responsável por liberar recursos de conexão após um tempo limite e por sinalizar a entrega de mensagens com/sem êxito.
ReliableUdpState.DisposeByTimeout:

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

Só é substituído no estado Efetuado.
Concluído.DisposeByTimeout:

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

Método ProcessPackets

O método ProcessPackets é responsável pelo processamento adicional de um pacote ou pacotes. Chamado diretamente ou por meio de um temporizador de espera de pacote.

Em condição montagem o método é substituído e é responsável por verificar pacotes perdidos e fazer a transição para o estado Efetuado, no caso de receber o último pacote e passar por uma verificação bem-sucedida
Montagem.ProcessoPacotes:

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

Em condição Ciclo de Envio este método é chamado apenas em um timer, e é responsável por reenviar a última mensagem, bem como habilitar o timer de fechamento da conexão.
SendingCycle.ProcessPackets:

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

Em condição Efetuado o método interrompe o cronômetro em execução e envia a mensagem aos assinantes.
Concluído.Pacotes de processo:

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

Método ReceivePacket

Em condição PrimeiroPacoteRecebido a principal tarefa do método é determinar se o primeiro pacote de mensagens realmente chegou à interface e também coletar uma mensagem que consiste em um único pacote.
PrimeiroPacketReceived.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);
  }
}

Em condição Ciclo de Envio esse método é substituído para aceitar confirmações de entrega e solicitações de retransmissão.
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));
}

Em condição montagem no método ReceivePacket, ocorre o trabalho principal de montar uma mensagem a partir dos pacotes recebidos.
Montagem.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);
  }
}

Em condição Efetuado a única tarefa do método é enviar uma nova confirmação da entrega bem-sucedida da mensagem.
Concluído.ReceivePacket:

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

Enviar método de pacote

Em condição PrimeiroPacoteEnviando esse método envia o primeiro pacote de dados ou, se a mensagem não exigir confirmação de entrega, a mensagem inteira.
PrimeiroPacketSending.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);
}

Em condição Ciclo de Envio neste método, um bloco de pacotes é enviado.
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 );
  }
}

Mais profundo no código. Criando e Estabelecendo Conexões

Agora que vimos os estados básicos e os métodos usados ​​para lidar com os estados, vamos detalhar alguns exemplos de como o protocolo funciona.
Diagrama de transmissão de dados em condições normais:Implementação do protocolo Reliable Udp para .Net

Considere em detalhes a criação registro de conexão para conectar e enviar o primeiro pacote. A transferência é sempre iniciada pelo aplicativo que chama a API de envio de mensagem. Em seguida, é invocado o método StartTransmission do bloco de controle de transmissão, que inicia a transmissão dos dados para a nova mensagem.
Criando uma conexão de saída:

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

Enviando o primeiro pacote (estado FirstPacketSending):

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

Depois de enviar o primeiro pacote, o remetente entra no estado Ciclo de Envio – aguarde a confirmação da entrega do pacote.
O lado receptor, usando o método EndReceive, recebe o pacote enviado, cria um novo registro de conexão e passa este pacote, com um cabeçalho pré-analisado, para o método ReceivePacket do estado para processamento PrimeiroPacoteRecebido
Criando uma conexão no lado receptor:

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

Recebendo o primeiro pacote e enviando uma confirmação (estado FirstPacketReceived):

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

Mais profundo no código. Fechando a conexão no tempo limite

A manipulação de tempo limite é uma parte importante do UDP confiável. Considere um exemplo em que um nó intermediário falhou e a entrega de dados em ambas as direções tornou-se impossível.
Diagrama para fechar uma conexão por timeout:Implementação do protocolo Reliable Udp para .Net

Como pode ser visto no diagrama, o cronômetro de trabalho do remetente inicia imediatamente após o envio de um bloco de pacotes. Isso acontece no método SendPacket do estado Ciclo de Envio.
Habilitando o timer de trabalho (estado SendingCycle):

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

Os períodos do temporizador são definidos quando a conexão é criada. O ShortTimerPeriod padrão é de 5 segundos. No exemplo, é definido como 1,5 segundos.

Para uma conexão de entrada, o cronômetro inicia após o recebimento do último pacote de dados de entrada, isso acontece no método ReceivePacket do estado montagem
Ativando o temporizador de trabalho (estado de montagem):

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

Nenhum pacote chegou à conexão de entrada enquanto aguardava o cronômetro de trabalho. O timer disparou e chamou o método ProcessPackets, onde os pacotes perdidos foram encontrados e os pedidos de reentrega foram enviados pela primeira vez.
Enviando solicitações de reentrega (estado de montagem):

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

A variável TimerSecondTry é definida como verdadeiro. Esta variável é responsável por reiniciar o timer de trabalho.

Do lado do remetente, o timer de trabalho também é acionado e o último pacote enviado é reenviado.
Ativando o temporizador de fechamento da conexão (estado SendingCycle):

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

Depois disso, o temporizador de fechamento da conexão é iniciado na conexão de saída.
ConfiávelUdpState.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);
}

O período de tempo limite do temporizador de fechamento da conexão é de 30 segundos por padrão.

Após um curto período de tempo, o cronômetro de trabalho no lado do destinatário é acionado novamente, as solicitações são enviadas novamente, após o que o cronômetro de fechamento da conexão é iniciado para a conexão de entrada

Quando os cronômetros de fechamento disparam, todos os recursos de ambos os registros de conexão são liberados. O remetente relata a falha de entrega ao aplicativo upstream (consulte API UDP confiável).
Liberando recursos de registro de conexão:

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

Mais profundo no código. Restaurando transferência de dados

Diagrama de recuperação da transmissão de dados em caso de perda de pacotes:Implementação do protocolo Reliable Udp para .Net

Conforme já discutido no fechamento da conexão no tempo limite, quando o timer de trabalho expirar, o receptor verificará se há pacotes perdidos. Em caso de perda de pacotes, será compilada uma lista com o número de pacotes que não chegaram ao destinatário. Esses números são inseridos na matriz LostPackets de uma conexão específica e as solicitações de reenvio são enviadas.
Enviando solicitações para reenviar pacotes (estado de montagem):

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

O remetente aceitará a solicitação de reentrega e enviará os pacotes ausentes. Vale ressaltar que neste momento o remetente já iniciou o timer de fechamento da conexão e, ao receber uma requisição, ele é zerado.
Reenviando pacotes perdidos (estado SendingCycle):

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

O pacote reenviado (pacote nº 3 no diagrama) é recebido pela conexão de entrada. Uma verificação é feita para ver se a janela de recepção está cheia e a transmissão normal de dados é restaurada.
Verificando ocorrências na janela de recebimento (estado de montagem):

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 confiável

Para interagir com o protocolo de transferência de dados, existe uma classe Udp confiável aberta, que é um wrapper sobre o bloco de controle de transferência. Aqui estão os membros mais importantes da 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()    
}

As mensagens são recebidas por assinatura. Assinatura delegada para o método de retorno de chamada:

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

Mensagem:

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

Para assinar um tipo de mensagem específico e/ou um remetente específico, dois parâmetros opcionais são usados: ReliableUdpMessageTypes messageType e IPEndPoint ipEndPoint.

Tipos de mensagem:

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

A mensagem é enviada de forma assíncrona, para isso o protocolo implementa um modelo de programação assíncrona:

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

O resultado do envio de uma mensagem será true - se a mensagem chegou ao destinatário com sucesso e false - se a conexão foi encerrada por timeout:

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusão

Muito não foi descrito neste artigo. Mecanismos de correspondência de threads, tratamento de exceções e erros, implementação de métodos de envio de mensagens assíncronas. Mas o núcleo do protocolo, a descrição da lógica para processar pacotes, estabelecer uma conexão e lidar com tempos limite, deve ser claro para você.

A versão demonstrada do protocolo de entrega confiável é robusta e flexível o suficiente para atender aos requisitos definidos anteriormente. Mas quero acrescentar que a implementação descrita pode ser melhorada. Por exemplo, para aumentar a taxa de transferência e alterar dinamicamente os períodos do temporizador, mecanismos como janela deslizante e RTT podem ser adicionados ao protocolo, também será útil implementar um mecanismo para determinar o MTU entre os nós de conexão (mas apenas se grandes mensagens forem enviadas) .

Obrigado pela atenção, aguardo seus comentários e comentários.

PS Para quem estiver interessado nos detalhes ou apenas quiser testar o protocolo, o link do projeto no GitHube:
Projeto UDP confiável

Links e artigos úteis

  1. Especificação do protocolo TCP: em ingles и на русском
  2. Especificação do protocolo UDP: em ingles и на русском
  3. Discussão do protocolo RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Protocolo de dados confiáveis: rf 908 и rf 1151
  5. Uma implementação simples de confirmação de entrega sobre UDP: Assuma o controle total de sua rede com .NET e UDP
  6. Artigo descrevendo mecanismos NAT traversal: Comunicação ponto a ponto entre tradutores de endereços de rede
  7. Implementação do modelo de programação assíncrona: Implementando o modelo de programação assíncrona CLR и Como implementar o padrão de design IAsyncResult
  8. Portando o modelo de programação assíncrona para o padrão assíncrono baseado em tarefas (APM em TAP):
    Programação assíncrona TPL e .NET tradicional
    Interoperabilidade com outros padrões e tipos assíncronos

Atualização: obrigado prefeito и sidristij pela ideia de adicionar uma tarefa à interface. A compatibilidade da biblioteca com sistemas operacionais antigos não é violada, porque A 4ª estrutura suporta o servidor XP e 2003.

Fonte: habr.com

Adicionar um comentário