Implementação do protocolo Reliable UDP para .Net

A Internet mudou há muito tempo. Um dos principais protocolos da Internet – o UDP – é utilizado por aplicações não apenas para a entrega de datagramas e transmissões em massa, mas também para garantir conexões 'peer-to-peer' entre os nós da rede. Devido à sua simplicidade, este protocolo emergiu com várias aplicações não planejadas anteriormente; no entanto, as desvantagens do protocolo, como a ausência de entrega garantida, não desapareceram. Este artigo descreve a implementação de um protocolo de entrega garantida sobre o UDP.
Conteúdo:Introdução
Requisitos do protocolo
Cabeçalho Reliable UDP
Princípios gerais de operação do protocolo
Time-outs e temporizadores do protocolo
Diagrama de estados da transmissão Reliable UDP
Mais a fundo no código. Bloco de controle de transmissão
Mais a fundo no código. Estados

Mais a fundo no código. Criação e estabelecimento de conexões
Mais a fundo no código. Fechamento da conexão por time-out
Mais a fundo no código. Recuperação da transmissão de dados
API Reliable UDP
Conclusão
Links e artigos úteis

Introdução

A arquitetura inicial da Internet previa um espaço de endereço homogêneo, onde cada nó tinha um endereço IP global e único, e podia se comunicar diretamente com outros nós. Atualmente, a Internet, de fato, possui uma arquitetura diferente – uma área de endereços IP globais e várias áreas com endereços privados, ocultos atrás de dispositivos NAT.Nesta arquitetura, apenas dispositivos que estão no espaço de endereços global podem interagir facilmente com qualquer um na rede, pois possuem um IP roteável único e global. Um nó em uma rede privada pode se conectar a outros nós na mesma rede, assim como a outros nós bem conhecidos no espaço de endereços global. Tal interação é alcançada em grande parte graças ao mecanismo de tradução de endereços de rede. Dispositivos NAT, como roteadores Wi-Fi, criam registros especiais nas tabelas de tradução para conexões de saída e modificam endereços IP e números de porta nos pacotes. Isso permite estabelecer uma conexão de saída da rede privada com nós no espaço de endereços global. No entanto, os dispositivos NAT geralmente bloqueiam todo o tráfego de entrada, a menos que regras específicas sejam definidas para conexões de entrada.

Essa arquitetura da Internet é bastante adequada para a interação cliente-servidor, onde os clientes podem estar em redes privadas e os servidores possuem um endereço global. Contudo, isso cria dificuldades para a conexão direta entre dois nós em diferentes redes privadas. privadas. A conexão direta entre dois nós é crucial para aplicações peer-to-peer, como transmissão de voz (Skype), acesso remoto a computadores (TeamViewer) ou jogos online.

Um dos métodos mais eficazes para estabelecer uma conexão peer-to-peer entre dispositivos em diferentes redes privadas é chamado de "hole punching". Essa técnica é frequentemente utilizada em aplicativos baseados no protocolo UDP.

Porém, se o seu aplicativo requer entrega garantida de dados, como ao transferir arquivos entre computadores, o uso de UDP acarretará diversas dificuldades, uma vez que o UDP não é um protocolo de entrega garantida e não assegura a entrega dos pacotes na ordem correta, ao contrário do protocolo TCP.

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

Quero notar imediatamente que existe uma técnica chamada TCP hole punching para estabelecer conexões TCP entre nós em diferentes redes privadas, mas devido à falta de suporte dessa técnica por muitos dispositivos NAT, ela geralmente não é considerada o método principal para conectar esses nós.

Neste artigo, vou abordar apenas a implementação do protocolo de entrega garantida. A implementação da técnica UDP hole punching será descrita em artigos futuros.

Requisitos do protocolo

  1. Entrega confiável de pacotes, realizada por meio de um mecanismo de reconhecimento positivo (o chamado positive acknowledgment)
  2. A necessidade de transmissão eficaz de grandes volumes de dados, ou seja, o protocolo deve evitar retransmissões desnecessárias de pacotes
  3. Deve haver a possibilidade de cancelar o mecanismo de confirmação de entrega (a possibilidade de operar como um protocolo UDP 'puro')
  4. Capacidade de implementar um modo de comando, com confirmação de cada mensagem
  5. A unidade básica de transmissão de dados no protocolo deve ser uma mensagem

Esses requisitos coincidem em grande parte com os requisitos do Reliable Data Protocol, conforme descrito em rfc 908 e rfc 1151, e baseei-me nesses padrões ao desenvolver este protocolo.

