📜 ⬆️ ⬇️

Implementing a Reliable Udp Protocol for .Net

The Internet has changed a long time ago. One of the main Internet protocols, UDP, is used by applications not only to deliver datagrams and broadcasts, but also to provide peer-to-peer connections between network nodes. Due to its simple device, this protocol has a lot of not previously planned methods of use, however, protocol flaws, such as the lack of guaranteed delivery, have not disappeared anywhere. This article describes the implementation of the guaranteed delivery protocol over UDP.

Introduction


The original architecture of the Internet meant a uniform address space in which each node had a global and unique IP address and could communicate directly with other nodes. Now the Internet, in fact, has a different architecture - one area of ​​global IP addresses and many areas with private addresses hidden behind NAT devices. In such an architecture, only devices located in the global address space can easily communicate with anyone on the network because they have a unique, global, routable IP address. A node that is in a private network can connect to other nodes on the same network, as well as connect to other well-known nodes in the global address space. This interaction is achieved largely due to the network address translation mechanism . NAT devices, such as Wi-Fi routers, create special entries in the translation tables for outgoing connections and modify the IP addresses and port numbers in the packets. This allows you to establish an outgoing connection to nodes in the global address space from a private network. But at the same time, NAT devices usually block all incoming traffic, unless separate rules are established for incoming connections.

This architecture of the Internet is sufficiently correct for client-server interaction, when clients can be in private networks, and servers have a global address. But it makes it difficult to directly connect two nodes between different private networks. Direct connection of two nodes is important for “peer-to-peer” applications, such as voice transmission (Skype), remote access to a computer (TeamViewer), or online games.

One of the most effective methods for establishing peer-to-peer connections between devices located in different private networks is called “hole punching”. This technique is most commonly used with applications based on the UDP protocol.
')
But if your application requires guaranteed data delivery, for example, you transfer files between computers, then using UDP will cause many difficulties due to the fact that UDP is not a guaranteed delivery protocol and does not provide delivery of packets in order, unlike TCP.

In this case, to ensure guaranteed packet delivery, it is required to implement an application-level protocol that provides the necessary functionality and works over UDP.

Just want to note that there is a TCP hole punching technique for establishing TCP connections between nodes in different private networks, but due to the lack of support for it by many NAT devices, it is usually not considered as the main way to connect such nodes.

Further in this article I will consider only the implementation of the guaranteed delivery protocol. The implementation of UDP hole punching techniques will be described in the following articles.

Protocol requirements


  1. Reliable packet delivery implemented through positive feedback mechanism (so-called positive acknowledgment)
  2. The need for efficient transfer of big data, i.e. protocol must avoid unnecessary packet retransmissions
  3. It should be possible to cancel the delivery confirmation mechanism (the ability to function as a "pure" UDP protocol)
  4. Ability to implement command mode, with confirmation of each message
  5. The basic unit of data transmission protocol must be a message

These requirements largely coincide with the requirements for the Reliable Data Protocol, described in rfc 908 and rfc 1151 , and I based on these standards in the development of this protocol.

To understand these requirements, let's look at the timing of data transfer between two nodes on the network protocols TCP and UDP. Suppose that in both cases we have one package lost.
Transmission of non-interactive data via TCP:


As can be seen from the diagram, in case of packet loss, TCP will detect the lost packet and notify the sender of this by requesting the number of the lost segment.
Data transmission via UDP protocol:


UDP takes no loss detection steps. Control of transmission errors in the UDP protocol is completely on the application.

Error detection in the TCP protocol is achieved by establishing a connection with the end node, maintaining the state of this connection, specifying the number of bytes sent in each packet header, and receiving notifications using the “acknowledge number” confirmation number.

Additionally, to improve performance (ie, sending more than one segment without receiving confirmation), the TCP protocol uses the so-called transmission window — the number of bytes of data that the sender of the segment expects to receive.

More details on the TCP protocol can be found in rfc 793 , with UDP in rfc 768 , where they are, strictly speaking, defined.

