Standard library · Network

net/udp

Sending and receiving UDP packets, with broadcast and multicast.

import net.udp

UDP sends separate packets, with no connection and no guarantee that they arrive or arrive in order; in exchange it is fast and simple. It is what DNS, games, live streaming and discovering devices on a local network use. Up to about 1500 bytes per packet is safe.

A UDP socket is not tied to one peer. The same socket can send to many addresses and receive from anyone, and every Packet carries from_host and from_port so you know where to answer. Each Send is one packet and each Receive returns one whole packet, so unlike TCP there is no need to find where a message ends.

Choose UDP when a late answer is worth nothing (game state, sensor readings, voice) or when one small question gets one small answer. When every byte must arrive, in order, use net/tcp. To look up names, net/dns already speaks the DNS protocol over UDP for you.

What trips people up

  • A timeout looks like an empty packet. Receive returns empty data both when the timeout passed and when a peer really sent zero bytes, so never use empty packets as messages.
  • Too small a buffer cuts the packet. The rest of a packet larger than max is thrown away, not kept for the next call. Pass the largest size your protocol allows (0 means 65536 for Receive).
  • Text stops at a zero byte. Receive gives a str; binary packets need ReceiveBytes.
  • IPv4 only. Sockets, addresses and multicast groups are all IPv4.
  • Nothing is guaranteed. Packets can be lost, repeated or reordered; the retry guide shows the usual answer.

Answering a request

A UDP server binds a port and answers each packet to the address it came from. The client uses Open, which picks a free port for it, sends a packet and waits for the reply on the same socket. Set a timeout on both sides, because a lost packet would otherwise leave Receive waiting forever.

net-udp-guide-ping.salam
import net.udp
import str

mut server := udp.Socket {}

func pong():
    udp.SetTimeout(server, 2000)
    mut answered := 0
    until answered < 3:
        p := udp.Receive(server, 1500)
        if p.data == "":
            break
        end
        answered += 1
        _ := udp.Send(server, p.from_host, p.from_port, "pong " + str.FromInt(answered) + " for " + p.data)
    end
end

func main:
    server = udp.BindOn("127.0.0.1", 0)
    t := spawn(pong)
    client := udp.Open()
    udp.SetTimeout(client, 2000)
    each name in ["alpha", "beta", "gamma"]:
        _ := udp.Send(client, "127.0.0.1", udp.Port(server), name)
        reply := udp.Receive(client, 1500)
        println reply.data
    end
    join(t)
    udp.Close(client)
    udp.Close(server)
end
Outputpong 1 for alpha pong 2 for beta pong 3 for gamma

Binding to 127.0.0.1 with BindOn keeps the server private to this machine. Bind listens on every address, which is what a service on the network needs.

Timeouts and retries

UDP never tells you that a packet was lost, so a client that expects an answer has to wait a short time and send again. With SetTimeout, Receive returns a packet with empty data when nothing arrived in time. The server in this example ignores the first two requests, the way a busy network might drop them, and the client tries until it gets an answer or runs out of attempts.

net-udp-guide-retry.salam
import net.udp

mut server := udp.Socket {}

func flaky():
    udp.SetTimeout(server, 3000)
    mut seen := 0
    until seen < 3:
        p := udp.Receive(server, 1500)
        if p.data == "":
            break
        end
        seen += 1
        if seen == 3:
            _ := udp.Send(server, p.from_host, p.from_port, "got " + p.data)
        end
    end
end

func ask(sock: udp.Socket, port: int, question: str, tries: int): str:
    repeat tries in i:
        _ := udp.Send(sock, "127.0.0.1", port, question)
        reply := udp.Receive(sock, 1500)
        if reply.data != "":
            ret reply.data
        end
        println "attempt", i + 1, "timed out"
    end
    ret ""
end

func main:
    server = udp.BindOn("127.0.0.1", 0)
    t := spawn(flaky)
    client := udp.Open()
    udp.SetTimeout(client, 200)
    answer := ask(client, udp.Port(server), "status?", 5)
    if answer == "":
        println "server unreachable"
    else:
        println "answer:", answer
    end
    join(t)
    udp.Close(client)
    udp.Close(server)