Para entender esses requisitos, vamos considerar os diagramas temporais de transmissão de dados entre dois nós na rede pelos protocolos TCP e UDP. Suponha que em ambos os casos tenhamos perdido um pacote.
Transmissão de dados não interativos via TCP:Implementação do protocolo Reliable UDP para .Net

Como pode ser visto no diagrama, em caso de perda de pacotes, o TCP detectará o pacote perdido e informará o remetente, solicitando o número do segmento perdido.
Transmissão de dados via protocolo UDP:Implementação do protocolo Reliable UDP para .Net

O UDP não toma nenhuma medida para detectar perdas. O controle de erros de transmissão no protocolo UDP é totalmente de responsabilidade da aplicação.

A detecção de erros no protocolo TCP é alcançada por meio da conexão com o nó final, manutenção do estado dessa conexão, indicação do número de bytes enviados em cada cabeçalho de pacote e notificações de recebimento com o número de reconhecimento 'acknowledge number'.

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

Mais detalhes sobre o protocolo TCP podem ser encontrados em rfc 793, com UDP em rfc 768, onde eles, na verdade, são definidos.

A partir do que foi descrito acima, é claro que para criar um protocolo de entrega de mensagens confiável sobre o UDP (daqui em diante chamado de Reliable UDP), é necessário implementar mecanismos de transmissão de dados semelhantes aos do TCP. Ou seja:

  • manter o estado da conexão
  • usar numeração de segmentos
  • utilizar pacotes especiais de confirmação
  • usar um mecanismo de janela simplificado para aumentar a capacidade de transmissão do protocolo

Além disso, é necessário:

  • sinalizar o início da mensagem, para alocar recursos para a conexão
  • sinalizar o final da mensagem, para transmitir a mensagem recebida ao aplicativo superior e liberar os recursos do protocolo
  • permitir que o protocolo para conexões específicas desative o mecanismo de confirmação de entrega, para funcionar como um UDP "puro"

Cabeçalho Reliable UDP

Lembremos que o datagrama UDP é encapsulado dentro de um datagrama IP. O pacote Reliable UDP, por sua vez, é "embrulhado" em um datagrama UDP.
Encapsulamento do cabeçalho Reliable UDP:Implementação do protocolo Reliable UDP para .Net

A estrutura do cabeçalho Reliable UDP é bastante simples:

Implementação do protocolo Reliable UDP para .Net

  • Flags – bandeiras de controle do pacote
  • MessageType – tipo de mensagem, usado por aplicativos superiores para se inscrever em mensagens específicas
  • TransmissionId — número da transmissão, junto com o endereço e a porta do destinatário, identifica de forma única a conexão
  • PacketNumber – número do pacote
  • Options – opções adicionais do protocolo. No caso do primeiro pacote, é usado para indicar o tamanho da mensagem

As bandeiras são as seguintes:

  • FirstPacket — primeiro pacote da mensagem
  • NoAsk — a mensagem não requer a ativação do mecanismo de confirmação
  • LastPacket — último pacote da mensagem
  • RequestForPacket — pacote de confirmação ou solicitação de pacote perdido

Princípios gerais de operação do protocolo

Como o Reliable UDP é orientado à entrega garantida de mensagens entre dois nós, ele deve ser capaz de estabelecer uma conexão com o outro lado. Para estabelecer a conexão, o lado remetente envia um pacote com a bandeira FirstPacket, cuja resposta significará a conexão estabelecida. Todos os pacotes de resposta, ou, de outra forma, pacotes de confirmação, sempre definem o campo PacketNumber como um mais do que o maior valor de PacketNumber dos pacotes que chegaram com sucesso. No campo Options do primeiro pacote enviado, é registrado o tamanho da mensagem.

Um completar a conexão, é utilizado um mecanismo similar. No último pacote de mensagens, um sinalizador LastPacket é definido. No pacote de resposta, é indicado o número do último pacote + 1, o que significa para a parte receptora que a entrega da mensagem foi bem-sucedida.
Diagrama de estabelecimento e encerramento de conexão:Implementação do protocolo Reliable UDP para .Net

Quando a conexão é estabelecida, a transmissão 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, que é igual ao tamanho da janela de recepção/transmissão. O último bloco de dados pode ter um número menor de pacotes. Após o envio de cada bloco, a parte remetente aguarda a confirmação de entrega ou solicita a retransmissão de pacotes perdidos, mantendo a janela de recepção/transmissão aberta para receber respostas. Após receber a confirmação de entrega do bloco, a janela de recepção/transmissão é deslocada e o próximo bloco de dados é enviado.