From the above, it is clear that to create a reliable message delivery protocol over UDP (hereinafter referred to as Reliable UDP ), it is required to implement data transfer mechanisms similar to TCP. Namely:
Additionally required:

Reliable UDP header


Recall that a UDP datagram is encapsulated into an IP datagram. The Reliable UDP packet is correspondingly wrapped in a UDP datagram.
Reliable UDP Header Encapsulation:


The structure of the Reliable UDP header is quite simple:




Flags are as follows:

General principles of the protocol


Since Reliable UDP is focused on guaranteed message transfer between two nodes, it must be able to establish a connection with the other party. To establish a connection, the sending side sends a packet with the FirstPacket flag, the answer to which will mean setting up the connection. All reply packets, or, in other words, acknowledgment packets, always set the value of the PacketNumber field to one greater than the highest PacketNumber value of successfully received packets. In the Options field for the first packet sent, the message size is recorded.

A similar mechanism is used to terminate the connection. The last batch of the message sets the LastPacket flag. The reply packet indicates the number of the last packet + 1, which for the receiving party means successful delivery of the message.
Connection establishment and termination diagram:


When the connection is established, data transfer begins. Data is transmitted in blocks of packets. Each block, except the last, contains a fixed number of packets. It is equal to the size of the receive / transmit window. The last data block may have fewer packets. After sending each block, the sending side waits for a confirmation of delivery, or a request to re-deliver the lost packets, leaving the receive / transmit window open to receive replies. After receiving confirmation of the delivery of the block, the transmit / receive window is shifted and the next block of data is sent.

The receiving party accepts packets. Each packet is checked for entry into the transmission window. Packages that do not enter the window and duplicates are eliminated. Since Since the window size is strictly fixed and the same for both the receiver and the sender, in the case of delivery of a block of packets without losses, the window is shifted to receive packets of the next data block and a delivery confirmation is sent. If the window does not fill up after the period set by the working timer, a check will be launched on which packets have not been delivered and re-delivery requests will be sent.
Retransmission Diagram:


Timeouts and protocol timers


There are several reasons why a connection cannot be established. For example, if the receiving party is offline. In this case, when trying to establish a connection, the connection will be closed on timeout. The Reliable UDP implementation uses two timers to set timeouts. The first, a work timer, is used to wait for a response from a remote host. If it is triggered on the sending side, then the last sent packet is re-sent. If the timer is triggered by the recipient, then it checks for lost packets and sends re-delivery requests.

The second timer is necessary to close the connection in case of lack of communication between the nodes. For the sender side, it starts immediately after the working timer is triggered, and waits for a response from the remote node. If there is no response for the established period, the connection is terminated and the resources are released. For the recipient side, the connection closure timer starts after the double operation of the working timer. This is necessary for insurance against the loss of a confirmation package. When the timer is triggered, the connection is also completed and resources are released.

Reliable UDP transmission state diagram


The principles of the protocol are implemented in a finite state machine, each state of which is responsible for a certain packet processing logic.
Reliable UDP state diagram:



Closed - not really a state, it is the starting and ending point for the automaton. For the Closed state, the transmission control block is taken, which, realizing an asynchronous UDP server, redirects packets to the appropriate connections and starts state processing.

FirstPacketSending - the initial state in which the outgoing connection is located when sending a message.

In this state, the first packet is sent for regular messages. For messages without sending confirmation, this is the only state - the entire message is sent there.

SendingCycle — The ground state for sending message packets.

Transition to it from the FirstPacketSending state is carried out after sending the first message packet. All confirmations and requests for retransmissions come to this state. Exit from it is possible in two cases - in case of successful delivery of the message or on time-out.

FirstPacketReceived - the initial state for the recipient of the message.

It checks the correctness of the start of the transfer, creates the necessary structures, and sends an acknowledgment that the first packet has been received.

For a message consisting of a single packet and sent without using delivery confirmation, this is the only state. After processing such a message, the connection is closed.

