Datagram Sockets: Message-Oriented IPC
Datagram sockets work differently from stream sockets. Instead of a continuous byte stream, each sendto() call sends a self-contained message, and each recvfrom() call receives exactly one message. Message boundaries are preserved โ if you send a 50-byte message, you receive exactly 50 bytes in one call.
For UNIX domain (local) datagram sockets, messages are reliable and ordered โ they are never lost or reordered. This is quite different from UDP over a network, where packets can be dropped, duplicated, or arrive out of order.
A key practical difference from stream sockets: since there is no connection, the client usually needs to bind to a path too so the server knows where to send its replies. Without a bound address, the server cannot reply to the client.
Key Terms
| Aspect | SOCK_STREAM | SOCK_DGRAM |
|---|---|---|
| Connection required | Yes (connect + accept) | No |
| Message boundaries | None (byte stream) | Preserved |
| Send function | write() / send() | sendto() |
| Receive function | read() / recv() | recvfrom() |
| Client must bind? | No | Yes (if expecting replies) |
| Reliable (UNIX domain) | Yes | Yes (unlike UDP) |
| listen() needed? | Yes | No |
| Oversized message | Split across reads | Truncated (MSG_TRUNC) |
#include <sys/socket.h>
/* Send a datagram to a specific address */
ssize_t sendto(int sockfd,
const void *buf, size_t len,
int flags,
const struct sockaddr *dest_addr,
socklen_t addrlen);
/* Receive a datagram, also getting sender's address */
ssize_t recvfrom(int sockfd,
void *buf, size_t len,
int flags,
struct sockaddr *src_addr, /* Filled with sender's address */
socklen_t *addrlen); /* Set to length of address */
/* flags is usually 0 */
/* For recvfrom, if src_addr is NULL, sender's address is discarded */
Important: If the buffer provided to recvfrom() is smaller than the incoming message, the excess bytes are silently discarded (the return value reflects the original message size, and MSG_TRUNC is set in msg_flags if you use recvmsg()). Always make your receive buffer large enough.
This example shows a simple request-reply protocol. The client sends a message and receives a response. Note how both sides bind to a path.
Server (ud_sv.c):
/* ud_sv.c โ UNIX domain datagram socket server
* Receives messages from clients and sends back an acknowledgment.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SV_SOCK_PATH "/tmp/ud_sv"
#define BUF_SIZE 256
int main(void)
{
struct sockaddr_un sv_addr, cl_addr;
socklen_t cl_addrlen;
int sfd;
ssize_t n;
char buf[BUF_SIZE];
char reply[BUF_SIZE];
/* Step 1: Create datagram socket */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Step 2: Remove old socket file and bind */
remove(SV_SOCK_PATH);
memset(&sv_addr, 0, sizeof(sv_addr));
sv_addr.sun_family = AF_UNIX;
strncpy(sv_addr.sun_path, SV_SOCK_PATH, sizeof(sv_addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *)&sv_addr, SUN_LEN(&sv_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
printf("Server ready at %s\n", SV_SOCK_PATH);
/* Step 3: Receive messages in a loop */
for (;;) {
cl_addrlen = sizeof(cl_addr);
memset(&cl_addr, 0, sizeof(cl_addr));
/* recvfrom() blocks until a message arrives */
n = recvfrom(sfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr *)&cl_addr, &cl_addrlen);
if (n == -1) {
perror("recvfrom");
exit(EXIT_FAILURE);
}
buf[n] = '\0';
printf("Received %zd bytes from '%s': %s\n",
n,
(cl_addrlen > sizeof(sa_family_t)) ? cl_addr.sun_path : "(unnamed)",
buf);
/* Send an acknowledgment back to the client */
snprintf(reply, sizeof(reply), "ACK: received %zd bytes", n);
/* We can only reply if client gave us their address */
if (cl_addrlen > sizeof(sa_family_t)) {
if (sendto(sfd, reply, strlen(reply), 0,
(struct sockaddr *)&cl_addr, cl_addrlen) == -1) {
perror("sendto (reply)");
}
}
}
remove(SV_SOCK_PATH);
close(sfd);
return 0;
}
Client (ud_cl.c):
/* ud_cl.c โ UNIX domain datagram socket client
* Sends a message to the server and waits for a reply.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SV_SOCK_PATH "/tmp/ud_sv"
#define CL_SOCK_PATH "/tmp/ud_cl" /* Client's own path for receiving replies */
#define BUF_SIZE 256
int main(int argc, char *argv[])
{
struct sockaddr_un sv_addr, cl_addr;
int sfd;
ssize_t n;
char buf[BUF_SIZE];
const char *msg;
if (argc < 2) {
fprintf(stderr, "Usage: %s <message>\n", argv[0]);
exit(EXIT_FAILURE);
}
msg = argv[1];
/* Step 1: Create datagram socket */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Step 2: Bind to OUR path so server can reply to us */
remove(CL_SOCK_PATH);
memset(&cl_addr, 0, sizeof(cl_addr));
cl_addr.sun_family = AF_UNIX;
strncpy(cl_addr.sun_path, CL_SOCK_PATH, sizeof(cl_addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *)&cl_addr, SUN_LEN(&cl_addr)) == -1) {
perror("bind (client)");
exit(EXIT_FAILURE);
}
/* Step 3: Build server address */
memset(&sv_addr, 0, sizeof(sv_addr));
sv_addr.sun_family = AF_UNIX;
strncpy(sv_addr.sun_path, SV_SOCK_PATH, sizeof(sv_addr.sun_path) - 1);
/* Step 4: Send message to server */
if (sendto(sfd, msg, strlen(msg), 0,
(struct sockaddr *)&sv_addr, SUN_LEN(&sv_addr)) == -1) {
perror("sendto");
exit(EXIT_FAILURE);
}
printf("Sent: %s\n", msg);
/* Step 5: Wait for reply from server */
n = recvfrom(sfd, buf, BUF_SIZE - 1, 0, NULL, NULL);
if (n == -1) {
perror("recvfrom");
exit(EXIT_FAILURE);
}
buf[n] = '\0';
printf("Reply: %s\n", buf);
remove(CL_SOCK_PATH);
close(sfd);
return 0;
}
To test:
# Terminal 1
./ud_sv
# Terminal 2
./ud_cl "Hello Server"
./ud_cl "Testing 1 2 3"
One of the most important properties of UNIX domain datagram sockets is that they are reliable. If the receiver’s socket buffer fills up (because the receiver is reading too slowly), the sender blocks instead of losing the message.
โธ BLOCKS (buffer full)
buffer
(reading slowly)
This blocking behavior is what makes UNIX domain datagrams reliable. In contrast, UDP datagrams over a network would simply be dropped when the buffer is full.
You can demonstrate this behavior with the following program sketch:
/* Sender that fills the receiver's buffer (Exercise 57-1) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#define SV_PATH "/tmp/dg_sv"
#define MSG_SIZE 100
int main(void)
{
int sfd;
struct sockaddr_un sv_addr;
char buf[MSG_SIZE];
long count = 0;
struct timespec ts;
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) { perror("socket"); exit(EXIT_FAILURE); }
memset(&sv_addr, 0, sizeof(sv_addr));
sv_addr.sun_family = AF_UNIX;
strncpy(sv_addr.sun_path, SV_PATH, sizeof(sv_addr.sun_path) - 1);
memset(buf, 'X', MSG_SIZE);
printf("Sending as fast as possible...\n");
for (;;) {
/* sendto() will BLOCK when receiver's buffer is full */
if (sendto(sfd, buf, MSG_SIZE, 0,
(struct sockaddr *)&sv_addr, SUN_LEN(&sv_addr)) == -1) {
perror("sendto");
exit(EXIT_FAILURE);
}
count++;
if (count % 1000 == 0) {
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("Sent %ld messages at t=%ld.%03ld\n",
count, ts.tv_sec, ts.tv_nsec / 1000000);
}
}
}
You can call connect() on a datagram socket to set a default destination. After that, you can use write() / send() instead of sendto(), and only datagrams from that one peer will be received by read() / recv().
/* Connect a datagram socket to a fixed peer */
struct sockaddr_un peer_addr;
memset(&peer_addr, 0, sizeof(peer_addr));
peer_addr.sun_family = AF_UNIX;
strncpy(peer_addr.sun_path, "/tmp/peer_sock", sizeof(peer_addr.sun_path) - 1);
/* After connect(), we can use write() and read() */
if (connect(sfd, (struct sockaddr *)&peer_addr, SUN_LEN(&peer_addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}
/* Now use write() instead of sendto() */
write(sfd, "hello", 5);
/* And read() instead of recvfrom() */
char buf[100];
read(sfd, buf, sizeof(buf));
/* To disconnect (remove the default destination), connect to a UNIX socket
* with sun_family = AF_UNSPEC */
struct sockaddr unspec_addr;
unspec_addr.sa_family = AF_UNSPEC;
connect(sfd, &unspec_addr, sizeof(sa_family_t));
Exercise 57-4 concept: If you connect datagram socket A to socket B, and then a third socket C tries to sendto() socket A, what happens? Answer: The kernel rejects it with ECONNREFUSED because A is connected and will only accept datagrams from B.
Because datagram sockets are connectionless. When the client sends a message to the server, the server receives it but does not know where to send a reply unless the client has a known address. By binding to a path (e.g., /tmp/ud_cl), the client’s address appears in the src_addr output of the server’s recvfrom() call. Without binding, the client has no address, and the server cannot reply to it.
The excess bytes are silently discarded. Unlike stream sockets where unread bytes remain available for the next read, datagram messages are atomic โ you either receive the whole message or, if your buffer is too small, the rest is thrown away. To detect this, use recvmsg() with the MSG_TRUNC flag โ the return value will be the original (full) message size, letting you detect truncation.
UNIX domain datagrams are reliable. They are handled entirely inside the Linux kernel using in-memory buffers. Messages are never dropped, duplicated, or reordered. If the receiver’s buffer is full, the sender blocks. UDP over a network is unreliable โ packets can be lost due to network congestion, router buffers filling up, or network errors. UDP has no blocking mechanism; excess packets are simply dropped by the network.
ENOENT โ “No such file or directory”. The kernel tries to look up the path in the file system and fails because no socket file exists there. This is different from a TCP connection refused error. For UNIX domain sockets, if the server has exited and removed its socket file, the client gets ENOENT, not ECONNREFUSED.
The kernel sends back an error to C. Specifically, if C is also a UNIX domain socket, C’s sendto() fails with ECONNREFUSED. This is the behavior described in Exercise 57-4 of TLPI. A connected datagram socket only accepts messages from the address it connected to; all other senders are rejected.
Yes, but only if the socket is connected (after calling connect()). write() is equivalent to sendto() with the connected peer as destination. read() is equivalent to recvfrom() but discards the sender address. For unconnected datagram sockets, you must use sendto() to specify the destination, and recvfrom() to discover the sender’s address.
Continue Learning
โ Part 2: Stream Sockets Next: Permissions & Credentials โ