A parte receptora aceita os pacotes. Cada pacote é verificado para ver se está dentro da janela de transmissão. Pacotes que não cabem na janela e duplicatas são descartados. Como o tamanho da janela é fixo e igual para o receptor e o remetente, no caso de entrega do bloco de pacotes sem perdas, a janela é deslocada para receber os pacotes do próximo bloco de dados e uma confirmação de entrega é enviada. Se a janela não for preenchida dentro do período estabelecido pelo timer de trabalho, uma verificação será iniciada para identificar quais pacotes não foram entregues e serão enviadas solicitações de retransmissão.
Diagrama de retransmissão:Implementação do protocolo Reliable UDP para .Net

Time-outs e temporizadores do protocolo

Existem várias razões pelas quais uma conexão pode não ser estabelecida. Por exemplo, se a parte receptora estiver fora da rede. Nesse caso, ao tentar estabelecer a conexão, a conexão será encerrada por tempo limite. Na implementação do Reliable UDP, são utilizados dois timers para o estabelecimento de timeouts. O primeiro, o timer de trabalho, serve para aguardar a resposta do host remoto. Se ele expirar do lado do remetente, a retransmissão do último pacote enviado é realizada. Se o timer expirar no receptor, uma verificação será feita para identificar pacotes perdidos e solicitações de retransmissão serão enviadas.

O segundo temporizador é necessário para encerrar a conexão em caso de ausência de comunicação entre os nós. Para a parte remetente, ele é acionado imediatamente após a ativação do temporizador de trabalho e aguarda a resposta do nó remoto. Caso não haja resposta dentro do período estabelecido, a conexão é encerrada e os recursos são liberados. Para a parte receptora, o temporizador de encerramento da conexão é acionado após a ativação dupla do temporizador de trabalho. Isso é necessário para prevenir a perda do pacote de confirmação. Quando o temporizador é acionado, a conexão também é encerrada e os recursos são liberados.

Diagrama de estados da transmissão Reliable UDP

Os princípios de funcionamento do protocolo são implementados em uma máquina de estados, cada estado da qual responde por uma determinada lógica de processamento dos pacotes.
Diagrama de estados do Reliable UDP:

Implementação do protocolo Reliable UDP para .Net

Fechado – na verdade, não é um estado, é um ponto inicial e final para a máquina. O estado Fechado é considerado um bloco de controle de transmissão que, ao implementar um servidor UDP assíncrono, redireciona pacotes para as conexões apropriadas e inicia o processamento dos estados.

PrimeiroPacoteEnviado – estado inicial em que a conexão de saída se encontra ao enviar uma mensagem.

Neste estado, o primeiro pacote é enviado para mensagens normais. Para mensagens sem confirmação de envio, este é o único estado – nele ocorre o envio de toda a mensagem.

CicloDeEnvio – estado principal para a transmissão de pacotes de mensagem.

A transição para ele do estado PrimeiroPacoteEnviado ocorre após o envio do primeiro pacote de mensagem. É para este estado que vão todas as confirmações e pedidos de retransmissão. A saída dele é possível em dois casos – em caso de entrega bem-sucedida da mensagem ou por timeout.

PrimeiroPacoteRecebido – estado inicial para o receptor da mensagem.

Neste estado, é verificada a correção do início da transmissão, são criadas as estruturas necessárias e enviada a confirmação do recebimento do primeiro pacote.

Para uma mensagem composta de um único pacote e enviada sem a confirmação de entrega, este é o único estado. Após o processamento dessa mensagem, a conexão é encerrada.

Montagem – estado principal para o recebimento de pacotes de mensagem.

Ele grava pacotes em um armazenamento temporário, verifica a ausência de perda de pacotes, envia confirmações de entrega do bloco de pacotes e da mensagem completa, e solicita a retransmissão de pacotes perdidos. Em caso de recebimento bem-sucedido da mensagem completa – a conexão passa para o estado Completed, caso contrário, um tempo limite é executado.

Completed – fechamento da conexão em caso de recebimento bem-sucedido da mensagem completa.

Esse estado é necessário para compor a mensagem e para o caso em que a confirmação de entrega da mensagem foi perdida a caminho do remetente. A saída desse estado ocorre após o tempo limite, mas a conexão é considerada fechada com sucesso.

Mais a fundo no código. Bloco de controle de transmissão