Assembling - the main state for receiving message packets.

It writes packages to the temporary storage, checks for packet loss, sends confirmation of block block delivery and full messages, and sends requests for the re-delivery of lost packets. In case of successful receipt of the entire message, the connection goes to the Completed state, otherwise the output is timed out.

Completed - closing the connection in case of successful receipt of the entire message.

This state is necessary to assemble the message and for the case when the confirmation of the message delivery was lost on the way to the sender. The exit from this state is made on a time-out, but the connection is considered to be closed successfully.

Deeper into the code. Transmission Control Unit


One of the key elements of Reliable UDP is the transmission control block. The task of this unit is to store current connections and auxiliary elements, distribute incoming packets to the appropriate connections, provide an interface for sending packets to the connection, and implement the protocol API. The transmission control block receives packets from the UDP layer and forwards them to the state machine for processing. To receive packets, it uses an asynchronous UDP server.
Some members of the ReliableUdpConnectionControlBlock class:
internal class ReliableUdpConnectionControlBlock : IDisposable { //     .      public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;} //     .     . public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; } // connection record   . private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers; //    . private readonly List<ReliableUdpSubscribeObject> m_subscribers; //   private Socket m_socketIn; //     private int m_port; //  IP  private IPAddress m_ipAddress; //    public IPEndPoint LocalEndpoint { get; private set; } //    //    public StatesCollection States { get; private set; } //   .    TransmissionId private readonly RNGCryptoServiceProvider m_randomCrypto; //... } 


Implement an asynchronous UDP server:
 private void Receive() { EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0); //   ,   socket.BeginReceiveFrom byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length]; //         this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer); } private void EndReceive(IAsyncResult ar) { EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0); int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient); // ,    Receive(); // ..       -     //  IAsyncResult.AsyncState byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead); //    ReliableUdpHeader header; if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header)) { //    -   return; } //     connection record'   Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId); //   connection record    ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType)); //        record.State.ReceivePacket(record, header, bytes); } 


For each message transmission, a structure is created containing the connection information. Such a structure is called a connection record .
Some members of the ReliableUdpConnectionRecord class are:
 internal class ReliableUdpConnectionRecord : IDisposable { //     public byte[] IncomingStream { get; set; } //      public ReliableUdpState State { get; set; } // ,   connection record //     public Tuple<EndPoint, Int32> Key { get; private set;} //     public int WindowLowerBound; //    public readonly int WindowSize; //     public int SndNext; //     public int NumberOfPackets; //   (      Tuple) //     public readonly Int32 TransmissionId; //  IP endpoint –    public readonly IPEndPoint RemoteClient; //  ,     IP  //    MTU – (IP.Header + UDP.Header + RelaibleUDP.Header) public readonly int BufferSize; //    public readonly ReliableUdpConnectionControlBlock Tcb; //      BeginSendMessage/EndSendMessage public readonly AsyncResultSendMessage AsyncResult; //     public bool IsNoAnswerNeeded; //     (    ) public int RcvCurrent; //      public int[] LostPackets { get; private set; } //    .   bool. public int IsLastPacketReceived = 0; //... } 


Deeper into the code. States


The states implement the state machine Reliable UDP, in which the main packet processing takes place. The abstract class ReliableUdpState provides an interface for the state:



All the logic of the protocol is implemented by the above classes, together with an auxiliary class that provides static methods, such as, for example, constructing a ReliableUdp header from a connection record.

Further, we will consider in details the implementation of interface methods that define the basic algorithms of the protocol.

DisposeByTimeout method


The DisposeByTimeout method is responsible for releasing connection resources after a timeout and for signaling a successful / unsuccessful message delivery.
ReliableUdpState.DisposeByTimeout:
 protected virtual void DisposeByTimeout(object record) { ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record; if (record.AsyncResult != null) { connectionRecord.AsyncResult.SetAsCompleted(false); } connectionRecord.Dispose(); } 


It is redefined only in the Completed state.
Completed.DisposeByTimeout:
 protected override void DisposeByTimeout(object record) { ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record; //      SetAsCompleted(connectionRecord); } 

ProcessPackets method


The ProcessPackets method is responsible for additional processing of the package or packages. It is called directly or via the packet idle timer.

In the state of Assembling, the method is redefined and is responsible for checking for lost packets and transitioning to the Completed state if the last packet was received and successful
Assembling.ProcessPackets:
 public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) { if (connectionRecord.IsDone != 0) return; if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0)) { //   ,     foreach (int seqNum in connectionRecord.LostPackets) { if (seqNum != 0) { ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum); } } //     ,     if (!connectionRecord.TimerSecondTry) { connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; } //      WaitForPacketTimer //     -     StartCloseWaitTimer(connectionRecord); } else if (connectionRecord.IsLastPacketReceived != 0) //   { //       ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.State = connectionRecord.Tcb.States.Completed; connectionRecord.State.ProcessPackets(connectionRecord); //     //  ,  ,  //   ack         . //    -   //   Completed    StartCloseWaitTimer(connectionRecord); } //  ,  ack      else { if (!connectionRecord.TimerSecondTry) { ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; } //     StartCloseWaitTimer(connectionRecord); } } 

In the SendingCycle state , this method is called only by timer, and is responsible for re-sending the last message, as well as for turning on the connection closure timer.
SendingCycle.ProcessPackets:
 public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) { if (connectionRecord.IsDone != 0) return; //     // (     -   ,     ) ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1)); //   CloseWait –        StartCloseWaitTimer(connectionRecord); } 

In the Completed state, the method stops the working timer and passes the message to the subscribers.
Completed.ProcessPackets:
 public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) { if (connectionRecord.WaitForPacketsTimer != null) connectionRecord.WaitForPacketsTimer.Dispose(); //       ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord); } 

ReceivePacket method


In the FirstPacketReceived state , the main task of the method is to determine whether the first message packet actually came to the interface, and also collect a message consisting of a single packet.
FirstPacketReceived.ReceivePacket:
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket)) //   return; //    - FirstPacket  LastPacket -       if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) & header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket)) { ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length)); if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk)) { //    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); } SetAsCompleted(connectionRecord); return; } // by design  packet numbers   0; if (header.PacketNumber != 0) return; ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header); ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload); //  - ,    connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize)); //      (0) connectionRecord.RcvCurrent = header.PacketNumber; //      1 connectionRecord.WindowLowerBound++; //   connectionRecord.State = connectionRecord.Tcb.States.Assembling; //      //       if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk)) { connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1); } else { ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); } } 

