Loading content...
While TCP provides reliable, ordered delivery, many applications need something different: raw speed. Real-time games can't wait for retransmissions. Live video can skip a frame rather than freeze. DNS queries need quick responses, not perfect reliability.
This is where UDP sockets shine. UDP (User Datagram Protocol) provides a thin layer over IP—just enough to add port numbers and a checksum. There's no connection establishment, no acknowledgments, no retransmissions, no ordering guarantees. You send a datagram, and it either arrives or it doesn't.
This simplicity comes with responsibility. With TCP, the protocol handles reliability. With UDP, your application must decide how to handle lost, duplicated, or out-of-order packets—if it needs to handle them at all. Some applications simply ignore losses; others build their own reliability mechanisms on top of UDP.
By the end of this page, you will master UDP socket programming: the connectionless programming model, sending and receiving datagrams, the differences between connected and unconnected UDP sockets, multicast and broadcast, error handling, and patterns for building applications that need UDP's unique characteristics.
UDP provides a datagram-oriented, connectionless, unreliable communication service. Let's understand exactly what this means:
Datagram-Oriented Communication:
Unlike TCP's byte stream, UDP preserves message boundaries. Each sendto() call sends a discrete datagram; each recvfrom() call receives exactly one datagram:
TCP (byte stream): UDP (datagrams):
┌──────────────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ A B C D E F G H │ │ ABC │ │ DEF │ │ GH │
│ (continuous) │ │(packet)│ │(packet)│ │(packet)│
└──────────────────┘ └────────┘ └────────┘ └────────┘
↓ ↓ ↓ ↓
recv() might get: recvfrom() gets exactly:
- "ABCD" - "ABC"
- "EFGH" - "DEF"
- "ABCDEFGH" - "GH"
(any split) (one per call)
This property is extremely useful when your protocol is naturally message-based. You don't need to implement message framing—UDP does it for you.
Use UDP when: (1) Latency is more important than reliability, (2) Occasional packet loss is acceptable, (3) You need multicast/broadcast, (4) Message boundaries are natural to your protocol, or (5) You want to implement custom reliability. Common examples: real-time games, VoIP, video streaming, DNS, NTP, SNMP.
UDP socket programming is simpler than TCP because there's no connection to establish or maintain. The basic pattern involves creating a socket, optionally binding it, then sending and receiving datagrams.
Creating a UDP Socket:
// Create a UDP socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
Note the SOCK_DGRAM (datagram) type instead of SOCK_STREAM used for TCP.
Server Setup—Bind to a Port:
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // All interfaces
server_addr.sin_port = htons(PORT);
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// Ready to receive datagrams!
Unlike TCP, there's no listen() or accept(). After bind(), the socket is immediately ready to receive datagrams from any sender.
UDP servers don't need listen() or accept() because there's no connection. A single UDP socket can simultaneously handle datagrams from unlimited peers. The socket serves everyone who sends to it.
Sending Datagrams—sendto():
const char *message = "Hello, UDP!";
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, "192.168.1.100", &dest_addr.sin_addr);
ssize_t sent = sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (sent < 0) {
perror("sendto failed");
} else if (sent != strlen(message)) {
// Unlike TCP, partial sends are rare in UDP
// But check anyway
}
Receiving Datagrams—recvfrom():
char buffer[65535];
struct sockaddr_in sender_addr;
socklen_t sender_len = sizeof(sender_addr);
ssize_t received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&sender_addr, &sender_len);
if (received < 0) {
perror("recvfrom failed");
} else {
buffer[received] = '\0';
// We know who sent this!
char sender_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sender_addr.sin_addr, sender_ip, sizeof(sender_ip));
printf("Received %zd bytes from %s:%d: %s\n",
received, sender_ip, ntohs(sender_addr.sin_port), buffer);
}
| Operation | TCP | UDP |
|---|---|---|
| Create socket | socket(SOCK_STREAM) | socket(SOCK_DGRAM) |
| Server setup | bind + listen + accept | bind only |
| Client setup | connect (required) | connect (optional) |
| Send data | send() / write() | sendto() or send() |
| Receive data | recv() / read() | recvfrom() or recv() |
| Peer address | Implicit (connected) | Explicit in sendto/recvfrom |
While UDP is connectionless at the protocol level, you can call connect() on a UDP socket. This doesn't create a connection—it sets a default destination and enables some optimizations.
Connecting a UDP Socket:
// Without connect: must specify address every send
sendto(sockfd, msg, len, 0, (struct sockaddr *)&dest, sizeof(dest));
// With connect: address is remembered
connect(sockfd, (struct sockaddr *)&dest, sizeof(dest));
send(sockfd, msg, len, 0); // Simpler API, address implicit
recv(sockfd, buffer, buflen, 0); // Only receives from connected peer
What connect() Does for UDP:
Unlike TCP, connect() on UDP sends no packets and establishes no connection. It's purely local state in the kernel. The "connection" can be changed by calling connect() again, or removed by connecting to an invalid address (AF_UNSPEC).
ICMP Error Handling:
A crucial difference: unconnected UDP sockets don't receive ICMP error notifications. Connected UDP sockets do:
// Unconnected UDP: sendto() returns success even if port is closed
sendto(sockfd, msg, len, 0, &dest_addr, sizeof(dest_addr));
// Returns success! ICMP "port unreachable" is delivered but lost
// Connected UDP: ICMP errors are reported
connect(sockfd, &dest_addr, sizeof(dest_addr));
send(sockfd, msg, len, 0);
// If port is closed, next send() or recv() returns -1
// with errno = ECONNREFUSED
When to Use Connected UDP:
One of UDP's unique capabilities is one-to-many communication. While TCP can only do point-to-point (one sender, one receiver), UDP supports broadcast (one-to-all on local network) and multicast (one-to-many interested receivers).
Broadcasting:
// Enable broadcast
int broadcast_enable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST,
&broadcast_enable, sizeof(broadcast_enable));
// Send to broadcast address
struct sockaddr_in broadcast_addr;
memset(&broadcast_addr, 0, sizeof(broadcast_addr));
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(PORT);
broadcast_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST); // 255.255.255.255
// Or use directed broadcast: 192.168.1.255 for 192.168.1.0/24 network
sendto(sockfd, message, len, 0,
(struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
Broadcast packets are not forwarded by routers—they only reach the local network segment. For cross-network one-to-many communication, use multicast instead.
Multicast:
Multicast sends data to all hosts that have joined a specific multicast group. Multicast addresses fall in the range 224.0.0.0 to 239.255.255.255.
// Joining a multicast group
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("239.0.0.1"); // Multicast group
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // Local interface
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
&mreq, sizeof(mreq)) < 0) {
perror("setsockopt IP_ADD_MEMBERSHIP failed");
exit(EXIT_FAILURE);
}
// Now recvfrom() will receive datagrams sent to 239.0.0.1
// Sending to a multicast group
struct sockaddr_in mcast_addr;
mcast_addr.sin_family = AF_INET;
mcast_addr.sin_port = htons(PORT);
mcast_addr.sin_addr.s_addr = inet_addr("239.0.0.1");
sendto(sockfd, message, len, 0,
(struct sockaddr *)&mcast_addr, sizeof(mcast_addr));
Multicast TTL:
Multicast datagrams have a TTL (Time To Live) that limits their scope:
unsigned char ttl = 32; // Up to 32 router hops
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
| Feature | Broadcast | Multicast |
|---|---|---|
| Scope | Local network only | Can span networks (with routing) |
| Recipients | All hosts on local subnet | Only hosts that join group |
| Efficiency | All hosts process packet | Only interested hosts receive |
| Address | 255.255.255.255 or directed | 224.0.0.0 - 239.255.255.255 |
| Routing | Not routed | Can be routed (IGMP, PIM) |
| Socket option | SO_BROADCAST | IP_ADD_MEMBERSHIP |
| Use case | Local discovery (DHCP, ARP) | Streaming, distributed systems |
UDP error handling is fundamentally different from TCP because UDP is unreliable. Many "errors" that TCP reports are simply not detected by UDP—packets just disappear.
What UDP Cannot Detect:
What UDP Can Detect:
A successful sendto() only means the datagram was handed to the kernel. It says nothing about whether the datagram reached the network, arrived at the destination, or was processed by an application. Guaranteed delivery requires application-level acknowledgments.
Common UDP Errors:
| errno | Cause | Notes |
|---|---|---|
| EMSGSIZE | Datagram too large | MTU exceeded or system limit |
| ENOBUFS | Kernel buffer full | Sending too fast |
| EAGAIN/EWOULDBLOCK | Would block (non-blocking mode) | Retry later |
| ECONNREFUSED | ICMP port unreachable (connected UDP) | Peer closed |
| ENETUNREACH | Network unreachable | Routing problem |
| EHOSTUNREACH | Host unreachable | Target not reachable |
Handling Datagram Size:
UDP has a protocol limit of 65,535 bytes per datagram, but practical limits are lower:
// Maximum UDP payload size
const size_t MAX_UDP_PAYLOAD = 65535 - 8; // 8-byte UDP header
// But for Internet use, stay under typical MTU
const size_t SAFE_UDP_SIZE = 508; // Minimum MTU (576) - IP header (60) - UDP header (8)
// For local network, can use larger
const size_t LAN_UDP_SIZE = 1472; // 1500 MTU - 20 IP - 8 UDP
// If datagram exceeds MTU and DF (Don't Fragment) is set:
char buffer[2000];
ssize_t sent = sendto(sockfd, buffer, 2000, 0, &dest, sizeof(dest));
// May fail with EMSGSIZE, or may be fragmented (fragmentation is problematic)
Implementing Application-Level Reliability:
If your application needs some reliability on UDP:
// Example: Simple request-response with timeout
for (int attempts = 0; attempts < MAX_RETRIES; attempts++) {
// Send request
sendto(sockfd, request, request_len, 0, &server, sizeof(server));
// Wait for response with timeout
struct pollfd pfd = {sockfd, POLLIN, 0};
int ready = poll(&pfd, 1, TIMEOUT_MS);
if (ready > 0 && (pfd.revents & POLLIN)) {
ssize_t received = recvfrom(sockfd, response, response_size, 0,
NULL, NULL);
if (received > 0) {
// Success!
return received;
}
}
// Timeout or error—retry
}
// All retries exhausted
return -1;
UDP's simplicity enables high performance, but achieving that performance requires understanding several factors that affect throughput and latency.
Buffer Sizing:
UDP socket buffers determine how many datagrams can be queued before packets are dropped:
// Increase receive buffer for high-throughput applications
int recv_buf = 8 * 1024 * 1024; // 8 MB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buf, sizeof(recv_buf));
// Check what the kernel actually set (may differ)
int actual_size;
socklen_t optlen = sizeof(actual_size);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &actual_size, &optlen);
printf("Actual receive buffer: %d\n", actual_size);
// Note: Linux doubles the value and returns the doubled value
Linux doubles the buffer size you request (for bookkeeping overhead) and has system-wide limits in /proc/sys/net/core/rmem_max and /proc/sys/net/core/wmem_max. For high-performance applications, you may need to increase these system limits.
Batching with recvmmsg()/sendmmsg():
For high packet rates, system call overhead becomes significant. Linux provides batched operations:
#include <sys/socket.h>
// Receive multiple datagrams with one system call
struct mmsghdr msgs[BATCH_SIZE];
struct iovec iovecs[BATCH_SIZE];
char buffers[BATCH_SIZE][DATAGRAM_SIZE];
for (int i = 0; i < BATCH_SIZE; i++) {
iovecs[i].iov_base = buffers[i];
iovecs[i].iov_len = DATAGRAM_SIZE;
msgs[i].msg_hdr.msg_iov = &iovecs[i];
msgs[i].msg_hdr.msg_iovlen = 1;
}
int received = recvmmsg(sockfd, msgs, BATCH_SIZE, 0, NULL);
// received = number of datagrams received (up to BATCH_SIZE)
// Process each datagram
for (int i = 0; i < received; i++) {
size_t len = msgs[i].msg_len;
process_datagram(buffers[i], len);
}
Performance Comparison:
| Technique | Packets/sec (typical) | Notes |
|---|---|---|
| Individual recvfrom() | ~300K | High syscall overhead |
| recvmmsg() batched | ~1M | Amortizes syscall cost |
| DPDK/kernel bypass | ~10M+ | Bypasses kernel entirely |
Several patterns recur in UDP application design. Understanding these patterns helps you build robust UDP-based systems.
Pattern 1: Simple Request-Response (DNS-style)
// Client: Send query, wait for response
while (retries-- > 0) {
sendto(sock, query, query_len, 0, &server, sizeof(server));
struct pollfd pfd = {sock, POLLIN, 0};
if (poll(&pfd, 1, timeout_ms) > 0) {
recvfrom(sock, response, sizeof(response), 0, NULL, NULL);
return parse_response(response);
}
}
return ERROR_TIMEOUT;
// Server: Receive query, send response
while (1) {
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
ssize_t len = recvfrom(sock, buffer, sizeof(buffer), 0,
(struct sockaddr *)&client, &client_len);
response_len = process_query(buffer, len, response);
sendto(sock, response, response_len, 0,
(struct sockaddr *)&client, client_len);
}
Pattern 2: Real-Time Streaming (Audio/Video)
// Sender: Stream data at fixed rate
while (has_more_data) {
// Add sequence number and timestamp
struct packet pkt;
pkt.seq = sequence_number++;
pkt.timestamp = get_timestamp();
memcpy(pkt.data, source_data, data_len);
sendto(sock, &pkt, sizeof(pkt), 0, &dest, sizeof(dest));
// Rate limiting
usleep(1000000 / PACKETS_PER_SECOND);
}
// Receiver: Handle out-of-order and lost packets
uint32_t expected_seq = 0;
while (1) {
recvfrom(sock, &pkt, sizeof(pkt), 0, NULL, NULL);
if (pkt.seq > expected_seq) {
// Gap detected—packets lost
handle_gap(expected_seq, pkt.seq);
} else if (pkt.seq < expected_seq) {
// Old packet (duplicate or reordered)
if (pkt.seq > expected_seq - WINDOW_SIZE) {
// Recent reorder—try to use it
insert_reordered(pkt);
}
// Else: too old, discard
}
expected_seq = pkt.seq + 1;
process_packet(pkt);
}
Pattern 3: Reliable UDP (ARQ)
For applications needing UDP's properties but with reliability:
// Sender with acknowledgment
for (int seq = 0; seq < total_packets; seq++) {
int acked = 0;
int retries = 0;
while (!acked && retries < MAX_RETRIES) {
// Send packet with sequence number
pkt.seq = seq;
sendto(sock, &pkt, sizeof(pkt), 0, &dest, sizeof(dest));
// Wait for ACK
struct pollfd pfd = {sock, POLLIN, 0};
if (poll(&pfd, 1, ACK_TIMEOUT_MS) > 0) {
struct ack_pkt ack;
recvfrom(sock, &ack, sizeof(ack), 0, NULL, NULL);
if (ack.seq == seq) {
acked = 1;
}
}
retries++;
}
if (!acked) return ERROR_DELIVERY_FAILED;
}
Pattern 4: UDP Hole Punching (NAT Traversal)
Enabling peer-to-peer communication through NAT devices:
// Both peers: Coordinate via rendezvous server, then:
// 1. Both peers send packet to each other's public address
sendto(sock, "PUNCH", 5, 0, &peer_public_addr, sizeof(peer_public_addr));
// 2. This creates NAT mapping allowing peer's response
// 3. When peer's punch arrives, NAT mapping is bidirectional
// 4. Direct peer-to-peer communication now possible
UDP provides a lightweight, datagram-oriented communication service that trades reliability for speed and simplicity. Let's consolidate the essential concepts:
What's Next:
With both TCP and UDP sockets understood, we'll explore connection handling—the patterns and techniques for managing multiple concurrent connections, handling errors gracefully, and building robust networked applications.
You now understand UDP socket programming at a professional level: the connectionless model, datagram transmission, connected vs. unconnected sockets, broadcast and multicast, error handling, performance optimization, and common patterns. This knowledge enables you to build real-time, high-performance networked applications.