Um dos elementos-chave do Reliable UDP é o bloco de controle de transmissão. A função desse bloco é armazenar as conexões atuais e elementos auxiliares, distribuir os pacotes recebidos pelas conexões apropriadas, fornecer uma interface para envio de pacotes à conexão e implementar a API do protocolo. O bloco de controle de transmissão recebe pacotes do nível UDP e os redireciona para processamento no autômato finito. Um servidor UDP assíncrono foi implementado para receber pacotes.
Alguns membros da classe ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // array de bytes para a chave especificada. Usado para compor mensagens de entrada
  public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
  // array de bytes para a chave especificada. Usado para enviar mensagens de saída.
  public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
  // registro da conexão para a chave especificada.
  private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
  // lista de assinantes de mensagens.
  private readonly List m_subscribers;
  // socket local
  private Socket m_socketIn;
  // porta para mensagens de entrada
  private int m_port;
  // endereço IP local
  private IPAddress m_ipAddress;
  // ponto final local
  public IPEndPoint LocalEndpoint { get; private set; }
  // coleção de estados
  // do autômato finito previamente inicializados
  public StatesCollection States { get; private set; }
  // gerador de números aleatórios. Usado para criar TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;
  	//...
}

Implementação de um servidor UDP assíncrono:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // Criamos um novo buffer para cada socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // Passamos o buffer como parâmetro para o método assíncrono
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  // Pacote recebido, prontos para aceitar o próximo        
  Receive();
  // O jeito mais simples de resolver a questão do buffer é obter uma referência a ele 
  // do IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // Obtemos o cabeçalho do pacote        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // Pacote recebido é inválido - descartamos
    return;
  }
  // Construa uma chave para identificar o registro da conexão para o pacote
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // Obtemos o registro da conexão existente ou criamos um novo
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // Iniciamos o processamento do pacote na máquina de estados
  record.State.ReceivePacket(record, header, bytes);
}

Para cada transmissão de mensagem, uma estrutura contendo informações sobre a conexão é criada. Essa estrutura é chamada de registro de conexão.
Alguns membros da classe ReliableUdpConnectionRecord:

classe interna ReliableUdpConnectionRecord : IDisposable
{    
  // matriz de bytes com a mensagem    
  public byte[] IncomingStream { get; set; }
  // referência para o estado da máquina de estados    
  public ReliableUdpState State { get; set; }    
  // par que identifica univocamente o registro de conexão
  // no bloco de controle de transmissão     
  public Tuple Key { get; private set;}
  // limite inferior da janela de recebimento    
  public int WindowLowerBound;
  // tamanho da janela de transmissão
  public readonly int WindowSize;     
  // número do pacote a ser enviado
  public int SndNext;
  // número de pacotes a serem enviados
  public int NumberOfPackets;
  // número de transmissão (esta é a segunda parte do Tuple)
  // cada mensagem tem o seu próprio	
  public readonly Int32 TransmissionId;
  // endpoint IP remoto – o destinatário da mensagem
  public readonly IPEndPoint RemoteClient;
  // tamanho do pacote, para evitar fragmentação a nível IP
  // não deve exceder o MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // bloco de controle de transmissão
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // encapsula os resultados da operação assíncrona para BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // não enviar pacotes de confirmação
  public bool IsNoAnswerNeeded;
  // último pacote recebido corretamente (sempre definido para o maior número)
  public int RcvCurrent;
  // matriz com os números dos pacotes perdidos
  public int[] LostPackets { get; private set; }
  // o último pacote chegou? Usado como bool.
  public int IsLastPacketReceived = 0;
  //...
}

Mais a fundo no código. Estados

Os estados implementam a máquina de estados do protocolo Reliable UDP, 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 de operação do protocolo é realizada pelas classes apresentadas acima, juntamente com uma classe auxiliar que fornece métodos estáticos, como, por exemplo, a construção do cabeçalho ReliableUdp a partir do registro de conexão.

Em seguida, serão examinadas em detalhes as implementações dos métodos da interface que definem os principais algoritmos de operação do protocolo.

Método DisposeByTimeout

O método DisposeByTimeout é responsável por liberar os recursos da conexão após o tempo limite e para sinalizar a entrega bem-sucedida/não bem-sucedida da mensagem.
ReliableUdpState.DisposeByTimeout:

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

Ele é sobrescrito apenas no estado Completed.
Completed.DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // Notificamos que a mensagem foi recebida com sucesso
  SetAsCompleted(connectionRecord);
}

Método ProcessPackets

O método ProcessPackets é responsável por um processamento adicional de um ou mais pacotes. É chamado diretamente ou através de um temporizador de espera.