In the SendingCycle state , this method is redefined to receive delivery acknowledgments and retransmission requests.
SendingCycle.ReceivePacket:
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { if (connectionRecord.IsDone != 0) return; if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket)) return; //     //    + 1,     int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets)); //      if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound) return; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); //    : if (header.PacketNumber == connectionRecord.NumberOfPackets) { //   Interlocked.Increment(ref connectionRecord.IsDone); SetAsCompleted(connectionRecord); return; } //      c  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1)) { //    SendPacket(connectionRecord); } //       else if (header.PacketNumber == windowHighestBound) { //   / connectionRecord.WindowLowerBound += connectionRecord.WindowSize; //     connectionRecord.WindowControlArray.Nullify(); //    SendPacket(connectionRecord); } //      –    else ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber)); } 

In the Assembling state, in the ReceivePacket method, the main work is done on assembling messages from incoming packets.
Assembling.ReceivePacket:
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { if (connectionRecord.IsDone != 0) return; //        if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk)) { //   connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1); //   ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload); //       -   if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket)) { connectionRecord.State = connectionRecord.Tcb.States.Completed; connectionRecord.State.ProcessPackets(connectionRecord); } return; } //     int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1)); //       if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound)) return; //   if (connectionRecord.WindowControlArray.Contains(header.PacketNumber)) return; //   ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload); //    connectionRecord.PacketCounter++; //         connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber; //     if (header.PacketNumber > connectionRecord.RcvCurrent) connectionRecord.RcvCurrent = header.PacketNumber; //   connectionRecord.TimerSecondTry = false; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); //     if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket)) { Interlocked.Increment(ref connectionRecord.IsLastPacketReceived); } //      ,    //     else if (connectionRecord.PacketCounter == connectionRecord.WindowSize) { //  . connectionRecord.PacketCounter = 0; //    connectionRecord.WindowLowerBound += connectionRecord.WindowSize; //     connectionRecord.WindowControlArray.Nullify(); ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); } //      if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0) { //   ProcessPackets(connectionRecord); } } 

In the Completed state, the only task of the method is to send another confirmation of successful delivery of the message.
Completed.ReceivePacket:
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { //        , //   ack     if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket)) { ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); } } 

SendPacket method


In the FirstPacketSending state , this method sends the first data packet, or, if the message does not require delivery confirmation, the entire message.
FirstPacketSending.SendPacket:
 public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) { connectionRecord.PacketCounter = 0; connectionRecord.SndNext = 0; connectionRecord.WindowLowerBound = 0; //     -    //    if (connectionRecord.IsNoAnswerNeeded) { //    As Is do { ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord))); connectionRecord.SndNext++; } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets); SetAsCompleted(connectionRecord); return; } //       ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord); ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header)); //   connectionRecord.SndNext++; //   connectionRecord.WindowLowerBound++; connectionRecord.State = connectionRecord.Tcb.States.SendingCycle; //   connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); } 

In the SendingCycle state, this method sends a block of packets.
SendingCycle.SendPacket:
 public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) { //    for (connectionRecord.PacketCounter = 0; connectionRecord.PacketCounter < connectionRecord.WindowSize && connectionRecord.SndNext < connectionRecord.NumberOfPackets; connectionRecord.PacketCounter++) { ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord); ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header)); connectionRecord.SndNext++; } //     ,     connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 ); if ( connectionRecord.CloseWaitTimer != null ) { connectionRecord.CloseWaitTimer.Change( -1, -1 ); } } 

Deeper into the code. Creating and making connections


Now that we have learned about the basic states and methods used to process states, we can analyze a few more examples of the operation of the protocol.
Data transmission diagram in normal conditions:


Let us consider in detail the creation of a connection record for connecting and sending the first packet. The initiator of the transfer is always the application that calls the API method to send the message. Next, the StartTransmission method of the transmission control block is activated, which starts the data transfer for the new message.
Create outbound connection:
 private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult) { if (m_isListenerStarted == 0) { if (this.LocalEndpoint == null) { throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" ); } //     StartListener(LocalEndpoint); } //    ,   EndPoint  ReliableUdpHeader.TransmissionId byte[] transmissionId = new byte[4]; //    transmissionId m_randomCrypto.GetBytes(transmissionId); Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0)); //       , //         if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult))) { //   –      m_randomCrypto.GetBytes(transmissionId); key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0)); if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult))) //     –   throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary"); } //     m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]); } 

Sending the first packet (FirstPacketSending status):
 public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) { connectionRecord.PacketCounter = 0; connectionRecord.SndNext = 0; connectionRecord.WindowLowerBound = 0; // ... //       ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord); ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header)); //   connectionRecord.SndNext++; //   connectionRecord.WindowLowerBound++; //    SendingCycle connectionRecord.State = connectionRecord.Tcb.States.SendingCycle; //   connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); } 

After sending the first packet, the sender enters the SendingCycle state — wait for confirmation of the packet delivery.
The receiving side, using the EndReceive method, accepts the sent packet, creates a new connection connection record and sends the packet, with a pre-parsed header, to the FirstPacketReceived state's ReceivePacket method for processing
Creating a connection on the receiving side:
 private void EndReceive(IAsyncResult ar) { // ... //   //    ReliableUdpHeader header; if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header)) { //    -   return; } //     connection record'   Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId); //   connection record    ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType)); //        record.State.ReceivePacket(record, header, bytes); } 

Receiving the first packet and sending a confirmation (FirstPacketReceived state):
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket)) //   return; // ... // by design  packet numbers   0; if (header.PacketNumber != 0) return; //       ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header); //      ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload); //  - ,    connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize)); //      (0) connectionRecord.RcvCurrent = header.PacketNumber; //      1 connectionRecord.WindowLowerBound++; //   connectionRecord.State = connectionRecord.Tcb.States.Assembling; if (/*    */) // ... else { //   ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); } } 

Deeper into the code. Closing the connection by timeout


Working out timeouts is an important part of Reliable UDP. Consider an example in which a failure occurred at the intermediate node and the delivery of data in both directions became impossible.
Connection closure diagram on timeout:


As can be seen from the diagram, the working timer at the sender is activated immediately after sending a block of packets. This happens in the SendPacket method of the SendingCycle state.
Enable the work timer (SendingCycle state):
 public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) { //    // ... //     connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 ); if ( connectionRecord.CloseWaitTimer != null ) connectionRecord.CloseWaitTimer.Change( -1, -1 ); } 

Timer periods are set when creating a connection. By default, ShortTimerPeriod is 5 seconds. In the example, it is set to 1.5 seconds.

For an incoming connection, the timer starts after receiving the last received data packet; this happens in the state's ReceivePacket method of Assembling
Enable the work timer (Assembling state):
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { // ... //   connectionRecord.TimerSecondTry = false; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); // ... } 

In the incoming connection, more packets did not arrive during the waiting time of the working timer. The timer worked and called the ProcessPackets method, in which the lost packets were found and re-delivery requests were sent the first time.
Sending re-delivery requests (Assembling status):
 public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) { // ... if (/*    */) { //      //     ,     if (!connectionRecord.TimerSecondTry) { connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; } //      WaitForPacketTimer //     -     StartCloseWaitTimer(connectionRecord); } else if (/*      */) { // ... StartCloseWaitTimer(connectionRecord); } //  ack      else { if (!connectionRecord.TimerSecondTry) { //   ack connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; } //     StartCloseWaitTimer(connectionRecord); } } 

The variable TimerSecondTry is set to true . This variable is responsible for restarting the working timer.

The sender also triggers a working timer and resends the last packet sent.
Enable the connection closure timer (SendingCycle state):
 public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) { // ... //     // ... //   CloseWait –        StartCloseWaitTimer(connectionRecord); } 

After that, the outgoing connection starts the timer to close the connection.
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); } 

