UDP and Datagram Sockets

UDP is the other transport protocol on IP. It provides no connections, no guaranteed delivery, no ordering. You send a datagram, and it either arrives whole or not at all. This simplicity makes UDP fast and the right choice for DNS lookups, live video, gaming, and service discovery.

This chapter covers sendto/recvfrom, multicast, broadcast, and builds a practical service discovery protocol.

UDP vs TCP Recap

  TCP (SOCK_STREAM)                  UDP (SOCK_DGRAM)
  +----------------------------------+----------------------------------+
  | 3-way handshake                  | No handshake                     |
  | Guaranteed delivery (retransmit) | Fire and forget                  |
  | Ordered byte stream              | Independent datagrams            |
  | Flow control, congestion control | None built-in                    |
  | ~200 bytes overhead per segment  | 8-byte header                    |
  +----------------------------------+----------------------------------+

A UDP Echo Server in C

/* udp_echo_server.c -- receive datagrams, echo them back */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    struct sockaddr_in addr = {0};
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port        = htons(5000);

    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); return 1;
    }
    printf("UDP echo server on port 5000\n");

    for (;;) {
        char buf[65535];
        struct sockaddr_in client;
        socklen_t clen = sizeof(client);

        ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                             (struct sockaddr *)&client, &clen);
        if (n < 0) { perror("recvfrom"); continue; }

        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client.sin_addr, ip, sizeof(ip));
        printf("From %s:%d (%zd bytes)\n", ip, ntohs(client.sin_port), n);

        /* Echo back to sender */
        sendto(fd, buf, n, 0, (struct sockaddr *)&client, clen);
    }

    close(fd);
    return 0;
}

Notice: no listen(), no accept(). A single socket handles all clients. recvfrom tells you who sent the datagram; sendto sends a reply directly to that address.

A UDP Client in C

/* udp_client.c -- send a message, wait for reply */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    struct sockaddr_in server = {0};
    server.sin_family = AF_INET;
    server.sin_port   = htons(5000);
    inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);

    const char *msg = "Hello, UDP!";
    sendto(fd, msg, strlen(msg), 0,
           (struct sockaddr *)&server, sizeof(server));

    char buf[1024];
    struct sockaddr_in from;
    socklen_t flen = sizeof(from);
    ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
                         (struct sockaddr *)&from, &flen);
    if (n > 0) {
        buf[n] = '\0';
        printf("Reply: %s\n", buf);
    }

    close(fd);
    return 0;
}

Caution: recvfrom blocks forever if no reply comes. In production, set a timeout with setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...) or use poll() before reading.

Try It: Start the UDP server, run the client, then kill the server and run the client again. Observe that sendto succeeds even though nobody is listening -- UDP does not detect that the remote end is unreachable (unless the network returns an ICMP port-unreachable, which may or may not arrive).

When UDP Is Appropriate

  • DNS -- a single question-answer exchange. Retransmit if no reply in 2 seconds.
  • Gaming -- player position updates arrive 60 times per second. A lost packet is stale by the time it would be retransmitted.
  • Live video/audio -- a dropped frame is better than a delayed frame.
  • Service discovery -- "who's on the network?" is a broadcast/multicast question, and TCP cannot broadcast.
  • IoT sensors -- tiny devices with limited memory cannot afford TCP's state machine.

Handling Packet Loss at the Application Layer

UDP gives you no retransmission. If reliability matters, build it yourself.

  Sender                          Receiver
    |                                |
    |--- [seq=1] data ------------->|
    |                                |--- [ack=1] ---------->|
    |--- [seq=2] data ------------->|
    |          (lost)                |
    |--- (timeout, resend seq=2) -->|
    |                                |--- [ack=2] ---------->|

The minimum reliable protocol over UDP:

  1. Attach a sequence number to each datagram.
  2. The receiver acknowledges each sequence number.
  3. The sender retransmits if no acknowledgment arrives within a timeout.
  4. The receiver discards duplicates.
/* reliable_header.h -- minimal reliability over UDP */
#ifndef RELIABLE_HEADER_H
#define RELIABLE_HEADER_H

#include <stdint.h>

struct reliable_hdr {
    uint32_t seq;       /* sequence number (network byte order) */
    uint32_t ack;       /* acknowledgment number */
    uint16_t flags;     /* 0x01 = DATA, 0x02 = ACK */
    uint16_t len;       /* payload length */
};

#define FLAG_DATA 0x01
#define FLAG_ACK  0x02

#endif

Driver Prep: Many industrial and automotive protocols (CAN bus, some PROFINET variants) run on UDP or raw frames and implement their own reliability layer. This pattern shows up everywhere below TCP.

Broadcast