end
Outputattempt 1 timed out attempt 2 timed out answer: got status?

Because a request can arrive twice (the first copy was only slow, not lost), a request that changes something should carry an id, so the server can recognise a repeat and answer it without doing the work again. DNS works exactly like this.

Binary packets

Compact protocols put numbers in bytes instead of text. SendBytes sends a block of memory as one packet and ReceiveBytes fills a buffer and reports the sender through its last two arguments. The packet here has one byte for the kind of message, one for a sensor number and two for a value, high byte first, which is the usual network order.

net-udp-guide-binary.salam
import net.udp
import mem

func main:
    receiver := udp.BindOn("127.0.0.1", 0)
    sender := udp.Open()
    udp.SetTimeout(receiver, 2000)

    out := mem.AllocateZeroed(8 as u64)
    o := out as u8*
    o[0] = 1 as u8
    o[1] = 42 as u8
    o[2] = 1 as u8
    o[3] = 44 as u8
    sent := udp.SendBytes(sender, "127.0.0.1", udp.Port(receiver), out, 4)
    println "sent", sent, "bytes"

    buf := mem.AllocateZeroed(1500 as u64)
    mut host := ""
    mut port := 0
    n := udp.ReceiveBytes(receiver, buf, 1500, host, port)
    b := buf as u8*
    kind := b[0] as int
    sensor := b[1] as int
    value := (b[2] as int) * 256 + (b[3] as int)
    println "received", n, "bytes from", host
    println "kind", kind, "sensor", sensor, "value", value

    mem.Free(out)
    mem.Free(buf)
    udp.Close(sender)
    udp.Close(receiver)
end
Outputsent 4 bytes received 4 bytes from 127.0.0.1 kind 1 sensor 42 value 300

Keep packets below about 1500 bytes. Larger ones are split by the network and lost completely if one part goes missing. Use ReceiveBytes rather than Receive whenever the data may contain a zero byte, since a str ends at the first one.

Finding devices on the local network

To find a printer, a game host or another copy of your program on the same network without knowing its address, send to a group instead of to one machine. With JoinMulticast a socket receives what anyone sends to the group address; SetBroadcast instead allows sending to the broadcast address of the whole network, such as 255.255.255.255. This example depends on how the network is set up, so it is only compiled here. On most machines it prints heard: printer at 192.168.1.50, because the sender also receives its own multicast packets.

net-udp-guide-discovery.salam
import net.udp

func main:
    group := "239.255.42.99"
    port := 9999
    listener := udp.Bind(port)
    if not udp.JoinMulticast(listener, group):
        println "could not join", group
        ret 1
    end
    udp.SetTimeout(listener, 1000)

    announcer := udp.Open()
    _ := udp.Send(announcer, group, port, "printer at 192.168.1.50")

    p := udp.Receive(listener, 1500)
    if p.data == "":
        println "nobody announced anything"
    else:
        println "heard:", p.data
    end
    _ := udp.LeaveMulticast(listener, group)
    udp.Close(announcer)
    udp.Close(listener)
    ret 0
end

Multicast addresses from 239.0.0.0 to 239.255.255.255 are meant for private use inside an organisation. Every program that wants the announcements binds the same port and joins the same group.

Types

Socket

struct Socket
A UDP socket.
NameTypeDescription
fdi64 = -1The socket descriptor.
net-udp-Echo.salam
import net.udp
import mem