The timeout period for the connection closure timer is 30 seconds by default.

After a short time, the working timer on the receiver side re-activates, requests are sent again, after which the connection closure timer is started at the incoming connection

. The sender reports a failed delivery to the parent application ( see Reliable UDP API ).
Releasing connection record'a resources:
 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); } } 


Deeper into the code. Data transfer recovery


Data Recovery Recovery Diagram with Packet Loss:


As already discussed in closing the connection with a timeout, upon expiration of the working timer, the recipient will be checked for lost packets. In the case of packet loss, a list of the number of packets that have not reached the recipient will be compiled. These numbers are entered into the LostPackets array of the specific connection and re-delivery requests are sent.
Sending requests for re-delivery of packages (Assembling state):
 public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) { //... if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0)) { //   ,     foreach (int seqNum in connectionRecord.LostPackets) { if (seqNum != 0) { ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum); } } // ... } } 

The sender will accept the request for re-delivery and send the missing packets. It is worth noting that at this moment the sender has already started the closure of the connection and, upon receipt of the request, it is reset.
Resending lost packets (SendingCycle state):
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { // ... connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); //     if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); // ... //      –    else ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber)); } 

The retransmitted packet (packet # 3 in the diagram) is received by the incoming connection. A check is performed to fill the receive window and normal data transfer is restored.
Check for getting into the reception window (Assembling state):
 public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload) { // ... //    connectionRecord.PacketCounter++; //         connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber; //     if (header.PacketNumber > connectionRecord.RcvCurrent) connectionRecord.RcvCurrent = header.PacketNumber; //   connectionRecord.TimerSecondTry = false; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); // ... //      ,    //     else if (connectionRecord.PacketCounter == connectionRecord.WindowSize) { //  . connectionRecord.PacketCounter = 0; //    connectionRecord.WindowLowerBound += connectionRecord.WindowSize; //     connectionRecord.WindowControlArray.Nullify(); ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); } // ... } 

Reliable UDP API


To interact with the data transfer protocol, there is an open class Reliable Udp, which is a wrapper over the transmission control block. Here are the most important members of the class:
 public sealed class ReliableUdp : IDisposable { //     public IPEndPoint LocalEndpoint //   ReliableUdp   //      IP  //  .  0     //    public ReliableUdp(IPAddress localAddress, int port = 0) //      public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null) //     public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject) //    // :   XP  Server 2003  , ..  .NET Framework 4.0 public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken) //     public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state) //     public bool EndSendMessage(IAsyncResult asyncResult) //   public void Dispose() } 

Receiving a message is by subscription. Delegate signature for callback method:
 public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient ); 

Message:
 public class ReliableUdpMessage { //  ,   public ReliableUdpMessageTypes Type { get; private set; } //   public byte[] Body { get; private set; } //    true –      //     public bool NoAsk { get; private set; } } 

To subscribe to a specific type of message and / or to a specific sender, two optional parameters are used: ReliableUdpMessageTypes messageType and IPEndPoint ipEndPoint.

Types of messages:
 public enum ReliableUdpMessageTypes : short { //  Any = 0, //   STUN server StunRequest = 1, //   STUN server StunResponse = 2, //   FileTransfer =3, // ... } 


The message is sent asynchronous; for this, the protocol implements an asynchronous programming model :
 public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state) 

The result of sending a message will be true - if the message successfully reached the recipient and false - if the connection was closed due to a timeout:
 public bool EndSendMessage(IAsyncResult asyncResult) 


Conclusion


Much has not been described in this article. Thread matching mechanisms, exception and error handling, implementation of asynchronous message sending methods. But the core of the protocol, the description of the packet processing logic, the setting up of the connection, and the processing of timeouts must be clarified for you.

The demonstrated version of the reliable delivery protocol is fairly stable and flexible, and meets previously defined requirements. But I want to add that the described implementation can be improved. For example, to increase bandwidth and dynamically change the periods of timers in the protocol, you can add mechanisms such as sliding window and RTT , the implementation of the MTU detection mechanism would also be usefulbetween the nodes of the connection (but only in the case of sending large messages).

Thank you for your attention, waiting for your comments and comments.

PS For those who are interested in details or just want to test the protocol, the link to the project on GitHube:
Project Reliable UDP

Useful links and articles


  1. TCP protocol specification: in English and in Russian
  2. UDP protocol specification: in English and in Russian
  3. RUDP protocol discussion: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 and rfc 1151
  5. Simple implementation of confirming delivery via UDP: Take Total Control Of Your Networking With .NET And UDP
  6. An article describing the mechanisms to overcome NATs: Peer-to-Peer Communication Across Network Address Translators
  7. Implementing an Asynchronous Programming Model : Implementing the CLR Asynchronous Programming Model and the IAsyncResult design pattern
  8. Transferring asynchronous programming model in an asynchronous pattern, based on tasks (APM in the TAP):
    the TPL and Traditional .NET Asynchronous Programming
    of Interop with Other Asynchronous Patterns and the Types


Update: Thanks to mayorovp and sidristij for the idea of ​​adding a task to the interface. Compatibility of the library with the old OS is not broken, because The 4th framework supports both XP and 2003 server.

Source: https://habr.com/ru/post/250227/


All Articles