Em estado Montagem o método é sobrecarregado e é responsável por verificar pacotes perdidos e mudar para o estado Completed, ao receber o último pacote e passar pela verificação bem-sucedida
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // há pacotes perdidos, solicitamos os mesmos
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // configuramos o timer pela segunda vez, para tentativas de retransmissão
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // se após duas tentativas de WaitForPacketTimer
    // não conseguimos receber pacotes - iniciamos o timer de fechar a conexão
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // verificação bem-sucedida 
  {
    // enviamos confirmação de recebimento do bloco de dados
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // em vez de implementação imediata de recursos
    // iniciamos o timer, caso
    // se o último ack não chegar ao remetente e ele solicitar novamente.
    // ao expirar o timer - implementamos recursos
    // no estado Completed, o método do timer é sobrecarregado
    StartCloseWaitTimer(connectionRecord);
  }
  // este é o caso em que a confirmação do bloco de pacotes foi perdida
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // iniciamos o timer de fechamento da conexão
    StartCloseWaitTimer(connectionRecord);
  }
}

Em estado CicloDeEnvio este método é chamado apenas pelo timer, e é responsável pela retransmissão da última mensagem, bem como pela ativação do timer de fechamento da conexão.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  // reenviamos o último pacote
  // (em caso de recuperação da conexão, o nó receptor reenvia os pedidos que não chegaram até ele)
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // ativamos o timer CloseWait – para aguardar a recuperação ou término da conexão
  StartCloseWaitTimer(connectionRecord);
}

Em estado Completed o método para o timer de trabalho e envia uma mensagem para os assinantes.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // coletamos a mensagem e a enviamos para os assinantes
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Método ReceivePacket

Em estado PrimeiroPacoteRecebido a principal tarefa do método é determinar se realmente o primeiro pacote da mensagem chegou à interface, além de coletar a mensagem que consiste em um único pacote.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // descartamos o pacote
    return;
  // a combinação de duas flags - FirstPacket e LastPacket - indica que temos uma mensagem única
  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))
    {
      // enviamos pacote de confirmação
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // por design, todos os números de pacote começam com 0;
  if (header.PacketNumber != 0)
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // contamos o número de pacotes que devem chegar
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // registramos o número do último pacote recebido (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // depois movemos a janela de recepção para 1
  connectionRecord.WindowLowerBound++;
  // trocamos o estado
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // se não for necessário o mecanismo de confirmação
  // iniciamos o temporizador que liberará todas as estruturas
  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 estado CicloDeEnvio Este método foi sobrescrito para receber 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;
  // cálculo do limite superior da janela
  // pega o limite da janela + 1, para receber confirmação de entrega
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // verificação se está dentro da janela        
  if (header.PacketNumber  windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // verificar se é o último pacote:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // transmissão concluída
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // esta é uma resposta ao primeiro pacote com confirmação         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // sem deslocamento da janela
    SendPacket(connectionRecord);
  }
  // chegou confirmação de recebimento do bloco de dados
  else if (header.PacketNumber == windowHighestBound)
  {
    // deslocamos a janela de recebimento/transmissão
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // zeramos o array de controle de transmissão
    connectionRecord.WindowControlArray.Nullify();
    // enviamos o bloco de pacotes
    SendPacket(connectionRecord);
  }
  // este é um pedido para retransmissão – enviamos o pacote requerido          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Em estado Montagem No método ReceivePacket, a principal tarefa é montar a mensagem a partir dos pacotes recebidos.
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // processamento de pacotes sem o mecanismo de confirmação de entrega
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // redefinindo o timer
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // gravando os dados
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // se recebemos um pacote com a última flag - finalizamos
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }
  // cálculo do limite superior da janela
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // descartando pacotes fora da janela
  if (header.PacketNumber  (windowHighestBound))
    return;
  // descartando duplicatas
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // gravando os dados
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // aumentando o contador de pacotes
  connectionRecord.PacketCounter++;
  // gravando no array de controle da janela o número atual do pacote
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // definindo o maior pacote recebido
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // reiniciando os timers
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // se o último pacote chegou
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // se recebemos todos os pacotes da janela, redefinimos o contador
  // e enviamos um pacote de confirmação
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // redefinindo o contador.
    connectionRecord.PacketCounter = 0;
    // movendo a janela de transmissão
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // zerando o array de controle de transmissão
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // se o último pacote já foi recebido
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // verificando pacotes
    ProcessPackets(connectionRecord);
  }
}

Em estado Completed A única tarefa do método é enviar uma confirmação de retransmissão da entrega bem-sucedida da mensagem.
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // reenvio do último pacote, pois
  // o último ack não chegou ao remetente
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Método SendPacket

