In networking, datagram sockets (UDP) are unreliable โ packets can be lost, reordered, or duplicated. But when you use datagram sockets inside the UNIX domain (AF_UNIX), the rules change. Everything stays inside the Linux kernel, so messages are delivered reliably, in order, and without duplication. You get the simple connectionless API of datagrams with the reliability of streams.
This tutorial covers the theory of UNIX domain datagram sockets and walks through a full uppercase-conversion server and client example, showing exactly how recvfrom() and sendto() work.
UNIX Domain Datagrams Are Reliable
The critical thing to understand upfront: UNIX domain datagram sockets are reliable. This surprises many people because UDP datagrams over a network are not.
Why is this so? Because both sender and receiver are on the same machine. The kernel copies the message directly from the sender’s buffer to the receiver’s socket receive buffer. There is no network path, no routing, no packet loss risk. The kernel either delivers the message or returns an error to the sender.
If the receiver’s socket receive buffer is full (the process is not calling recvfrom() fast enough), new datagrams may be silently dropped by the kernel. This is the one case where a UNIX domain datagram can be lost. In practice this is rare, but it is something to be aware of in high-throughput applications.
Maximum Datagram Size
POSIX (SUSv3) does not define a maximum datagram size for UNIX domain sockets. Linux allows quite large datagrams. The limits are controlled by:
/* Check / change send buffer size with SO_SNDBUF */
int sndbuf_size;
socklen_t optlen = sizeof(sndbuf_size);
/* Read current limit */
getsockopt(sfd, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, &optlen);
printf("Send buffer size: %d bytes\n", sndbuf_size);
/* Set a new limit (kernel may round up to next allowed value) */
sndbuf_size = 65536;
setsockopt(sfd, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, sizeof(sndbuf_size));
You can also inspect and change defaults via the /proc filesystem:
# View default socket receive buffer size (bytes)
cat /proc/sys/net/core/rmem_default
# View maximum allowed receive buffer size
cat /proc/sys/net/core/rmem_max
# Temporarily increase the max (as root)
echo 262144 > /proc/sys/net/core/rmem_max
Some non-Linux UNIX implementations limit UNIX domain datagrams to as little as 2048 bytes. If you want your application to run on other UNIX systems (FreeBSD, macOS, Solaris), keep individual datagram sizes well below this limit. Design your protocol so that messages fit in a small, conservative maximum.
Stream vs Datagram โ Key Differences
| Aspect | SOCK_STREAM (TCP-like) | SOCK_DGRAM (UDP-like) |
|---|---|---|
| Connection | Required โ connect() + accept() | Not required โ no connect() |
| Message boundaries | No โ data is a byte stream | Yes โ each send/recv is one message |
| Server bind() | Required | Required |
| Client bind() | Optional (kernel autobinds) | Required if server needs to reply |
| Send / Receive API | read() / write() | recvfrom() / sendto() |
| listen() / accept() | Required on server | Not used |
The biggest practical difference: in a datagram exchange, the client must bind to a path so the server knows where to send its reply. In the stream model, the server gets a connected socket from accept() and can write back on it without knowing the client’s address.
Why the Datagram Client Must Call bind()
/tmp/ud_ucase_cl.PID
from recvfrom()
/tmp/ud_ucase
When the server calls recvfrom(), it gets the message and the sender’s address (stored in the claddr structure). The server then uses this address in sendto() to send the reply. For this to work, the client’s socket must be bound to a pathname โ otherwise the kernel has no address to report to the server.
The client makes its address unique by including its process ID in the pathname:
/* Client: create a unique pathname using PID */
snprintf(claddr.sun_path, sizeof(claddr.sun_path),
"/tmp/ud_ucase_cl.%ld", (long) getpid());
Using the PID ensures no two clients accidentally use the same address, even if several clients run at the same time.
The Shared Header (ud_ucase.h)
Both server and client share a header that defines the buffer size and the server’s well-known socket pathname:
/* ud_ucase.h */
#include <sys/un.h>
#include <sys/socket.h>
#include <ctype.h>
#include "tlpi_hdr.h"
/* Small buffer โ in practice, larger messages can be used on Linux */
#define BUF_SIZE 10
/* Server's well-known address โ clients must know this */
#define SV_SOCK_PATH "/tmp/ud_ucase"
BUF_SIZE = 10 is intentionally tiny here for demonstration. Real applications would use something like 1024 or 4096, keeping portability concerns in mind.
The Datagram Server (ud_ucase_sv.c)
The server creates a socket, binds it to a well-known path, then loops calling recvfrom(). Each call gives it one complete message plus the sender’s address. It converts the text to uppercase and sends it back with sendto().
/* ud_ucase_sv.c โ UNIX domain datagram server (uppercase converter) */
#include "ud_ucase.h"
int
main(int argc, char *argv[])
{
struct sockaddr_un svaddr, claddr;
int sfd, j;
ssize_t numBytes;
socklen_t len;
char buf[BUF_SIZE];
/* 1. Create DGRAM socket */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1)
errExit("socket");
/* 2. Clean up any old socket file, then bind to well-known address */
if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT)
errExit("remove-%s", SV_SOCK_PATH);
memset(&svaddr, 0, sizeof(struct sockaddr_un));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &svaddr, sizeof(struct sockaddr_un)) == -1)
errExit("bind");
/* 3. Receive-process-reply loop (no listen/accept needed for datagrams) */
for (;;) {
len = sizeof(struct sockaddr_un);
/*
* recvfrom() blocks until a datagram arrives.
* It fills claddr with the SENDER's address so we can reply.
* len is updated to the actual size of the sender's address.
*/
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numBytes == -1)
errExit("recvfrom");
printf("Server received %ld bytes from %s\n",
(long) numBytes, claddr.sun_path);
/* Convert received text to uppercase */
for (j = 0; j < numBytes; j++)
buf[j] = toupper((unsigned char) buf[j]);
/*
* sendto() sends reply to the client's address (claddr).
* This works because the client bound its socket to a pathname.
*/
if (sendto(sfd, buf, numBytes, 0,
(struct sockaddr *) &claddr, len) != numBytes)
fatal("sendto");
}
}
No listen() or accept(): Datagram servers do not need these calls. There is no connection to establish. The server just sits and waits for datagrams via recvfrom().
recvfrom() gives the sender’s address: The last two parameters (&claddr and &len) are output parameters. After the call, claddr holds the client’s socket address. Without this, the server would have no way to reply.
toupper() cast: The argument is cast to unsigned char. This is important because toupper() is defined for values in the range of unsigned char or the value EOF. Passing a plain char (which can be negative) directly is technically undefined behavior.
The Datagram Client (ud_ucase_cl.c)
The client creates a socket, binds it to a unique path, then for each command-line argument: sends the argument text to the server and reads the uppercase reply.
/* ud_ucase_cl.c โ UNIX domain datagram client (uppercase conversion) */
#include "ud_ucase.h"
int
main(int argc, char *argv[])
{
struct sockaddr_un svaddr, claddr;
int sfd, j;
size_t msgLen;
ssize_t numBytes;
char resp[BUF_SIZE];
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s msg...\n", argv[0]);
/* 1. Create DGRAM socket */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1)
errExit("socket");
/*
* 2. Bind the CLIENT socket to a unique path.
* Without this, the server cannot reply because it would have
* no address to send the response to.
* We use the PID to make the path unique across multiple clients.
*/
memset(&claddr, 0, sizeof(struct sockaddr_un));
claddr.sun_family = AF_UNIX;
snprintf(claddr.sun_path, sizeof(claddr.sun_path),
"/tmp/ud_ucase_cl.%ld", (long) getpid());
if (bind(sfd, (struct sockaddr *) &claddr, sizeof(struct sockaddr_un)) == -1)
errExit("bind");
/* 3. Build the server's address (its well-known path) */
memset(&svaddr, 0, sizeof(struct sockaddr_un));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);
/* 4. Loop over command-line arguments: send each as a separate message */
for (j = 1; j < argc; j++) {
msgLen = strlen(argv[j]);
/*
* sendto() sends one datagram to the server.
* Each call sends exactly one complete message.
*/
if (sendto(sfd, argv[j], msgLen, 0,
(struct sockaddr *) &svaddr,
sizeof(struct sockaddr_un)) != (ssize_t) msgLen)
fatal("sendto");
/* 5. Wait for the server's uppercase reply */
numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
if (numBytes == -1)
errExit("recvfrom");
printf("Response %d: %.*s\n", j, (int) numBytes, resp);
}
/* 6. Remove our socket file before exiting */
remove(claddr.sun_path);
exit(EXIT_SUCCESS);
}
The client calls remove(claddr.sun_path) before exiting. Unlike a stream socket client (which doesn’t bind and leaves no file), this client created a file at /tmp/ud_ucase_cl.<PID> when it called bind(). Failing to remove it leaves a stale file on the filesystem. Since the PID is unique per run, the file name changes each time โ but it is still good practice to clean up.
Complete Self-Contained Demo (No TLPI Library)
Here is a complete working example you can compile without the TLPI helper library. The server converts incoming messages to uppercase; the client sends words and prints responses.
dgram_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define SV_PATH "/tmp/dgram_sv"
#define BUF_SIZE 256
int main(void)
{
int sfd;
struct sockaddr_un svaddr, claddr;
char buf[BUF_SIZE];
ssize_t n;
socklen_t len;
int i;
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) { perror("socket"); exit(1); }
if (remove(SV_PATH) == -1 && errno != ENOENT)
{ perror("remove"); exit(1); }
memset(&svaddr, 0, sizeof(svaddr));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_PATH, sizeof(svaddr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *)&svaddr, sizeof(svaddr)) == -1)
{ perror("bind"); exit(1); }
printf("Datagram server ready at %s\n", SV_PATH);
for (;;) {
len = sizeof(claddr);
n = recvfrom(sfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr *)&claddr, &len);
if (n == -1) { perror("recvfrom"); continue; }
buf[n] = '\0';
printf("Got from %s: %s\n", claddr.sun_path, buf);
/* Convert to uppercase in place */
for (i = 0; i < n; i++)
buf[i] = toupper((unsigned char) buf[i]);
/* Send reply back to the client */
if (sendto(sfd, buf, n, 0,
(struct sockaddr *)&claddr, len) != n)
perror("sendto");
}
close(sfd);
remove(SV_PATH);
return 0;
}
dgram_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define SV_PATH "/tmp/dgram_sv"
#define BUF_SIZE 256
int main(int argc, char *argv[])
{
int sfd, i;
struct sockaddr_un svaddr, claddr;
char clpath[108];
char resp[BUF_SIZE];
ssize_t n;
if (argc < 2) {
fprintf(stderr, "Usage: %s word1 word2 ...\n", argv[0]);
exit(1);
}
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) { perror("socket"); exit(1); }
/* Bind client to unique path using PID */
snprintf(clpath, sizeof(clpath), "/tmp/dgram_cl.%ld", (long)getpid());
memset(&claddr, 0, sizeof(claddr));
claddr.sun_family = AF_UNIX;
strncpy(claddr.sun_path, clpath, sizeof(claddr.sun_path) - 1);
if (remove(clpath) == -1 && errno != ENOENT) { perror("remove"); exit(1); }
if (bind(sfd, (struct sockaddr *)&claddr, sizeof(claddr)) == -1)
{ perror("bind"); exit(1); }
/* Build server address */
memset(&svaddr, 0, sizeof(svaddr));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_PATH, sizeof(svaddr.sun_path) - 1);
for (i = 1; i < argc; i++) {
size_t len = strlen(argv[i]);
/* Send message to server */
if (sendto(sfd, argv[i], len, 0,
(struct sockaddr *)&svaddr, sizeof(svaddr)) != (ssize_t)len) {
perror("sendto"); continue;
}
/* Read the uppercase response */
n = recvfrom(sfd, resp, BUF_SIZE - 1, 0, NULL, NULL);
if (n == -1) { perror("recvfrom"); continue; }
resp[n] = '\0';
printf("Sent: %-15s Reply: %s\n", argv[i], resp);
}
remove(clpath);
close(sfd);
return 0;
}
/* Compile & run:
gcc -o dgram_server dgram_server.c
gcc -o dgram_client dgram_client.c
Terminal 1: ./dgram_server
Terminal 2: ./dgram_client hello world embedded linux
*/
Expected output in Terminal 2:
Sent: hello Reply: HELLO
Sent: world Reply: WORLD
Sent: embedded Reply: EMBEDDED
Sent: linux Reply: LINUX
API Reference: recvfrom() and sendto()
#include <sys/socket.h>
/*
* recvfrom() โ receive one datagram and get sender's address
*
* sockfd โ your bound socket
* buf โ buffer to store received data
* len โ size of buf
* flags โ usually 0
* src_addr โ OUTPUT: filled with sender's address after the call
* addrlen โ INPUT: size of src_addr struct; OUTPUT: actual size filled
*
* Returns: number of bytes received, or -1 on error
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
/*
* sendto() โ send one datagram to a specific address
*
* sockfd โ your socket
* buf โ data to send
* len โ number of bytes to send
* flags โ usually 0
* dest_addr โ destination address
* addrlen โ size of dest_addr
*
* Returns: number of bytes sent, or -1 on error
* For datagrams, returns == len on success (all or nothing)
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
Unlike stream sockets where write() might send fewer bytes than requested (partial writes), sendto() for a UNIX domain datagram either sends the entire message or returns an error. There is no such thing as a partial datagram send. This is why in the server code, we compare the return value of sendto() directly with numBytes and treat any mismatch as a fatal error.
Interview Questions
Yes. Unlike UDP datagrams sent over a network, UNIX domain datagrams stay entirely within the kernel. The kernel copies data directly between socket buffers. Messages are delivered in order, without duplication, and without loss โ unless the receiver’s socket buffer overflows, in which case a datagram may be silently dropped.
A stream server gets a connected socket from accept(). It can write back to the client on that connected socket without knowing the client’s address. A datagram server uses recvfrom() to get the sender’s address, then sends the reply to that address using sendto(). If the client did not bind to a pathname, recvfrom() on the server side would receive an empty or unnamed address, and sendto() would have no valid destination for the reply.
Each client binds to a unique pathname (typically including its PID, like /tmp/ud_ucase_cl.1234). When the server calls recvfrom(), it receives the client’s bound pathname as the source address. The server uses this address in sendto() to send the reply to the right client. If two clients had the same address, they would receive each other’s replies.
POSIX does not define a maximum. On Linux, the limit is controlled by the SO_SNDBUF socket option and kernel parameters in /proc/sys/net/core/. However, some UNIX implementations limit datagrams to 2048 bytes. For portable code, keep individual message sizes well below this limit.
No. listen() and accept() are only used for stream (connection-oriented) sockets. A datagram server simply calls socket(), bind(), and then enters a loop calling recvfrom(). Each recvfrom() call retrieves one complete datagram from any client.
On the server side, recvfrom() will read only up to BUF_SIZE bytes. The remaining bytes of that datagram are silently discarded โ they are not held for the next recvfrom() call. This is a fundamental property of datagram sockets: if the receive buffer is smaller than the incoming message, the excess is lost. This is why both sides must agree on a maximum message size.
The toupper() function from <ctype.h> is defined only for values in the range of unsigned char (0 to UCHAR_MAX) plus the special value EOF (-1). On platforms where char is signed, characters with values above 127 (e.g., accented characters) would be negative, causing undefined behavior when passed directly to toupper(). Casting to unsigned char first is the correct, portable approach.
When you pass NULL for src_addr and NULL for addrlen, recvfrom() still receives the datagram but does not report the sender’s address. This is fine when you do not need to reply or when you already know who sent the message. The client in the example passes NULL to recvfrom() because it is only reading the server’s reply โ it does not need the server’s address (it already knows it from SV_SOCK_PATH).
Datagrams are better when: (1) you need to preserve message boundaries โ each send is one discrete unit; (2) you have a simple request-reply pattern with short messages; (3) you want to avoid the overhead of connection setup (no connect()/accept()); (4) a server needs to handle messages from many clients without maintaining per-client connection state. Streams are better for large data transfers or when you need flow control.
Explore more IPC mechanisms โ pipes, FIFOs, System V and POSIX message queues, shared memory, and semaphores.
