Skip to content

Using the UDP Protocol

SmartFoxServer 3 offers a large update over its predecessor with a more sophisticated approach to UDP especially for live audio streaming and fast-paced action games.

We have introduced RDP (Revamped Datagram Protocol), a new and more sophisticated Datagram protocol, built on top of UDP and providing different modalities and quality of service, that can be mixed and matched in your application based on your use case.

RDP offers the three modalities:

  • UDP_RAW: this is the base UDP implementation without any extra processing. Packets can get lost without notice and the order of reception is not guaranteed. This is what we normally refer to as unordered and unreliable

  • UDP_UNRELIABLE: this the unreliable but ordered version where packets that are too old are automatically discarded and you're guaranteed that you will only receive the most recent ones. Still there may be "holes" in the reception due to packet loss

  • UDP_RELIABLE: this is the reliable and ordered version of the protocol that delivers packets in the order they were sent and without "holes" (*)

While the first one is essentially pure UDP without extra cherries on top, let's take a closer look at the other modes:

Unreliable UDP

This is a convenient UDP modality to use when you need to send very fast updates (e.g. 50/60pps) and you don't care particularly about packet loss. What you need is just to receive packets in the right order without older packets (i.e. packets with a lower sequence number) getting in the way.

This kind of approach is used in innumerable FPS and fast-paced action games, as it allows high packet rate updates at the cost of some accuracy loss that can be compensated with client side prediction. We demonstrate this approach in our more advanced tutorials (see the Unity FPS tutorial, the SpaceWar tutorial etc.)

Reliable UDP

This is the classic reliable UDP solution based on ACK (and NACK) packets to handle re-transmissions. However our implementation is aggressively fine tuned towards games and latency reduction, which outperforms most of the solutions we have had the chance to test.

We also complement the protocol with multiple QoS (quality of service) options to fine tune the protocol based on your needs.

While the Reliable version of RDP does guarantee reliability and ordering and it doesn't attempt to rebuild TCP and there are significant differences. Here are some of the most notable that need to be kept in mind:

  • no congestion control: RDP fires packets at the requested rate at all times, never slowing down, even in case of congestion
  • no flow control: while TCP continuously adapts to the other end's state to keep a balanced use of bandwidth, RDP is all about speed and lowest possible latency
  • not 100% reliable: when a TCP packet is lost TCP will wait even for entire minutes for the lost packet slowing down everything to a crawl. This is far from ideal. RDP, on the other hand, uses a max number of configured retransmissions after which it will move on in order not to sacrifice speed.

Takeaway concept: The point of a reliable UDP is not to remake TCP, otherwise it you might be better of using TCP in the first place. The point of RDP is to strive for reliability without sacrificing the game's pace or low latency. After all it's much better to lose a couple of packets and move on with the action than remaining stuck or getting frequent stutters while playing.

Getting started with UDP

The UDP protocol can only be activated after you have successfully logged in SmartFoxServer. Once that's done you call the SmartFox.connectUdp() method to activate a secondary UDP connection.

var sfs = new SmartFox();

// Add event listeners
sfs.AddEventListener(SFSEvent.CONNECTION, OnConnection);
sfs.AddEventListener(SFSEvent.LOGIN, OnLogin);
sfs.AddEventListener(SFSEvent.UDP_CONNECTION, OnUdpConnection);
sfs.AddEventListener(SFSEvent.UDP_CONNECTION_LOST, OnUdpConnectionLost);

// Set connection parameters
ConfigData cfg = new ConfigData();
cfg.Host = "127.0.0.1";
cfg.Zone = "Playground";

// Connect to SmartFoxServer
sfs.Connect(cfg);

private void OnConnection(ApiEvent evt)
{
    bool success = (bool) evt.GetParam(EventParam.Success)!;

    if (success)
        sfs.Send(new LoginRequest());
    else
        Log("Error, connection failed");
}

private void OnLogin(ApiEvent evt)
{
    // Start UDP connection
    sfs.ConnectUDP();
}

private void OnUdpConnection(ApiEvent evt)
{
    bool success = (bool) evt.GetParam(EventParam.Success)!;

    if (success)
        Log("UDP Connection success!");
    else
        Log("UDP Connection failed!");
}

private void OnUdpConnectionLost(ApiEvent evt)
{
    Log("UDP connection was closed");
}

Quality of Service settings

We want to preface this section by emphasizing that this is quite an advanced topic and if you're not too familiar with the concepts discussed here it would probably be best to stick with the default setting until you have good reasons to change them.

Under the AdminTool > Zone Configurator select a Zone and in the Advanced tab you will find a number of UDP related settings:

udp settings

RDP Quality of Service offers the following options:

  • FAST_DELIVERY max 3 rtx attempts over a short period of time, no backoff
  • BEST_EFFORT max 5 rtx attempts over a short period of time, with backoff
  • RESILIENT max 20 rtx attempts over a short period of time, with backoff

The number of rtx (re-transmissions) impacts the performance of the protocol when the network is slow or congested. The default setting is BEST_EFFORT

RDP Retransmission Type offers the following options:

  • ACK_ACTIVE reply with an ACK for every received packet, using an active timer to monitor un-ACKed packets (for rtx)
  • ACK_PASSIVE reply with an ACK for every received packet, using a passive trigger to monitor un-ACKed packets (for rtx)
  • NACK_PASSIVE reply with an NACK only for missing packets and using a packet history of the last 32 missing packets

An "active" system requires a (virtual) thread to actively monitor the queue of un-ACKed packets to trigger re-transmissions. On the other hand a "passive" system is triggered (passively) by packets coming from the client, without extra work.

The advantage of the former is latency reduction at the cost of some extra processing. On the other hand the main difference between ACK and NACK notifications is bandwidth use:

  • ACK must be sent for every packet received to acknowledge its reception, uses a bit more bandwidth
  • NACK must only be sent for packets that were not received, which are usually a low number and therefore uses less bandwidth

Additionally the NACK uses a 32 bit window to signal which of the last 32 packets were not received, in bulk.

The default value is ACK_PASSIVE which strikes a good balance between low latency and performance.

Examples of usage

For every packet sent via Extension from both client and server you can choose which protocol to use, allowing to optimize the communication on a per-request basis.

For example, positional updates might be sent with unreliable UDP for max speed. If a few updates are lost a bit of client side prediction can fix the issue without major visual glitches.

var upd = new SFSObject();
upd.PutFloat("x", currPx);
upd.PutFloat("y", currPy);
upd.PutFloat("z", currPz);
upd.PutVector3("s", currSpeedVec);

sfs.Send(new ExtensionRequest("mu", upd, sfs.LastJoinedRoom, TransportType.UDP_UNRELIABLE));

When we shoot bullets however we want reliability so that bullets are registered by the server and added to the simulation:

var bullet = new SFSObject();
bullet.PutVector3("b", bulletSpeedVec);
bullet.PutByte("t", bulletType);

sfs.Send(new ExtensionRequest("sh", bullet, sfs.LastJoinedRoom, TransportType.UDP_RELIABLE));

And sometimes there may be operations such as a player buying an upgrade for their spaceship, paid with hard earned in-game coins. In this case we don't care about low latency but we want absolute guarantees on the transaction, therefore we just use TCP:

var shipUpdate = new SFSObject();
shipUpdate.PutInt("id", shopItemId);
shipUpdate.PutDouble("pr", itemPrice);
shipUpdate.PutInt("sid", targetShip);

sfs.Send(new ExtensionRequest("shop.buy", shipUpdate, sfs.LastJoinedRoom, TransportType.TCP));