Em estado PrimeiroPacoteEnviado este método realiza o envio do primeiro pacote de dados ou, se a mensagem não requer confirmação de entrega, toda a mensagem.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // se a confirmação não for necessária, enviamos todos os pacotes
  // e liberamos recursos
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Aqui ocorre o envio As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // criamos o cabeçalho do pacote e o enviamos 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // aumentamos o contador
  connectionRecord.SndNext++;
  // movemos a janela
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Iniciamos o temporizador
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Em estado CicloDeEnvio neste método ocorre o envio de um bloco de pacotes.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // enviamos um bloco de pacotes      
  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++;
  }
  // em caso de uma grande janela de transmissão, reinicializamos o temporizador após o envio
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
  {
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  }
}

Mais a fundo no código. Criação e estabelecimento de conexões

Agora que conhecemos os principais estados e métodos usados para gerenciar estados, podemos analisar um pouco mais alguns exemplos de funcionamento do protocolo.
Diagrama de transmissão de dados em condições normais:Implementação do protocolo Reliable UDP para .Net

Vamos analisar em detalhes a criação registro de conexão para a conexão e envio do primeiro pacote. O iniciador da transmissão é sempre o aplicativo que chama o método API para enviar a mensagem. Em seguida, é utilizado o método StartTransmission do bloco de controle de transmissão, que inicia a transmissão de dados para a nova mensagem.
Criação de 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("", "Você deve usar o construtor com parâmetros ou iniciar o listener antes de enviar a mensagem");
    }
    // inicia o processamento de pacotes de entrada
    StartListener(LocalEndpoint);
  }
  // cria uma chave para o dicionário com base em EndPoint e ReliableUdpHeader.TransmissionId
  byte[] transmissionId = new byte[4];
  // gera um número aleatório transmissionId
  m_randomCrypto.GetBytes(transmissionId);
  Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // cria uma nova entrada para a conexão e verifica,
  // se já existe esse número em nossos dicionários
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // se existir – gera novamente um número aleatório
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // se novamente não for possível – gera uma exceção
      throw new ArgumentException("O par TransmissionId & EndPoint já existe no dicionário");
  }
  // estado iniciado para processamento
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Envio do primeiro pacote (estado FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;
  // ...
  // cria o cabeçalho do pacote e o envia
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // incrementa o contador
  connectionRecord.SndNext++;
  // desloca a janela
  connectionRecord.WindowLowerBound++;
  // muda para o estado SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Inicia o timer
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Após o envio do primeiro pacote, o remetente muda para o estado CicloDeEnvio – aguardando confirmação de entrega do pacote.
A parte receptora, utilizando o método EndReceive, recebe o pacote enviado, cria um novo registro de conexão e encaminha este pacote, com o cabeçalho previamente analisado, para processamento no método ReceivePacket do estado. PrimeiroPacoteRecebido
Estabelecendo a conexão do lado receptor:

private void EndReceive(IAsyncResult ar)
{
  \/\/ ...
  \/\/ pacote recebido
  \/\/ analisando o cabeçalho do pacote        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    \/\/ pacote inválido recebido - descartando
    return;
  }
  \/\/ construindo a chave para determinar o registro de conexão do pacote
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  \/\/ obtendo o registro de conexão existente ou criando um novo
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  \/\/ iniciando o processamento do pacote na máquina de estados
  record.State.ReceivePacket(record, header, bytes);
}