Broadcast sends a datagram to every host on the local subnet. The destination address is 255.255.255.255 (limited broadcast) or the subnet broadcast address (e.g., 192.168.1.255 for a /24 network).

/* broadcast_sender.c -- send a broadcast message */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    /* Must enable broadcast on the socket */
    int broadcast = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST,
                   &broadcast, sizeof(broadcast)) < 0) {
        perror("setsockopt"); return 1;
    }

    struct sockaddr_in dest = {0};
    dest.sin_family      = AF_INET;
    dest.sin_port        = htons(5001);
    inet_pton(AF_INET, "255.255.255.255", &dest.sin_addr);

    const char *msg = "DISCOVER";
    sendto(fd, msg, strlen(msg), 0,
           (struct sockaddr *)&dest, sizeof(dest));
    printf("Broadcast sent\n");

    close(fd);
    return 0;
}

Caution: Broadcasting generates traffic that every host on the subnet must process. Do it sparingly. On large networks, prefer multicast.

Multicast

Multicast sends datagrams to a group address (224.0.0.0 - 239.255.255.255). Only hosts that join the group receive the traffic. The network infrastructure (IGMP) handles group membership.

/* mcast_receiver.c -- join a multicast group and print messages */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    /* Allow multiple receivers on same port */
    int reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    struct sockaddr_in addr = {0};
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port        = htons(5002);

    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); return 1;
    }

    /* Join multicast group 239.1.1.1 */
    struct ip_mreq mreq;
    inet_pton(AF_INET, "239.1.1.1", &mreq.imr_multiaddr);
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);

    if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
                   &mreq, sizeof(mreq)) < 0) {
        perror("setsockopt IP_ADD_MEMBERSHIP"); return 1;
    }
    printf("Joined multicast group 239.1.1.1, listening on port 5002\n");

    for (;;) {
        char buf[1024];
        ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0, NULL, NULL);
        if (n < 0) { perror("recvfrom"); break; }
        buf[n] = '\0';
        printf("Received: %s\n", buf);
    }

    close(fd);
    return 0;
}
/* mcast_sender.c -- send to a multicast group */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    /* Set TTL for multicast (1 = local subnet only) */
    unsigned char ttl = 1;
    setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));

    struct sockaddr_in dest = {0};
    dest.sin_family = AF_INET;
    dest.sin_port   = htons(5002);
    inet_pton(AF_INET, "239.1.1.1", &dest.sin_addr);

    const char *msg = "Hello, multicast group!";
    sendto(fd, msg, strlen(msg), 0,
           (struct sockaddr *)&dest, sizeof(dest));
    printf("Sent to multicast group 239.1.1.1\n");

    close(fd);
    return 0;
}

Try It: Start two or more mcast_receiver processes, then run mcast_sender. All receivers should print the message. Then stop one receiver and verify the others still work.

A Simple Discovery Protocol

Combine broadcast and timed responses to build a LAN service discovery mechanism.

/* discover_server.c -- respond to discovery broadcasts */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    int reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    struct sockaddr_in addr = {0};
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port        = htons(5003);

    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    printf("Discovery responder on port 5003\n");

    for (;;) {
        char buf[256];
        struct sockaddr_in client;
        socklen_t clen = sizeof(client);

        ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
                             (struct sockaddr *)&client, &clen);
        if (n < 0) continue;
        buf[n] = '\0';

        if (strcmp(buf, "DISCOVER") == 0) {
            const char *reply = "SERVICE:echo:7878";
            sendto(fd, reply, strlen(reply), 0,
                   (struct sockaddr *)&client, clen);
        }
    }
}
/* discover_client.c -- broadcast DISCOVER, collect responses */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    int broadcast = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

    /* Set 2-second receive timeout */
    struct timeval tv = { .tv_sec = 2, .tv_usec = 0 };
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    struct sockaddr_in dest = {0};
    dest.sin_family = AF_INET;
    dest.sin_port   = htons(5003);
    inet_pton(AF_INET, "255.255.255.255", &dest.sin_addr);

    sendto(fd, "DISCOVER", 8, 0,
           (struct sockaddr *)&dest, sizeof(dest));
    printf("Sent DISCOVER broadcast, waiting for replies...\n");

    for (;;) {
        char buf[256];
        struct sockaddr_in from;
        socklen_t flen = sizeof(from);
        ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
                             (struct sockaddr *)&from, &flen);
        if (n < 0) break;    /* timeout or error */
        buf[n] = '\0';

        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &from.sin_addr, ip, sizeof(ip));
        printf("Found: %s at %s\n", buf, ip);
    }

    printf("Discovery complete.\n");
    close(fd);
    return 0;
}
  Discovery flow:

  Client                          Network                     Server(s)
    |                                |                           |
    |-- DISCOVER (broadcast) ------->| ------->                  |
    |                                |         [server receives] |
    |<------- SERVICE:echo:7878 -----|<------                    |
    |                                |                           |
    |  (timeout: 2 seconds)          |                           |
    |  [done]                        |                           |

