UDP Sockets
UDP is a connectionless, message-oriented transport. Each send produces
exactly one datagram, and each recv consumes exactly one datagram. There
is no handshake, no acknowledgment, and no ordering guarantee.
For the protocol-level fundamentals (header layout, fragmentation, when to
choose UDP over TCP), see UDP: Fast,
Simple, Unreliable. This page covers the Corosio API: udp_socket, the
udp protocol tag, and the operations they support.
|
Code snippets assume:
|
The udp Protocol Tag
corosio::udp identifies the address family used to open a socket. The two
factory functions udp::v4() and udp::v6() return the IPv4 and IPv6 forms:
corosio::udp_socket sock(ioc);
sock.open(corosio::udp::v4()); // SOCK_DGRAM, AF_INET
// or
sock.open(corosio::udp::v6()); // SOCK_DGRAM, AF_INET6
open() defaults to udp::v4() when no protocol is supplied. The tag is also
used by connect() to pick a family automatically when the socket is not yet
open — see Connected Mode below.
Opening and Binding
A socket must be open before any I/O. To receive datagrams it must also be bound to a local endpoint:
corosio::udp_socket sock(ioc);
sock.open(corosio::udp::v4());
auto ec = sock.bind(
corosio::endpoint(corosio::ipv4_address::any(), 9000));
if (ec) /* handle bind failure */;
bind() returns std::error_code rather than throwing — port-already-in-use
is a routine outcome you typically want to react to.
To bind on the wildcard address (accept datagrams on any local interface),
use ipv4_address::any() or ipv6_address::any(). To restrict to loopback,
use ::loopback().
Senders that never need to receive replies on a known port may skip bind();
the kernel assigns an ephemeral port on the first send_to.
Connectionless Mode
The default mode. Each datagram carries an explicit destination, and each receive captures the sender’s address.
Sending
char const msg[] = "hello";
corosio::endpoint dest(corosio::ipv4_address::loopback(), 9000);
auto [ec, n] = co_await sock.send_to(
capy::const_buffer(msg, sizeof(msg)), dest);
send_to either delivers the entire datagram to the network or fails — there
is no partial send for UDP. On success n equals the buffer size; on failure
ec carries the reason.
Receiving
recv_from writes the datagram into the buffer and stores the sender’s
address in the endpoint reference you pass in:
char buf[1500];
corosio::endpoint sender;
auto [ec, n] = co_await sock.recv_from(
capy::mutable_buffer(buf, sizeof(buf)), sender);
if (!ec)
{
// buf[0..n) holds the datagram; sender holds the source address.
}
If the buffer is smaller than the incoming datagram, the excess is silently discarded. Size your buffer for the largest datagram you expect — 1500 bytes covers a typical Ethernet MTU; 65535 covers any IPv4/IPv6 datagram.
A Minimal Echo Server
capy::task<> echo(corosio::io_context& ioc)
{
corosio::udp_socket sock(ioc);
sock.open(corosio::udp::v4());
auto ec = sock.bind(
corosio::endpoint(corosio::ipv4_address::any(), 9000));
if (ec) co_return;
char buf[1500];
for (;;)
{
corosio::endpoint sender;
auto [rec, n] = co_await sock.recv_from(
capy::mutable_buffer(buf, sizeof(buf)), sender);
if (rec) co_return;
co_await sock.send_to(
capy::const_buffer(buf, n), sender);
}
}
Notice that one socket serves every client. UDP has no per-connection state, so there is no acceptor and no peer socket to manage.
Connected Mode
connect() does not perform a handshake — it sets a default peer in the
kernel. Datagrams from any other source are filtered out, and send/recv
become available without endpoint arguments:
corosio::udp_socket sock(ioc);
auto [cec] = co_await sock.connect(
corosio::endpoint(corosio::ipv4_address::loopback(), 9000));
if (cec) co_return;
co_await sock.send(capy::const_buffer("ping", 4));
char buf[64];
auto [rec, n] = co_await sock.recv(
capy::mutable_buffer(buf, sizeof(buf)));
If the socket is not yet open when connect() is called, it is opened
automatically using the address family of the destination endpoint. This
makes a connect-then-send client a two-line affair.
You can call connect() again at any time to switch peers, or call it with
an unspecified endpoint (AF_UNSPEC) on platforms that support it to
dissolve the association.
Connected mode is useful for two reasons:
-
Filtering — the kernel drops stray datagrams from unrelated senders before your code sees them, which simplifies request/response clients.
-
ICMP error reporting — when a peer is unreachable, the kernel surfaces the resulting ICMP error on a subsequent
sendorrecvinstead of silently discarding it.
Message Flags
send_to, recv_from, send, and recv accept an optional
corosio::message_flags:
| Flag | Effect |
|---|---|
|
Return data without removing it from the receive queue. The next |
|
Send/receive out-of-band data (rarely used with UDP). |
|
Bypass routing tables; deliver only on directly attached interfaces. |
auto [ec, n] = co_await sock.recv_from(
capy::mutable_buffer(buf, sizeof(buf)), sender,
corosio::message_flags::peek);
Socket Options
Options are set with the typed wrappers in <boost/corosio/socket_option.hpp>:
sock.set_option(corosio::socket_option::reuse_address(true));
sock.set_option(corosio::socket_option::broadcast(true));
sock.set_option(corosio::socket_option::receive_buffer_size(1 << 20));
auto bcast = sock.get_option<corosio::socket_option::broadcast>();
Options commonly relevant to UDP:
| Option | When to set |
|---|---|
|
Multiple sockets on the same address (e.g., a reload swap, or several receivers in the same multicast group). |
|
Required before sending to a broadcast address such as
|
|
Tune the kernel’s per-socket queues. UDP datagrams are dropped when the receive queue is full — bursts of inbound traffic argue for a larger receive buffer. |
|
On an IPv6 socket, refuse IPv4-mapped addresses. Off by default on most platforms; enable for IPv6-only services. |
See Sockets for the full list of generic socket options.
Multicast
To send multicast datagrams, open a UDP socket and write to a multicast address. To receive them, bind to the multicast port and join the group:
corosio::udp_socket sock(ioc);
sock.open(corosio::udp::v4());
sock.set_option(corosio::socket_option::reuse_address(true));
auto ec = sock.bind(
corosio::endpoint(corosio::ipv4_address::any(), 30001));
if (ec) co_return;
sock.set_option(corosio::socket_option::join_group_v4(
corosio::ipv4_address("239.255.0.1")));
Related options:
| Option | Purpose |
|---|---|
|
Subscribe to or unsubscribe from an IPv4 multicast group. |
|
IPv6 equivalents. |
|
Enable or disable receiving your own outgoing multicast. |
|
Set the multicast TTL (IPv4) / hop limit (IPv6). Default is |
|
Choose the outgoing interface for IPv6 multicast. |
Cancellation
cancel() aborts every operation in flight on the socket. They complete
with errc::operation_canceled:
sock.cancel();
Coroutine-scoped cancellation flows through the awaiting task’s environment.
Run the task with a std::stop_token, and any UDP operation it awaits
completes with the canceled error when stop is requested:
std::stop_source ss;
capy::run_async(ioc.get_executor(), ss.get_token())(my_task());
// ...
ss.request_stop(); // unblocks any in-flight recv_from inside my_task
For portable error comparison, check against capy::cond::canceled rather
than a platform-specific errc value. See
Error Handling.
Concurrent Operations
A udp_socket permits one outstanding send and one outstanding receive at
a time. Two simultaneous recv_from calls on the same socket are not
supported. This is enough for the common pattern of a single coroutine that
alternates send and receive, or a pair of coroutines where one drives
outbound traffic and the other drains the receive queue.
For higher concurrency, use multiple sockets — UDP has no per-connection cost in the kernel, so a server can run several receivers in parallel.
Comparison with TCP
| Aspect | tcp_socket |
udp_socket |
|---|---|---|
Model |
Reliable byte stream |
Unreliable datagrams |
Message boundaries |
Not preserved |
Preserved (one |
Setup |
Three-way handshake before I/O |
|
Server topology |
One acceptor, one socket per peer |
One socket serves all peers |
Per-peer state |
Kernel tracks each connection |
Application’s responsibility |
Loss recovery |
Automatic |
Application’s responsibility |
Multicast / broadcast |
Not supported |
Supported |
Next Steps
-
UDP: Fast, Simple, Unreliable — protocol-level background
-
Sockets — generic socket options and lifetime rules
-
Endpoints — IP address and port construction
-
Error Handling — the
io_resultpattern