Networking with the Standard Library

UDP Sockets

A UdpSocket (std::net::UdpSocket) is a connectionless datagram socket that sends and receives discrete, message-bounded packets over the User Datagram Protocol (UDP). Unlike TcpStream, a single UdpSocket can exchange packets with any number of remote peers without establishing a connection handshake.

Peer A (127.0.0.1:50001) -- send_to("ping") --> +----------------------------+
                                                |         UdpSocket          |
Peer B (127.0.0.1:50002) -- send_to("stat") --> |   bound to 127.0.0.1:9000  |
                                                | recv_from() -> (len, peer) |
Peer A (127.0.0.1:50001) <- send_to("pong") --- +----------------------------+

Unconnected vs Connected UdpSocket Methods

A UdpSocket is always created via UdpSocket::bind(addr). Once bound, it can operate in two modes:

  1. Unconnected (Ad-hoc Addressing): Uses send_to(buf, target) and recv_from(&mut buf) to specify or inspect the peer address on every packet.
  2. Connected (Default Peer Filter): Calling socket.connect(remote_addr) does not perform a network handshake; instead, it configures the OS kernel to send all .send(buf) datagrams to remote_addr and filter .recv(&mut buf) so only datagrams arriving from remote_addr are accepted.
Method Signature Description
UdpSocket::bind(addr) bind<A: ToSocketAddrs>(addr: A) -> io::Result<UdpSocket> Binds a new UDP socket to a local address and port.
.send_to(buf, addr) send_to<A: ToSocketAddrs>(&self, buf: &[u8], addr: A) -> io::Result<usize> Sends a single datagram to addr.
.recv_from(&mut buf) recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> Reads a single datagram into buf, returning (byte_count, sender_addr).
.peek_from(&mut buf) peek_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> Inspects the next datagram and sender without removing it from the receive queue.
.connect(addr) connect<A: ToSocketAddrs>(&self, addr: A) -> io::Result<()> Sets the default remote peer address for send and recv.
.send(buf) / .recv(&mut buf) send(&self, buf: &[u8]) -> io::Result<usize> Sends or receives datagrams to/from the connected peer.

Exchanging Datagrams Between Two UDP Sockets

use std::io;
use std::net::UdpSocket;

fn main() -> io::Result<()> {
    let server = UdpSocket::bind("127.0.0.1:0")?;
    let client = UdpSocket::bind("127.0.0.1:0")?;

    let server_addr = server.local_addr()?;
    let client_addr = client.local_addr()?;

    // Client connects to `server_addr` so it can use `.send()` and `.recv()`
    client.connect(server_addr)?;
    client.send(b"telemetry:cpu=42")?;

    // Server receives the datagram and the sender's SocketAddr via `.recv_from()`
    let mut buf = [0u8; 128];
    let (n, sender) = server.recv_from(&mut buf)?;
    assert_eq!(sender, client_addr);

    let msg = std::str::from_utf8(&buf[..n]).unwrap();
    println!("Server received ({n} bytes): {msg}");

    // Server replies directly to `sender`
    server.send_to(b"ACK:42", sender)?;

    let n_reply = client.recv(&mut buf)?;
    println!(
        "Client received reply: {}",
        std::str::from_utf8(&buf[..n_reply]).unwrap()
    );

    Ok(())
}
Server received (16 bytes): telemetry:cpu=42
Client received reply: ACK:42

Datagram Boundaries and Truncation Hazard

Unlike TCP (where unread bytes remain in the stream for the next read() call), each recv or recv_from call consumes an entire UDP datagram:

  • If a datagram is 500 bytes long and you pass a 64-byte buffer ([0u8; 64]), the first 64 bytes are copied into the buffer and the remaining 436 bytes of that datagram are permanently discarded (or on Windows, WSAEMSGSIZE is returned).
  • For general internet UDP protocols, keep datagram payloads under 1,400 bytes (to avoid IP-layer fragmentation below the standard 1,500-byte Ethernet MTU) and allocate a receive buffer of at least 1,500–65,535 bytes.