Systems · Lesson 26.7

UDP

Source
Send datagrams between two UdpSockets on the loopback address: SendTo and ReceiveFrom with an address on every datagram, a reply to the sender, message boundaries that survive, a connected pair that uses Send and Receive, and a read timeout for an answer that never comes.
You'll need: IP addresses, TCP, Duration, Guard

UDP is TCP's plainer sibling. There is no connection and no stream: a program sends a datagram — one self-contained message, up to about 64 KB — to an address, and the other side receives it whole, or not at all. Nothing is resent if it is lost, and nothing keeps datagrams in order.

That sounds worse, and for a file transfer it is. But each message costs one packet and no handshake, and a late answer is often worth nothing anyway: games, voice and video calls, clock synchronisation and DNS look-ups all use UDP.

TCPUDP
Setting upBind and Accept, or ConnectBind, and send straight away
What you senda stream of bytesseparate datagrams
Boundariesnone — you frame messageskept — one send is one receive
Lost dataresent automaticallylost
Orderkeptnot promised
import Net::{ IpAddress, Ipv4Address, SocketAddress, UdpSocket };

Sockets that send and receive

A UdpSocket is bound to an address, like a TCP listener, and can then both send and receive. There is no server or client in the type — only two sockets that know each other's addresses:

let loopback = SocketAddress::New(IpAddress::FromIpv4(Ipv4Address::Localhost()), 0);
let alice = UdpSocket::Bind(loopback)?;
let bob = UdpSocket::Bind(loopback)?;
let aliceAddress = alice.LocalAddress()?;
let bobAddress = bob.LocalAddress()?;

Both live in this one program, and one thread is enough: a datagram sent to a socket on the same machine waits in that socket's queue until it is received.

Sending to, receiving from

SendTo names where each datagram goes. ReceiveFrom waits for the next one, copies it into a buffer and returns a Datagram: its length, and its source — the address to reply to:

alice.SendTo("ping", bobAddress)?;
var buffer: char8[64];
let received = bob.ReceiveFrom(buffer[..])?;
PrintLine("bob got [{}] from alice? {}", buffer[..received.length],
    received.source.Equals(aliceAddress));
bob.SendTo("pong", received.source)?;

Replying to source is how a UDP server works: it never connects to anyone, it answers whoever asked. Make the buffer big enough for the largest datagram you expect. One that does not fit is lost either way, but not in the same way everywhere: on Linux and macOS the receive returns the part that fitted, while on Windows it fails with an IoError of kind Other.

Boundaries survive

Two sends are two datagrams, and each receive takes exactly one — the opposite of TCP, where the two would run together into one stream:

alice.SendTo("first", bobAddress)?;
alice.SendTo("second", bobAddress)?;
for _ in 0..2 {
    let datagram = bob.ReceiveFrom(buffer[..])?;
    PrintLine("bob got [{}], {} bytes", buffer[..datagram.length], datagram.length);
}

A connected pair

Connect on a UDP socket sends nothing. It fixes the one peer the socket talks to, so that Send and Receive need no address, and datagrams from anyone else are dropped:

alice.Connect(bobAddress)?;
bob.Connect(aliceAddress)?;
alice.Send("over the connected pair")?;
let length = bob.Receive(buffer[..])?;

Not waiting forever

Since nothing promises an answer, a receive that waits without a limit can wait for good. SetReadTimeout gives up after a duration, with an IoError of kind TimedOut, which a guard picks out:

bob.SetReadTimeout(Duration::FromMilliseconds(100))?;
match bob.Receive(buffer[..]) {
    .Success(_) => PrintLine("bob got something unexpected"),
    .Failure(error) if error.kind == IoErrorKind::TimedOut =>
        PrintLine("bob waited 100 ms: nothing"),
    .Failure(error) => fail error
}

A program that asks over UDP usually sends again after a timeout, a few times, before giving up.

Names and many sockets

Two more parts of Net round it out. Neither runs here, because the lesson stays off the real network.

Names. Real programs connect to names, not numbers. Resolve(allocator, host, port) asks the system's resolver and returns a Vector<SocketAddress> — often an IPv6 and an IPv4 address for the same name — to try in turn:

let addresses = Resolve(allocator, "localhost", 5353)?;

Resolver is the package's own DNS client, which asks name servers of your choosing over UDP, falling back to TCP for a long answer — and is a real example of the send, wait, retry pattern above.