Recepção do primeiro pacote e envio de confirmação (estado FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    \/\/ descartando pacote
    return;
  \/\/ ...
  \/\/ por design todos os números de pacote começam em 0;
  if (header.PacketNumber != 0)          
    return;
  \/\/ inicializando array para armazenar partes da mensagem
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  \/\/ gravando dados do pacote no array
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  \/\/ contando o número de pacotes que devem chegar
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length/(double)connectionRecord.BufferSize));
  \/\/ gravando o número do último pacote recebido (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  \/\/ após, deslocamos a janela de recepção em 1
  connectionRecord.WindowLowerBound++;
  \/\/ alternando o estado
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  if (\/\*se o mecanismo de confirmação não for necessário*\/)
  \/\/ ...
  else
  {
    \/\/ enviando confirmação
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Mais a fundo no código. Fechamento da conexão por time-out

O tratamento de timeouts é uma parte importante do Reliable UDP. Vamos considerar um exemplo em que ocorreu uma falha em um nó intermediário, tornando a entrega de dados em ambas as direções impossível.
Diagrama de fechamento de conexão por timeout:Implementação do protocolo Reliable UDP para .Net

Como pode ser visto no diagrama, o timer de trabalho do remetente é acionado imediatamente após o envio do bloco de pacotes. Isso ocorre no método SendPacket do estado CicloDeEnvio.
Ativação do timer de trabalho (estado SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  \/\/ enviando bloco de pacotes   
  \/\/ ...   
  \/\/ reiniciando o timer após o envio
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
}

Os períodos do temporizador são definidos ao criar a conexão. Por padrão, o ShortTimerPeriod é de 5 segundos. No exemplo, ele está definido para 1,5 segundos.

No caso da conexão de entrada, o temporizador é iniciado após o recebimento do último pacote de dados, isso ocorre no método ReceivePacket do estado. Montagem
Ativando o temporizador de trabalho (estado Assembling):

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

Na conexão de entrada, não chegaram mais pacotes durante o tempo de espera do temporizador de trabalho. O temporizador foi acionado e chamou o método ProcessPackets, onde foram detectados pacotes perdidos e os pedidos de retransmissão foram enviados pela primeira vez.
Envio de solicitações de retransmissão (estado Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*verificação de pacotes perdidos */)
  {
    // enviamos solicitações de retransmissão
    // configuramos o temporizador pela segunda vez, para uma tentativa de retransmissão
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // se após duas tentativas de WaitForPacketTimer 
    // não foi possível receber pacotes - iniciamos o temporizador de fechamento da conexão
    StartCloseWaitTimer(connectionRecord);
  }
  else if (/*chegou o último pacote e verificação bem-sucedida */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // se o ack do bloco de pacotes foi perdido
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // reenviamos o ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // iniciamos o temporizador de fechamento da conexão
    StartCloseWaitTimer(connectionRecord);
  }
}

A variável TimerSecondTry foi definida como verdadeiro. Esta variável é responsável pelo reinício do temporizador de trabalho.

Do lado do remetente, o temporizador 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)
{
  // ...        
  // reenviamos o último pacote 
  // ...        
  // ativamos o temporizador CloseWait – para esperar a recuperação da conexão ou seu fechamento
  StartCloseWaitTimer(connectionRecord);
}

Após isso, um temporizador de fechamento de conexão é iniciado na conexão de saída.
ReliableUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

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

Após um breve período, o temporizador de trabalho é acionado novamente do lado do receptor, novas solicitações são enviadas, após o que o temporizador de fechamento de conexão da conexão de entrada é iniciado.

Quando os temporizadores de fechamento são acionados, todos os recursos de ambos os registros de conexão são liberados. O remetente informa sobre a entrega falhada à aplicação superior (veja a API Reliable UDP).
Liberação de recursos do 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 a fundo no código. Recuperação da transmissão de dados

Diagrama de recuperação de transmissão de dados em caso de perda de pacote:Implementação do protocolo Reliable UDP para .Net

