UNIX Domain Datagram Sockets Message-based IPC with SOCK_DGRAM

 

UNIX Domain Datagram Sockets
Part 3 of 6 โ€” Message-based IPC with SOCK_DGRAM
๐Ÿ“‚ File 3 of 6
๐Ÿ“จ SOCK_DGRAM
๐Ÿ“– TLPI ยง57.4

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

sendto() recvfrom() message boundary connectionless ENOENT ECONNREFUSED blocking sender MSG_TRUNC

Datagram vs Stream: Key Differences
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)

The sendto() and recvfrom() Calls
#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.

Complete Datagram Example: Server and Client

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"

Reliability: Why Senders Block When Receivers Are Slow

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.

Sender blocking scenario:

Sender
sendto() msg 1 โœ…
sendto() msg 2 โœ…
sendto() msg N โœ…
sendto() msg N+1
โธ BLOCKS (buffer full)
sendto() resumes when receiver reads
โ†’โ†’โ†’
kernel
buffer
Receiver
๐Ÿ—ƒ๏ธ Buffer fills up
(reading slowly)
recvfrom() drains buffer โ†’ sender unblocks

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);
        }
    }
}

Connecting a Datagram Socket

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.

๐ŸŽฏ Interview Questions โ€” Datagram Sockets
Q1. Why does a UNIX domain datagram client usually need to bind() to its own path?

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.

Q2. What happens if you call recvfrom() with a buffer smaller than the incoming message?

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.

Q3. What is the difference between UNIX domain datagrams and UDP in terms of reliability?

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.

Q4. What error do you get when you sendto() a UNIX domain socket path that does not exist?

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.

Q5. If you connect() a UNIX domain datagram socket to peer B, what happens when a different peer C tries to send a message to this socket?

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.

Q6. Can you use read() and write() instead of recvfrom() and sendto() on a datagram socket?

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.

Leave a Reply

Your email address will not be published. Required fields are marked *