NetworkingC

UDP & Datagrams in C: High-Speed Connectionless Networking, Broadcasting & QUIC

TT
TopicTrick Team
UDP & Datagrams in C: High-Speed Connectionless Networking, Broadcasting & QUIC

UDP & Datagrams in C: High-Speed Connectionless Networking, Broadcasting & QUIC


Table of Contents


TCP vs UDP: The Fundamental Trade-off

ApplicationProtocolReason
Web (HTTP/1.1, HTTP/2)TCPReliability critical
DNS lookupsUDPSingle request-response; fast
Video streaming (HLS/DASH)TCPBuffered; stall is OK
Live video (WebRTC, RTSP)UDP100ms latency budget
Online gaming (position)UDPStale packets worthless
VoIP (voice call)UDPJitter tolerated; gaps OK
HTTP/3 (QUIC)UDPCustom reliability on UDP
DHCPUDPBroadcast; client has no IP yet

UDP Socket Lifecycle: No Connection State

Unlike TCP, UDP has no connection establishment or teardown. The simplified lifecycle:

text
socket() → bind() → sendto()/recvfrom() → close()

No listen(), no accept(), no connect() (optional for "connected" UDP). This simplicity is what makes UDP fast — there's no state machine to maintain in the kernel.


Building a UDP Echo Server

c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define UDP_PORT  9999
#define BUFSIZE   8192

int main(void) {
    // Step 1: Create UDP socket
    int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_fd < 0) { perror("socket"); return 1; }
    
    // Step 2: Allow port reuse
    int opt = 1;
    setsockopt(udp_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    // Step 3: Bind to port
    struct sockaddr_in server_addr = {
        .sin_family      = AF_INET,
        .sin_port        = htons(UDP_PORT),
        .sin_addr.s_addr = INADDR_ANY,
    };
    
    if (bind(udp_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind"); close(udp_fd); return 1;
    }
    
    printf("UDP Echo Server listening on port %d\n", UDP_PORT);
    
    // Step 4: Receive-process-respond loop (no accept() needed!)
    while (1) {
        char buffer[BUFSIZE];
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        
        // recvfrom: receive datagram AND capture sender's address
        ssize_t n = recvfrom(udp_fd, buffer, sizeof(buffer) - 1, 0,
                             (struct sockaddr*)&client_addr, &client_len);
        if (n < 0) { perror("recvfrom"); continue; }
        
        buffer[n] = '\0';
        
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("From %s:%d: %s\n", client_ip, ntohs(client_addr.sin_port), buffer);
        
        // Echo back to the same client
        sendto(udp_fd, buffer, n, 0,
               (struct sockaddr*)&client_addr, client_len);
    }
    
    close(udp_fd);
    return 0;
}

UDP Client: sendto and recvfrom

c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) { perror("socket"); return 1; }
    
    struct sockaddr_in server_addr = {
        .sin_family      = AF_INET,
        .sin_port        = htons(9999),
        .sin_addr.s_addr = inet_addr("127.0.0.1"),
    };
    
    const char *message = "Hello from UDP client!";
    
    // Send datagram to server
    ssize_t sent = sendto(sockfd, message, strlen(message), 0,
                          (struct sockaddr*)&server_addr, sizeof(server_addr));
    printf("Sent %zd bytes\n", sent);
    
    // Wait for response (with 5-second timeout)
    struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    
    char response[8192];
    struct sockaddr_in from_addr;
    socklen_t from_len = sizeof(from_addr);
    
    ssize_t n = recvfrom(sockfd, response, sizeof(response) - 1, 0,
                         (struct sockaddr*)&from_addr, &from_len);
    if (n < 0) {
        perror("recvfrom (timeout?)");
    } else {
        response[n] = '\0';
        printf("Response: %s\n", response);
    }
    
    close(sockfd);
    return 0;
}

Broadcasting: Sending to All Hosts on a Network

UDP broadcasting sends a single packet to all hosts on a subnet simultaneously:

c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void broadcast_service_announcement(uint16_t port) {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // Must explicitly enable broadcast permission
    int broadcast_enable = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable));
    
    // Broadcast address: last octet = 255 (subnet broadcast)
    // Or 255.255.255.255 for limited broadcast (all hosts on LAN)
    struct sockaddr_in broadcast_addr = {
        .sin_family      = AF_INET,
        .sin_port        = htons(port),
        .sin_addr.s_addr = htonl(INADDR_BROADCAST), // 255.255.255.255
    };
    
    const char *announcement = "SERVICE:MyApp:v1.2:8080";
    
    // One packet → ALL hosts on local network receive it
    sendto(sockfd, announcement, strlen(announcement), 0,
           (struct sockaddr*)&broadcast_addr, sizeof(broadcast_addr));
    
    printf("Broadcast sent: %s\n", announcement);
    close(sockfd);
}

Applications: DHCP (clients broadcast to find a DHCP server), UPnP device discovery, NetBIOS name resolution, Wake-on-LAN packets.


Multicasting: Selective Group Communication

Multicast is more targeted than broadcast — only hosts that have subscribed to the multicast group receive the packets. IPv4 multicast addresses are in the 224.0.0.0/4 range:

c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// Receiver: join a multicast group
void multicast_receiver(uint16_t port) {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    int reuse = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    
    struct sockaddr_in addr = {
        .sin_family      = AF_INET,
        .sin_port        = htons(port),
        .sin_addr.s_addr = INADDR_ANY,
    };
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    
    // Join multicast group 239.0.0.1
    struct ip_mreq group = {
        .imr_multiaddr.s_addr = inet_addr("239.0.0.1"),
        .imr_interface.s_addr = INADDR_ANY,
    };
    setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));
    
    // Now receive multicast packets
    char buf[4096];
    recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
    printf("Multicast: %s\n", buf);
}