Rust: UdpSocket

// udp_echo_server.rs -- UDP echo server
use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:5000")?;
    println!("UDP echo server on port 5000");

    let mut buf = [0u8; 65535];
    loop {
        let (n, src) = socket.recv_from(&mut buf)?;
        println!("From {} ({} bytes)", src, n);
        socket.send_to(&buf[..n], src)?;
    }
}
// udp_client.rs -- send a datagram, receive reply
use std::net::UdpSocket;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:0")?;  // OS picks port
    socket.set_read_timeout(Some(Duration::from_secs(3)))?;

    socket.send_to(b"Hello, UDP!", "127.0.0.1:5000")?;

    let mut buf = [0u8; 1024];
    match socket.recv_from(&mut buf) {
        Ok((n, src)) => {
            println!("Reply from {}: {}",
                     src, String::from_utf8_lossy(&buf[..n]));
        }
        Err(e) => eprintln!("No reply: {}", e),
    }
    Ok(())
}

Rust Note: UdpSocket::bind("0.0.0.0:0") binds to a random available port. The address string is parsed via the ToSocketAddrs trait, which also handles DNS resolution. The set_read_timeout method replaces the C setsockopt dance.

Rust: Multicast

// mcast_receiver.rs -- join multicast group and receive
use std::net::{UdpSocket, Ipv4Addr};

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:5002")?;

    let multiaddr: Ipv4Addr = "239.1.1.1".parse().unwrap();
    let interface = Ipv4Addr::UNSPECIFIED;
    socket.join_multicast_v4(&multiaddr, &interface)?;

    println!("Joined multicast group 239.1.1.1 on port 5002");

    let mut buf = [0u8; 1024];
    loop {
        let (n, src) = socket.recv_from(&mut buf)?;
        println!("From {}: {}", src,
                 String::from_utf8_lossy(&buf[..n]));
    }
}
// mcast_sender.rs -- send to multicast group
use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:0")?;
    socket.set_multicast_ttl_v4(1)?;

    socket.send_to(b"Hello, multicast group!", "239.1.1.1:5002")?;
    println!("Sent to multicast group");
    Ok(())
}

Rust: Discovery Protocol

// discover_client.rs -- broadcast discovery and collect replies
use std::net::UdpSocket;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:0")?;
    socket.set_broadcast(true)?;
    socket.set_read_timeout(Some(Duration::from_secs(2)))?;

    socket.send_to(b"DISCOVER", "255.255.255.255:5003")?;
    println!("Sent DISCOVER, waiting for replies...");

    let mut buf = [0u8; 256];
    loop {
        match socket.recv_from(&mut buf) {
            Ok((n, src)) => {
                let msg = String::from_utf8_lossy(&buf[..n]);
                println!("Found: {} at {}", msg, src);
            }
            Err(_) => break,
        }
    }
    println!("Discovery complete.");
    Ok(())
}

Maximum Datagram Size

  +-- Ethernet MTU: 1500 bytes ---+
  | IP header (20B) | UDP (8B) | payload (up to 1472B) |
  +------------------+-----------+-----------------------+

  Larger datagrams are fragmented by IP.
  Any lost fragment = entire datagram lost.
  Safe payload size for LAN: 1472 bytes
  Safe payload size for internet: ~512 bytes (conservative)
  Maximum theoretical UDP payload: 65,507 bytes

Caution: Sending 64 KB datagrams over the internet is asking for trouble. IP fragmentation dramatically increases the chance of packet loss because losing any single fragment kills the entire datagram. Stay under the path MTU.

Knowledge Check

  1. Why does a UDP server not need listen() or accept()?
  2. What socket option must be enabled before calling sendto with a broadcast address?
  3. How does multicast differ from broadcast in terms of network traffic?

Common Pitfalls

  • Assuming delivery -- UDP does not guarantee anything. Always plan for lost packets.
  • Assuming ordering -- datagrams can arrive out of order, especially across the internet.
  • Forgetting SO_BROADCAST -- sendto with a broadcast address fails with EACCES without it.
  • Large datagrams -- IP fragmentation silently destroys reliability. Keep payloads small.
  • No timeout on recvfrom -- blocks forever if no packet arrives. Always set SO_RCVTIMEO or use poll().
  • Multicast on loopback only -- by default, multicast may not leave the loopback interface. Check your routing table if receivers on other hosts do not get packets.