Many sockets at once. A server with hundreds of clients cannot block on each in turn. SetNonBlocking(true) makes a socket's calls fail with WouldBlock instead of waiting, and a Poller watches many sockets: Register each with a token, then Wait reports which ones are ready to read or write.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// UDP is TCP's simpler sibling. There is no connection to set up and no stream: a program sends
// a datagram — one self-contained message of up to about 64 KB — to an address, and the other
// side receives it whole, or not at all. Nothing is resent if it is lost and nothing keeps
// datagrams in order. In exchange each message costs one packet and no handshake, which suits
// games, voice, time and name lookups: small messages where a late answer is worth nothing.
//
// A `UdpSocket` is bound to an address, like a TCP listener, and can then both send and
// receive. `SendTo` names where each datagram goes; `ReceiveFrom` waits for the next one and
// returns a `Datagram`: its length and the address it came from, which is where to reply.
//
// Both sockets live in this one program, on the loopback address. Datagrams sent to a socket on
// the same machine wait in its queue until it receives them, so one thread is enough.
import Io::{ IoError, IoErrorKind, PrintLine };
import Net::{ IpAddress, Ipv4Address, SocketAddress, UdpSocket };
import Time::Duration;

func Main() -> ! IoError {
    // Port 0 again lets the system choose; the ports differ between runs and are not printed.
    let loopback = SocketAddress::New(IpAddress::FromIpv4(Ipv4Address::Localhost()), 0);
    let alice = UdpSocket::Bind(loopback)?;
    let bob = UdpSocket::Bind(loopback)?;
    let aliceAddress = alice.LocalAddress()?;
    let bobAddress = bob.LocalAddress()?;

    // One datagram there, and a reply to whoever sent it.
    alice.SendTo("ping", bobAddress)?;
    var buffer: char8[64];
    let received = bob.ReceiveFrom(buffer[..])?;
    PrintLine("bob got [{}] from alice? {}", buffer[..received.length],
        received.source.Equals(aliceAddress));
    bob.SendTo("pong", received.source)?;
    let answer = alice.ReceiveFrom(buffer[..])?;
    PrintLine("alice got [{}]", buffer[..answer.length]);

    // Each send is its own datagram, and each receive takes exactly one: the boundaries stay.
    alice.SendTo("first", bobAddress)?;
    alice.SendTo("second", bobAddress)?;
    for _ in 0..2 {
        let datagram = bob.ReceiveFrom(buffer[..])?;
        PrintLine("bob got [{}], {} bytes", buffer[..datagram.length], datagram.length);
    }

    // `Connect` fixes the one peer a socket talks to, so `Send` and `Receive` need no address.
    // Nothing is sent: for UDP, connecting only sets a default and filters what arrives.
    alice.Connect(bobAddress)?;
    bob.Connect(aliceAddress)?;
    alice.Send("over the connected pair")?;
    let length = bob.Receive(buffer[..])?;
    PrintLine("bob got [{}]", buffer[..length]);

    // Nobody promises an answer, so a receive that must not wait forever gets a timeout.
    bob.SetReadTimeout(Duration::FromMilliseconds(100))?;
    match bob.Receive(buffer[..]) {
        .Success(_) => PrintLine("bob got something unexpected"),
        .Failure(error) if error.kind == IoErrorKind::TimedOut =>
            PrintLine("bob waited 100 ms: nothing"),
        .Failure(error) => fail error
    }

    // A socket owns what the system gave it, so it is moved, never copied:
    //
    //     let carol = alice;
    //         error: move-only value 'alice' requires an explicit '<-' in initialization
}

Besides Io, its Rux.toml lists Net and Time under [Dependencies].

Run it

cd Examples/Systems/Udp
rux run
bob got [ping] from alice? true
alice got [pong]
bob got [first], 5 bytes
bob got [second], 6 bytes
bob got [over the connected pair]
bob waited 100 ms: nothing

Common mistakes

Try it yourself

  1. Make Bob reply to every datagram with its length, and have Alice print the replies.
  2. Bind a third socket, Carol, and have her send to Bob after Bob has connected to Alice. Does Bob receive it? Use a read timeout so the program cannot hang.
  3. Send a 20-byte datagram and receive it into an 8-byte buffer. What comes back on your system — a short datagram or a failure?
  4. Write a tiny request–reply loop: Alice sends a question, waits up to 50 milliseconds, and tries again up to three times before giving up.

Learn more

  • Net API — UdpSocket, Resolve, Resolver and Poller
  • TCP — the connected, reliable alternative
  • IP addresses — the addresses on every datagram
  • Duration — the timeout's type
  • Checkpoint: Grep, a command-line tool that puts this part to work