RUDP Written In C# For Diarkis
Before We Start
RUDP stands for Reliable UDP (User Datagram Protocol) is used to transport UDP messages. UDP by design is an unreliable network protocol. It means that your messages may get “lost”.
RUDP adds an extra feature to UDP so that your messages will not get “lost” and reliably delivered to the intended destination. For more detailed information on RUDP, please read some technical articles on RUDP as we will be focusing on our implementation of RUDP for Diarks
Diarkis is our real-time network engine and it implements its own version of RUDP along with standard network protocols such as UDP, TCP, and HTTP.
RUDP Implemented For Diarkis
Our version of RUDP implements the following: 1. Reliable transportation by having a retry mechanism. 2. Guaranteeing of packet order. 3.Automatic packet splitting and reconstruction on reception for large size packets.
Diarkis’ RUDP also allows you to mix with plain UDP. You may choose to send a message as either UDP or RUDP. Each message packet has a header that indicates the mode of transport (UDP or RUDP).
The header flag has the following:
- Plain UDP message
- RUDP initialization packet
- RUDP data packet
- RUDP acknowledge packet
- RUDP retry data packet
- RUDP retry acknowledge packet
1.Reliable Transportation With a Retry Mechanism
The sender of packets expects an acknowledgment packet back from the recipient for every packet received. Sent packets without acknowledgment packets from the recipient will be re-sent to the recipient after a set time. This set time will increase as you retry more in order to prevent “spamming” the recipient. This means retries will be slower as it retries more.
When multiple retries for the same packet fail, the sender considers the recipient to be “gone” and terminates the communication.
2. Guaranteeing Packet Order
UDP itself does not maintain the order of packets sent and received. Our RUDP adds a feature to maintain the order of packets being sent and received. This is behaviour very similar to TCP. If the previous packet is lost and resent, the next packet will not be processed until the first packet is received and handled.
3. Automatic Packet Splitting and Reconstruction on Reception
When sending a large packet (over 1,400bytes), Diarkis’ RUDP splits the packet into multiple separate packets and sends them. The recipient then reconstructs the split-packets into the original packet. This is useful in many circumstances. Most cloud services (GCP, AWS, Azure, etc.) limit (MTU – Maximum Transmission Unit) the size of a UDP packet to less then 1,400bytes (Not exactly 1,400bytes though). Packets that exceed the MTU are forcefully dropped. Many commercial routers also split large size packets and this leads to a higher chance of packet loss because if one split packet is lost, the rest will be lost also.
Technical Details on Implementation
Diarkis’ RUDP client spawns 2 threads. One thread is responsible for sending packets, and the other is responsible for receiving the packets. The main thread will send the packet to be sent into the send buffer for the sender thread to pick up and send. The recipient buffer will be stacking the received packets to be popped in the order of the packets sent. Reordering of packets and reconstruction of split packets happen in the recipient thread, but the actual processing of the received packets happens only in the main thread. Likewise, the main thread never actually sends packets, but only sends them to the buffer for the sender thread.
Why Two Extra Threads?
When you are waiting for packets to come in, you must block and wait. If we wait in the main thread, your entire application halts. We do not want that to happen, so we have a dedicated thread just to wait for incoming packets and processing them. The reason for having another thread for sending packets is very similar to having the recipient thread. Because the recipient thread blocks and waits, we cannot have the sending mechanism in the same thread. Otherwise, we will have to wait until we have some new packets coming in to send our own packets. Sending packets is a blocking operation, so we do not want to have that in the main thread either and that leads to us having a dedicated thread for sending packets.
Why Not Have Previous Packets Combined With Your Current Packet To Avoid Having To Retry?
Diarkis intentionally avoids this method of implementation because we wanted to be able to handle large size packets (packets that exceed MTU) also. If we were to combine previously sent packets with each sent packet, the size of the packet will grow, but we may have the size limitation from our cloud service and such. What will happen if we tried to send a large packet with this system? we will likely be combining the split packets together, but these “combined” split packets may be lost. If we do not have the retry mechanism like our Diarkis, those lost split packets will be forever lost making this RUDP not so reliable.
Writing Packet Reception Using Socket.Poll in C#
C# Socket class offers a Poll method that blocks and waits for incoming UDP messages. We will be using this method instead of writing a loop with Thread.Sleep for better performance (Less CPU consumption while “waiting”). If you are using UdpClient class instead of Socket class directly, you can access the Poll method like this: udpClientInstance.Client.Poll (udpClientInstance.Client is actually an instance of Socket).
Conclusion
Although the definition of RUDP is well defined and standardized, the implementation of RUDP is not. Depends on your own needs, you may choose to implement RUDP in a certain way as Diarkis has its own implementation of RUDP. You may even omit certain features such as packet order management if you do not need it. It is important to design how you want to implement RUDP based on your application’s requirements and what you need to achieve.