Multicast is used by: streaming protocols (IPTV, live sports), financial market data feeds (Bloomberg, Nasdaq quote dissemination), and cluster node discovery (etcd, Consul).


Implementing Reliable UDP: Sequence Numbers and ACKs

When you need UDP's speed but TCP's reliability, implement it yourself:

c
#include <stdint.h>
#include <stdbool.h>

// Custom reliable UDP message header
typedef struct __attribute__((packed)) {
    uint32_t magic;       // 0xDEADBEEF — validate this is our protocol
    uint32_t sequence;    // Incrementing sequence number
    uint32_t ack;         // Last sequence number received (for flow control)
    uint16_t msg_type;    // 0=DATA, 1=ACK, 2=RESEND-REQUEST
    uint16_t checksum;    // Simple checksum to detect corruption
    uint16_t payload_len; // Payload size in bytes
} ReliableUDPHeader;

// Simple sender state
typedef struct {
    uint32_t         next_seq;     // Next sequence number to send
    uint32_t         unacked_seq;  // Oldest unacknowledged sequence
    ReliableUDPHeader *unacked_pkt; // Packet waiting for ACK
    // In production: ring buffer of unacknowledged packets
} ReliableUDPSender;

This is essentially what QUIC (the protocol behind HTTP/3) does at a much more sophisticated level — connection multiplexing, congestion control, and encryption all implemented on top of UDP at the application layer.


UDP for Real-Time Games: The State Snapshot Model

Online games like first-person shooters use UDP because a lost position packet is irrelevant — the next packet (arriving 16ms later) will have newer data:

c
typedef struct {
    uint32_t sequence;       // Monotonically increasing
    uint32_t timestamp_ms;   // Server timestamp for jitter compensation
    uint8_t  player_id;
    float    pos_x, pos_y, pos_z;  // Current position (not delta)
    float    vel_x, vel_y, vel_z;  // Current velocity
    float    aim_yaw, aim_pitch;   // Camera orientation
    uint32_t input_flags;          // Button states as bit flags
} GameStatePacket;

// Client sends state 60 times/second over UDP
// Server uses the sequence number to detect and discard out-of-order packets
// Missing packets are simply ignored — render uses latest received state

The key insight: game state packets contain the full current state, not a delta. If a packet is lost, the next packet restores full state — no need to retransmit the lost one.


DNS: UDP in Production (Port 53)

DNS uses UDP for most queries:

  • Query packet: ~40-60 bytes typically.
  • Response packet: ~100-500 bytes for most lookups.
  • If response > 512 bytes, DNS falls back to TCP.
  • The entire round trip (query + response) costs one UDP exchange — no TCP handshake.
c
// Simplified DNS query packet structure (RFC 1035)
typedef struct __attribute__((packed)) {
    uint16_t  id;          // Query ID — matched with response
    uint16_t  flags;       // QR=0 (query), OPCODE, AA, TC, RD, RA, Z, RCODE
    uint16_t  qdcount;     // Number of questions
    uint16_t  ancount;     // Number of answers (0 in query)
    uint16_t  nscount;     // Number of authority records
    uint16_t  arcount;     // Number of additional records
    // Followed by variable-length question section
} DNSHeader;

QUIC: The Future of UDP (HTTP/3)

QUIC, standardized as RFC 9000 and implemented by Google, Cloudflare, and all major browsers, multiplexes HTTP streams over UDP:

  • No head-of-line blocking: In HTTP/2 over TCP, one lost packet blocks all streams. In QUIC, each stream independently handles retransmission.
  • 0-RTT connection: QUIC combines TLS handshake with connection establishment — first request can be sent in 0 round trips for known servers.
  • Connection migration: QUIC connections survive IP address changes (e.g., switching from Wi-Fi to 4G) because they're identified by a connection ID, not by (source IP, source port).

QUIC's entire reliability, ordering, and multiplexing layer is implemented in C/C++ at the application layer, on top of raw UDP sockets.


Frequently Asked Questions

When should I choose UDP over TCP? Use UDP when: latency is more important than reliability (live video, gaming, trading), the application-level data naturally overwrites stale data (position updates, sensor readings), you're implementing a custom protocol that already handles reliability (QUIC, SRTP, RTP), or you need broadcast/multicast (service discovery, DHCP).

Can UDP accidentally deliver duplicate packets? Yes — UDP does not filter duplicate packets. Network equipment can duplicate packets, and routers may send a packet on multiple paths. Your application protocol must handle duplicates by checking the sequence number.

Is UDP faster than TCP for large file transfers? Usually not — TCP's congestion control and buffer management are highly optimized for throughput. Tools like iPerf3 consistently show TCP matching or exceeding naive UDP for bulk transfers. UDP only wins for latency-sensitive workloads where TCP's retransmission delays are unacceptable.

What is the maximum size of a UDP datagram? The maximum UDP payload is 65,507 bytes (16-bit length field minus IP and UDP headers). However, packets larger than the network's MTU (~1,472 bytes for typical Ethernet after headers) will be fragmented at the IP layer, reducing efficiency. Production UDP applications typically limit to 1,400 bytes per datagram.


Key Takeaway

UDP is the Express Lane of Networking. Its connectionless model, no-handshake simplicity, and broadcast/multicast capabilities make it indispensable for real-time systems where TCP's reliability overhead is unacceptable. By implementing reliable messaging, ordering, and congestion control yourself — as QUIC does — you get the best of both worlds.

Read next: C23 Modern Evolution: auto, nullptr, constexpr →


Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.