func main:
    receiver := udp.BindOn("127.0.0.1", 0)
    sender := udp.Open()
    println udp.Ok(receiver), udp.Ok(sender), udp.Port(receiver) > 0
    udp.SetTimeout(receiver, 2000)
    println udp.Send(sender, "127.0.0.1", udp.Port(receiver), "hello")
    p := udp.Receive(receiver, 1500)
    println p.data, p.from_host, p.from_port > 0
    _ := udp.SendTo(sender, "127.0.0.1", udp.Port(receiver), "two")
    text := "three"
    _ := udp.SendBytes(sender, "127.0.0.1", udp.Port(receiver), text as void*, text.len())
    println udp.Receive(receiver, 1500).data
    buffer := mem.AllocateZeroed(64 as u64)
    mut host := ""
    mut port := 0
    println udp.ReceiveBytes(receiver, buffer, 64, host, port), host
    udp.SetTimeout(receiver, 100)
    println "[" + udp.Receive(receiver, 1500).data + "]"
    udp.SetBroadcast(sender, true)
    println udp.JoinMulticast(receiver, "239.1.2.3") or true, udp.LeaveMulticast(receiver, "239.1.2.3") or true
    other := udp.Bind(0)
    println udp.Ok(other)
    udp.Close(other)
    udp.Close(receiver)
    udp.Close(sender)
    mem.Free(buffer)
end
Outputtrue true true 5 hello 127.0.0.1 true two 5 127.0.0.1 [] true true true

Packet

struct Packet
One received packet.
NameTypeDescription
datastr = ""The contents; empty when nothing arrived within the timeout.
from_hoststr = ""The address of the sender.
from_portint = 0The port of the sender.

The example for this item is shown with the ones above: net-udp-Echo.salam

Functions

Bind

func Bind(port: int): Socket
Makes a socket that listens on the given port on every address; zero means a free port.

The example for this item is shown with the ones above: net-udp-Echo.salam

BindOn

func BindOn(host: str, port: int): Socket
Like Bind, but on the given address only.

The example for this item is shown with the ones above: net-udp-Echo.salam

Open

func Open(): Socket
A socket with no particular port, for sending.

The example for this item is shown with the ones above: net-udp-Echo.salam

Port

func Port(s: Socket): int
The port of the socket.

The example for this item is shown with the ones above: net-udp-Echo.salam

Ok

func Ok(s: Socket): bool
True when the socket was created properly.

The example for this item is shown with the ones above: net-udp-Echo.salam

Close

func Close(s: Socket)
Closes the socket.

The example for this item is shown with the ones above: net-udp-Echo.salam

Send

func Send(s: Socket, host: str, port: int, data: str): int
Sends one packet to a host and port and returns the number of bytes.

The example for this item is shown with the ones above: net-udp-Echo.salam

SendTo

func SendTo(s: Socket, host: str, port: int, data: str): int
The same as Send.

The example for this item is shown with the ones above: net-udp-Echo.salam

SendBytes

func SendBytes(s: Socket, host: str, port: int, ptr: void*, n: int): int
Sends n bytes from memory in one packet.

The example for this item is shown with the ones above: net-udp-Echo.salam

Receive

func Receive(s: Socket, max: int): Packet
Receives one packet of at most max bytes (65536 when max is 0); a longer packet is cut. When a timeout is set and nothing arrives, the packet comes back with empty data.

The example for this item is shown with the ones above: net-udp-Echo.salam

ReceiveBytes

func ReceiveBytes(s: Socket, buf: void*, max: int, out_host &: str, out_port &: int): int
Reads one packet into memory, returns its length (negative on timeout or error) and gives back the address and port of the sender.

The example for this item is shown with the ones above: net-udp-Echo.salam

SetBroadcast

func SetBroadcast(s: Socket, enable: bool)
Allows sending to the broadcast address of the network.

The example for this item is shown with the ones above: net-udp-Echo.salam

SetTimeout

func SetTimeout(s: Socket, millis: int)
How many milliseconds a receive may wait at most.

The example for this item is shown with the ones above: net-udp-Echo.salam

JoinMulticast

func JoinMulticast(s: Socket, group_ip: str): bool
Joins a multicast group so its packets arrive.

The example for this item is shown with the ones above: net-udp-Echo.salam

LeaveMulticast

func LeaveMulticast(s: Socket, group_ip: str): bool
Leaves a multicast group.

The example for this item is shown with the ones above: net-udp-Echo.salam