Radon is a C++ socket wrapper and accompanying protocol. Radon ensures packets are delivered once, reliably, but allows packets to be received out-of-order.
In real-time networked simulations, it is desirable that packets containing simulation state are sent reliably (i.e. retransmitted if not acknowledged). Although retransmission is possible, ideally, each packet should only be delivered to the caller once.
TCP accomplishes both of the above. However, since TCP is stream-oriented, it ensures packets are delivered to the receiver in the same order that that they were generated by the sender. In network simulations, this can cause undue latency, and packets which have already been received must wait until all packets with lower sequence numbers have also been received.
UDP doesn't have this latency issue; however, it also does not handle packet retransmission.
Radon handles sequencing your packets, automatically acknolwedging them, and retransmitting packets that appear to have been lost.
#include <rn/endpoint.h>
#include <rn/socket.h>
// ...
void my_init_socket(QUdpSocket *udp)
{
RnSocketOpt opt = { 0 };
opt.Timeout = 100; // In milliseconds
opt.MaxRetransmits = 3;
m_sock = new RnSocket(RnEndpoint::fromQt(udp), opt);
}
// ...
void my_send_data(const std::string &data)
{
m_socket->send((void)data.c_str(), data.size());
}
// ...
void my_update_frame(float dt)
{
RnSocket::update(dt);
RnPacket *packet = nullptr;
while (packet = m_sock->recv()) {
my_process_data(packet->data(), packet->size());
}
}
Radon uses Qt5 as its underlying socket API. In order to build Radon, you must have Qt 5.x set up on your system.
With Qt configured, all you need to build Radon is run
qmake && make
This builds a static library named radon.lib
or libradon.a
,
depending on your host platform.
Radon sockets communicate over UDP. Each UDP packet contains the following structure:
+-------------------+
| Packet Header |
+-------------------+
| |
| Data Buffer |
| |
+-------------------+
The packet header contains a sequence number, acknowledgement information, and the size of the data buffer. The Data Buffer contains user payload data, which is opaque to Radon.
The header is a 16-byte buffer with the following format:
+----------------+
| magic | stream |
+----------------+
| seq |
+----------------+
| ack |
+----------------+
| history |
+----------------+
| |
| payload |
| |
+----------------+
Bytes Field Header Variable
0-1 16-bit Magic Number magic
2-3 16-bit Stream Identifier stream
4-7 32-bit Sequence Number seq
8-11 32-bit ACK Number ack
12-15 32-bit ACK History history
16-? Variable-length payload
The size of the header is fixed at 16 bytes. The rest of the payload is considered to be part of the buffer. As such, the size of the payload returned to the caller upon receipt is simply the size of the UDP packet's payload minus the size of the header.
Each packet contains a sliding window containing the acknowledgement
history for the last 32 packets.
Each bit in the history
buffer represents a sequence number.
The bit is '0' if no packet with that sequence number has been received,
and is '1' if a packet with that sequence number has been received.
The least-significant bit contains represents the sequence number
contained in the ack
field, and each subsequent higher-order bit
represents the subsequent sequence number.
The header format is almost unmodified from this Gaffer on Games post.
When a packet is sent, the sender generates a unique sequence number for the packet and sends it to the receiver. Upon doing so, the sender adds the packet to a retransmission buffer. Each item in the retransmit buffer has the following fields:
- The packet data to resend if lost
- The remaining timeout until resend is needed
- The number of times the packet has been sent
The caller is responsible for calling RnSocket::update()
once per
frame of the simulation.
In update()
, Radon goes through each item in the retransmission buffer
and decrements its remaining timeout until retransmission.
If, upon decrementing, the timeout is zero or negative, Radon resends the packet and increments the number of times the packet has been sent. If the packet has now been sent the maximun number of retries, Radon removes the packet from the retransmission buffer and forgets about it. Otherwise, Radon resets the packet's timeout to the full timeout value.
When the sender receives a packet which acknowledges a packet in the retransmission buffer, it removes that packet from the retransmission buffer and forgets about it.
The socket tracks its own copies the ack
and history
fields from the
packet header format.
When a packet is received, its sequence number is inspected.
If the sequence number comes before the sliding window, Radon drops the packet due to its age. Otherwise, Radon checks the ack history to determine if the packet has already been received. If it has been received, Radon drops the packet as a duplicate. If the packet has not been received, Radon records its receipt in the ACK history (shifting the history forward far enough to contain the new sequence number, if necessary), and delivers the packet to the caller.
Radon uses a 32-bit unsigned integer as its packet sequence number. At ths time, Random explicitly does not handle wraparound, as the target application (networked games / simulations) rarely run long enough to run into wraparound problems.
For example, if a game sends 30 packets per second (which is a little higher than one would naturally expect), the sequence number would wrap around in:
2^32 Sequence numbers
/ 30 Sequence numbers used per second
/ 60 Seconds per minute
/ 60 Minutes per hour
/ 24 Hours per day
/ 365 Days per year
= ~4.5 Years before a sequence number wraparound