Non-blocking UDP sends and task-based batched receives.
A socket is bound once and kept in the model. bind! and send! are legal
in init!, update!, and tasks, and refused in render!, which draws.
receive! waits: it is legal in init!, where it blocks startup, and in
tasks, where it parks the task; it is refused in update! and render!.
receive! parks until one datagram arrives, then drains an arrival-ordered
bounded batch. Start the next receive task after that message arrives.
The operating-system socket buffer retains datagrams between receives and
may drop them silently on overflow. UDP does not guarantee delivery,
ordering, or uniqueness.
Framing, retransmission, serialization, discovery, and session state are the
app's or a package's. This module moves bytes and nothing else.
Addresses are IPv4 literals. An IPv6 address is InvalidAddress, and there
is no name resolution: bind! and send! take a dotted-quad string, never
a hostname, because resolving a name waits and neither of these effects
does.
An app binds only the ports it declared with UdpBind and sends
only to the peers it declared with UdpPeer; UdpAny
lifts both limits. A port or peer outside the declarations is
PermissionDenied, and using Udp with no UDP declaration at all stops the
app as a programmer error. Declaring a peer also permits an ephemeral bind
(port 0). Ports below 1024 usually need operating-system privileges, and
report AccessRefused when they are missing. Broadcast and multicast are not
enabled.
The deadline is short enough that a listener respawned every frame keeps
its socket's queue drained even when a peer goes quiet, and long enough
not to churn tasks at idle.
The largest payload one datagram may hold: 65535 bytes of IPv4 packet
less the 20-byte IP header and the 8-byte UDP header. A send over this
is TooLarge.
Sending anywhere near it is unwise on a real network, where anything
over the path MTU is fragmented and one lost fragment loses the whole
datagram. Real-time protocols stay under about 1200 bytes.
timeout_ms is how long to wait for the first datagram; 0 waits
forever, which only makes sense in a task the app is content to leave
parked until shutdown. max_datagrams caps the batch, and the host
clamps it to sixty-four -- past that the rest simply stay buffered for
the next receive.
A plain record; build one with { ..Udp.default_receive, timeout_ms: 200 }
rather than a chain of with_* calls.
AddressInUse is another socket already holding the port,
AddressUnavailable is an address that is not one of this machine's,
and ResourceLimit is this platform's own ceiling of eight open
sockets. InvalidAddress is a string that is not a dotted-quad IPv4
literal. PermissionDenied is a port the app did not declare, and
AccessRefused is the operating system refusing the bind.
WouldBlock is the send buffer being full: the datagram was not sent,
and the app is sending faster than the network link can transmit. TooLarge is a
payload over max_datagram_bytes, refused rather than truncated,
because a truncated datagram decodes into wrong data. None of these mean
the peer received anything, and no code means it did -- UDP does not
report that. PermissionDenied is a peer the app did not declare, and
AccessRefused is the operating system refusing the send.
Timeout is the deadline expiring with nothing having arrived, which is
an ordinary quiet moment rather than a failure. AlreadyReceiving is a
second task trying to receive on a socket that already has one parked.
The handle is reference counted: copy it freely, and when the last copy
goes -- out of the model, out of a task's captures, or at shutdown --
the socket is closed. There is nothing to remember to close.
Send one datagram. This does not wait for anything: the payload goes
to the operating system and the call is over, whether or not any
peer is listening. Nothing here reports that a datagram arrived,
because UDP does not.
It is a queued effect: the datagram is handed whole to the kernel's
bounded send buffer and the call returns at once, with the sending
itself happening off the frame thread. WouldBlock is that queue's
saturation -- the buffer was full, so this datagram was not taken --
and it is a typed result rather than a wait or a silent drop.
Legal in init!, update!, and tasks; refused in render!.
Wait for datagrams, and return every one that was ready.
Parks until the first datagram arrives or timeout_ms expires, then
returns it together with whatever else is already buffered, in
arrival order, up to max_datagrams. Err(Timeout) means nothing
arrived in time, which is a quiet moment rather than a failure.
One receive at a time per socket: a second one while another task is
parked is AlreadyReceiving.
Legal in init!, where it blocks startup, and in tasks, where it
parks the task; refused in update! and render!.
The handle never resolves to an open socket, so every effect made
through it fails as Unavailable -- Err(Unavailable) from send!
and from receive! -- the same way a call through a closed socket
does. It exists for the app that keeps a socket in its model, to let
a pure expect build that model. Do not use it to test delivery or
resource lifetime.
{ ip: "0.0.0.0", port: 0 } takes any interface and lets the operating
system choose the port; the returned socket's local_address reports
what it chose. Bind once, in init! or when the app decides to start
networking, and keep the socket in the model -- binding per frame would
exhaust the eight-socket ceiling and change the port every time.
Legal in init!, update!, and tasks; refused in render!.