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
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
- Entrega confiável de pacotes, realizada por meio de um mecanismo de reconhecimento positivo (o chamado positive acknowledgment)
- A necessidade de transmissão eficaz de grandes volumes de dados, ou seja, o protocolo deve evitar retransmissões desnecessárias de pacotes
- Deve haver a possibilidade de cancelar o mecanismo de confirmação de entrega (a possibilidade de operar como um protocolo UDP 'puro')
- Capacidade de implementar um modo de comando, com confirmação de cada mensagem
- 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 e , 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:
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:
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 , com UDP em , 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:
A estrutura do cabeçalho Reliable UDP é bastante simples:

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

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:

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:
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:
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:
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:
Links e artigos úteis
- Especificação do protocolo TCP: e
- Especificação do protocolo UDP: e
- Discussão sobre o protocolo RUDP:
- Protocolo de Dados Confiável: e
- Implementação simples de confirmação de entrega via UDP:
- Artigo descrevendo mecanismos para superar NATs:
- Implementação do modelo de programação assíncrona: e
- Transferindo o modelo de programação assíncrona para um modelo baseado em tarefas (APM no TAP):
Atualização: Obrigado e 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