Como já discutido no fechamento de conexão por timeout, após a expiração do temporizador de trabalho no receptor, uma verificação por pacotes perdidos será realizada. Caso sejam detectadas perdas, será elaborado uma lista dos números dos pacotes que não chegaram ao receptor. Esses números são inseridos no array LostPackets da conexão específica e solicitações são enviadas para reenvio.
Envio de solicitações para reenvio de pacotes (estado Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // há pacotes perdidos, enviamos solicitações para eles
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

O remetente aceitará o pedido de reenvio e enviará os pacotes ausentes. Vale ressaltar que, neste momento, o temporizador de fechamento de conexão já está em funcionamento no remetente e, ao receber o pedido, ele é reiniciado.
Reenvio de pacotes perdidos (estado SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  \/\/ ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  \/\/ redefinir o timer de fechamento da conexão 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  \/\/ ...
  \/\/ isso é um pedido de retransmissão – enviamos o pacote solicitado          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

O pacote retransmitido (packet#3 no diagrama) é recebido pela conexão de entrada. É feita uma verificação para preencher a janela de recebimento, e a transmissão de dados normal é retomada.
Verificação para ver se está na janela de recebimento (estado Assembling):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  \/\/ ...
  \/\/ incrementa o contador de pacotes        
  connectionRecord.PacketCounter++;
  \/\/ grava no array de controle da janela o número atual do pacote        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  \/\/ define o maior pacote recebido        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  \/\/ reinicia os timers        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  \/\/ ...
  \/\/ se recebemos todos os pacotes da janela, então redefinimos o contador
  \/\/ e enviamos o pacote de confirmação
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    \/\/ redefine o contador.      
    connectionRecord.PacketCounter = 0;
    \/\/ deslocou a janela de transmissão
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    \/\/ zerar o array de controle de transmissão
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  \/\/ ...
}

API Reliable UDP

Para interagir com o protocolo de transmissão de dados, existe uma classe pública Reliable Udp, que é uma camada sobre o bloco de controle de transmissão. Aqui estão os membros mais importantes da classe:

classe pública selada ReliableUdp : IDisposable
{
   obt1m o ponto final local
  public IPEndPoint LocalEndpoint    
   cria uma inst2ncia de ReliableUdp e inicia
   a escuta de pacotes de entrada no IP especificado
   e na porta. Um valor 0 para a porta significa uso
   de uma porta alocada dinamicamente
  public ReliableUdp(IPAddress localAddress, int port = 0) 
   assinatura para receber mensagens de entrada
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
   cancelamento da assinatura para receber mensagens
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
   envia uma mensagem de forma assíncrona 
   Nota: a compatibilidade com XP e Server 2003 é mantida, pois
   usa .NET Framework 4.0
  public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
   inicia o envio assíncrono da mensagem
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
   obtém o resultado do envio assíncrono
  public bool EndSendMessage(IAsyncResult asyncResult)  
   libera recursos
  public void Dispose()    
}

A recepção de mensagens é feita por assinatura. A assinatura do delegado para o método de retorno de chamada:

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

Mensagem:

public class ReliableUdpMessage
{
   tipo da mensagem, enum simples
  public ReliableUdpMessageTypes Type { get; private set; }
   dados da mensagem
  public byte[] Body { get; private set; }
   se definido como true - o mecanismo de reconhecimento de entrega será desativado
   para o envio de uma mensagem específica
  public bool NoAsk { get; private set; }
}

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

Tipos de mensagens:

public enum ReliableUdpMessageTypes : short
{ 
   Qualquer
  Any = 0,
   Solicitação para o servidor STUN 
  StunRequest = 1,
   Resposta do servidor STUN
  StunResponse = 2,
   Transferência de arquivo
  FileTransfer =3,
   ...
}

O envio da mensagem é feito de forma assíncrona, para isso o protocolo implementa um modelo de programação assíncrono:

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

O resultado do envio da mensagem será true - se a mensagem foi entregue ao destinatário e false - se a conexão foi fechada por timeout:

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusão

Muitas coisas não foram descritas neste artigo. Mecanismos de sincronização de fluxos, tratamento de exceções e erros, implementação de métodos assíncronos de envio de mensagens. Mas o núcleo do protocolo, a descrição da lógica de processamento de pacotes, o estabelecimento de conexão e o tratamento de timeouts devem ser esclarecidos para você.

A versão do protocolo de entrega confiável demonstrada é suficientemente robusta e flexível, atendendo a requisitos anteriormente definidos. No entanto, quero acrescentar que a implementação descrita pode ser aprimorada. Por exemplo, para aumentar a largura de banda e alterar dinamicamente os períodos dos temporizadores, mecanismos como sliding window e RTT podem ser adicionados ao protocolo, e também seria útil implementar um mecanismo para determinar o MTU entre os nós de conexão (mas apenas no caso de envio de mensagens grandes).

Obrigado pela atenção, aguardo suas comentários e sugestões.

P.S. Para aqueles que estão interessados em detalhes ou apenas querem testar o protocolo, aqui está o link para o projeto no GitHub:
Projeto Reliable UDP

Links e artigos úteis

  1. Especificação do protocolo TCP: em inglês e em português
  2. Especificação do protocolo UDP: em inglês e em português
  3. Discussão sobre o protocolo RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Protocolo de Dados Confiável: rfc 908 e rfc 1151
  5. Implementação simples de confirmação de entrega via UDP: Tenha Total Controle Sobre Suas Redes Com .NET E UDP
  6. Artigo descrevendo mecanismos para superar NATs: Comunicação Peer-to-Peer Através de Tradutores de Endereço de Rede
  7. Implementação do modelo de programação assíncrona: Implementando o Modelo de Programação Assíncrona CLR e Como implementar o padrão de design IAsyncResult
  8. Transferindo o modelo de programação assíncrona para um modelo baseado em tarefas (APM no TAP):
    TPL e Programação Assíncrona Tradicional .NET
    Interoperação com Outros Padrões e Tipos Assíncronos

Atualização: Obrigado mayorovp e sidristij pela ideia de adicionar uma tarefa à interface. A compatibilidade da biblioteca com sistemas operacionais antigos não é afetada, uma vez que o .NET 4 suporta tanto XP quanto 2003 server.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster