Crate uflow

source ·
Expand description

uflow is a non-blocking, connection-based layer over UDP that provides an ordered and drop-tolerant packet streaming interface for real-time applications (e.g. games). It manages connection state, packet sequencing, packet fragmentation, reliable delivery, and congestion control to create a simple and robust solution for low-latency internet communication.

Hosting a Server

A uflow server is created by calling Server::bind[...](), which opens a UDP socket bound to the specified address, and returns a corresponding Server object. The number of active connections will be restricted to the configured limit, and each incoming connection will be initialized using the given endpoint configuration (see: server::Config).

let server_address = "127.0.0.1:8888";
let config = uflow::server::Config {
    max_active_connections: 8,
    .. Default::default()
};

// Create a server object
let mut server = uflow::server::Server::bind(server_address, config)
    .expect("Failed to bind/configure socket");

As a non-blocking interface, a server object depends on periodic calls to Server::step() to process inbound traffic and update connection states. To signal pending events to the application, step() returns an iterator to a list of server::Event objects which contain information specific to each event type.

Once a client handshake has been completed, a RemoteClient object will be created to represent the new connection. These objects may be obtained by calling Server::client() with the appropriate address. Because RemoteClient does not store user data, it is expected that the application will store any necessary per-client data in a separate data structure.

A RemoteClient functions as a handle for a given connection, and allows the server application to send packets and query various connection details. However, no packets will be placed on the network, and no received packets will be processed until the next call to step(). The application may call Server::flush() to send outbound data immediately.

A basic server loop that extends the above example is shown below:

loop {
    // Process inbound UDP frames and handle events
    for event in server.step() {
        match event {
            uflow::server::Event::Connect(client_address) => {
                // TODO: Handle client connection
            }
            uflow::server::Event::Disconnect(client_address) => {
                // TODO: Handle client disconnection
            }
            uflow::server::Event::Error(client_address, error) => {
                // TODO: Handle connection error
            }
            uflow::server::Event::Receive(client_address, packet_data) => {
                // Echo the packet on channel 0
                let mut client = server.client(&client_address).unwrap().borrow_mut();
                client.send(packet_data, 0, uflow::SendMode::Unreliable);
            }
        }
    }

    // Send data, update server application state
    // ...

    // Flush outbound data
    server.flush();

    // Sleep for 30ms (≈33 updates/second)
    std::thread::sleep(std::time::Duration::from_millis(30));
}

See the echo_server example for a complete server implementation.

Connecting to a Server

A uflow client is created by calling Client::connect(), which opens a non-blocking UDP socket and returns a corresponding Client object. A connection will be initiated immediately using the provided destination address and configuration (see client::Config).

let server_address = "127.0.0.1:8888";
let config = Default::default();

// Create a client object
let mut client = uflow::client::Client::connect(server_address, config)
    .expect("Invalid address");

Like a server, a client depends on periodic calls to Client::step() in order to process inbound traffic and update its connection state. A basic client loop which extends the above example is shown below:

loop {
    // Process inbound UDP frames
    for event in client.step() {
        match event {
            uflow::client::Event::Connect => {
                // TODO: Handle connection
            }
            uflow::client::Event::Disconnect => {
                // TODO: Handle disconnection
            }
            uflow::client::Event::Error(error) => {
                // TODO: Handle connection error
            }
            uflow::client::Event::Receive(packet_data) => {
                // TODO: Handle received packets
            }
        }
    }

    // Send data, update client application state
    // ...

    // Flush outbound data
    client.flush();

    // Sleep for 30ms (≈33 updates/second)
    std::thread::sleep(std::time::Duration::from_millis(30));
}

See the echo_client example for a complete client implementation.

Sending Packets

Packets are sent to a remote host by calling client::Client::send() or server::RemoteClient::send(), which additionally requires a channel ID and a packet send mode. Any packets that are sent prior to establishing a connection will be sent once the connection succeeds.

let server_address = "127.0.0.1:8888";
let config = Default::default();
let mut client = uflow::client::Client::connect(server_address, config).unwrap();

let packet_data = "Hello world!".as_bytes();
let channel_id = 0;
let send_mode = uflow::SendMode::Reliable;

client.send(packet_data.into(), channel_id, send_mode);

Additional details relating to how packets are sent and received by uflow are described in the following subsections.

Packet Fragmentation and Aggregation

Small packets are aggregated into larger UDP frames, and large packets are divided into fragments such that no frame exceeds the internet MTU (1500 bytes). Each fragment is transferred with the same send mode as its containing packet—that is, fragments will be resent if and only if the containing packet is marked with SendMode::Persistent or SendMode::Reliable. A packet is considered received once all of its constituent fragments have been received.

Channels

Each connection contains 64 virtual channels that are used to ensure relative packet ordering: packets that are received on a given channel will be delivered to the receiving application in the order they were sent. Packets which have not yet been received may be skipped, depending on the send mode of the particular packet, and whether or not any subsequent packets have been received.

Because packets that are sent using SendMode::Reliable may not be skipped, and because all packets on a given channel must be delivered in-order, the receiving application will not see a given received packet until all previous reliable packets on the same channel have also been received. This means that if a reliable packet is dropped, that channel will effectively stall for its arrival, but packets received on other channels may still be delivered in the meantime.

Thus, by carefully choosing the send mode and channel of outgoing packets, the latency effects of intermittent network losses can be mitigated. Because uflow does not store packets by channel, and otherwise never iterates over the space of channel IDs, there is no penalty to using a large number of channels.

Packet Buffering

All packets are sent subject to adaptive rate control, a maximum transfer window, and a memory limit set by the receiving host. If any of these mechanisms prevent a packet from being sent, the packet will remain in a queue at the sender. Thus, a sender can expect that packets will begin to accumulate in its queue if the connection bandwidth is low, or if the receiver is not processing packets quickly enough.

The total size of all packets awaiting delivery can be obtained by calling Client::send_buffer_size() or RemoteClient::send_buffer_size(), and if desired, an application can use this value to terminate excessively delayed connections. In addition, the application may send packets using SendMode::TimeSensitive to drop packets at the sender if they could not be sent immediately (i.e. during the next call to step()). In the event that the total available bandwidth is limited, this prevents outdated packets from using any unnecessary bandwidth, and prioritizes sending newer packets in the send queue.

Receiving Packets (and Other Events)

Each time step() is called on a Client or Server object, connection events are returned via iterator. Because servers may have multiple connections, server events each contain an associated client address, whereas client events do not. See client::Event and server::Event for more details.

For client and server, the overall connection-event behavior is as follows. A Connect event will be generated when a connection is first established. If either end of the connection explicitly disconnects, a Disconnect event will be generated. Once a packet has been received (and that packet is not waiting for any previous packets), a Receive event will be generated. If an error is encountered, or the connection times out at any point, an Error event will be generated. No further events are generated after a Disconnect or an Error event.

Maximum Receive Allocation

If a sender is sending a continuous stream of packets, but step() is not called on the receiver for whatever reason, the number of packets in the receiver’s receive buffer will increase until its maximum receive allocation has been reached. At that point, any new packets will be silently ignored.

Note: This feature is intended to prevent memory allocation attacks. A well-behaved sender will ensure that it does not send new packets which would exceed the receiver’s memory limit, and the stall will back-propagate accordingly.

Optimal Acknowledgements

If desired, an application may call flush() on the associated client or server object immediately after all events from step() have been handled. By doing so, information relating to which packets have been delivered (and how much buffer space is available) will be relayed back to the sender as soon as possible.

Disconnecting

A connection is explicitly closed by calling disconnect() or disconnect_now() on the corresponding Client or RemoteClient object; disconnect() will make sure to send all pending outbound packets prior to disconnecting, whereas disconnect_now() will initiate the disconnection process on the next call to step(). In both cases the application must continue to call step() to ensure that the disconnection takes place.

client.disconnect();

// ... calls to client.step() continue

Servers may also call Server::drop(), which sends no further packets and forgets the connection immediately. This will generate a timeout error on the client.

Modules

Client-related connection objects and parameters.
Server-related connection objects and parameters.

Structs

Parameters used to configure either endpoint of a uflow connection.

Enums

A mode by which a packet is sent.

Constants

The maximum number of channels which may be used on a given connection.
The common maximum transfer unit (MTU) of the internet.
The maximum size of a packet fragment in bytes, according to frame serialization overhead.
The maximum size of a uflow frame in bytes, according to the internet MTU and UDP header size.
The maximum size of the frame transfer window, in sequence IDs.
The absolute maximum size of a packet, in bytes.
The maximum size of the packet transfer window, in sequence IDs.
The current protocol version ID.
The number of bytes in a UDP header (including the IP header).