UNIX Domain Datagram Sockets

 

UNIX Domain Datagram Sockets
Chapter 57 โ€” Linux System Programming | EmbeddedPathashala
๐Ÿ”˜ AF_UNIX
๐Ÿ“ค SOCK_DGRAM
โš  Message Truncation

What Are UNIX Domain Sockets?

UNIX domain sockets are a way for two programs running on the same machine to talk to each other. Unlike internet sockets (which use IP addresses and ports), UNIX domain sockets use a file path on disk as their address.

There are two types: stream sockets (like a phone call โ€” reliable, ordered, connection-based) and datagram sockets (like sending a letter โ€” each message is independent). This tutorial focuses on datagram sockets using SOCK_DGRAM.

Unlike internet UDP, UNIX domain datagram sockets are reliable on the same machine. Messages are not lost or reordered between local processes.

๐Ÿ  How UNIX Domain Datagram Communication Works

Both the client and server bind to a socket file path. The server listens on a well-known path. The client binds to a unique path (often using its PID) so the server knows where to reply.

CLIENT
binds to:
/tmp/ud_ucase_cl.PID
โ†’ sendto() โ†’
(message)
โ† recvfrom() โ†
(reply)
SERVER
binds to:
/tmp/ud_ucase_sv
๐Ÿ“„ Both addresses are file paths on disk (not IP addresses)

Key Terms in This Topic

AF_UNIX SOCK_DGRAM socket() bind() sendto() recvfrom() sockaddr_un sun_path sun_family message truncation

๐Ÿ”จ Server Side โ€” Receiving and Replying

The server creates a socket, binds it to a known path, and waits for messages. When a message arrives, it knows the sender’s address (from recvfrom) and sends a reply back.

In the uppercase server example: the server reads a string, converts it to uppercase using toupper(), and sends the result back to the client.

#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define SV_SOCK_PATH "/tmp/ud_ucase_sv"
#define BUF_SIZE     10

int main(void)
{
    struct sockaddr_un svaddr, claddr;
    socklen_t len;
    char buf[BUF_SIZE];
    ssize_t numBytes;
    int sfd, j;

    /* Step 1: Create a UNIX domain datagram socket */
    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1) { perror("socket"); exit(1); }

    /* Step 2: Remove any old socket file, then bind */
    remove(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) {
        perror("bind"); exit(1);
    }

    /* Step 3: Loop - receive messages and send uppercase replies */
    for (;;) {
        len = sizeof(struct sockaddr_un);

        /* recvfrom fills claddr with the sender's address */
        numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
                            (struct sockaddr *) &claddr, &len);
        if (numBytes == -1) { perror("recvfrom"); exit(1); }

        printf("Server received %zd bytes from %s\n",
               numBytes, claddr.sun_path);

        /* Convert to uppercase in-place */
        for (j = 0; j < numBytes; j++)
            buf[j] = toupper((unsigned char) buf[j]);

        /* Send the reply back to the client using sendto */
        if (sendto(sfd, buf, numBytes, 0,
                   (struct sockaddr *) &claddr, len) != numBytes) {
            perror("sendto"); exit(1);
        }
    }

    exit(EXIT_SUCCESS);
}
๐Ÿ’ก Key Points:

  • recvfrom() fills in claddr with the sender’s address automatically.
  • The server does NOT need to bind a client address manually โ€” it learns it at runtime.
  • remove(SV_SOCK_PATH) cleans up any leftover socket file from a previous run.

๐Ÿ‘ค Client Side โ€” Sending and Receiving

The client also needs to bind() โ€” unlike TCP clients, UNIX datagram clients must bind so the server has an address to reply to. The client uses its PID in the socket path to make it unique.

#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define SV_SOCK_PATH "/tmp/ud_ucase_sv"
#define BUF_SIZE     10

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) {
        fprintf(stderr, "Usage: %s msg...\n", argv[0]);
        exit(1);
    }

    /* Step 1: Create socket */
    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1) { perror("socket"); exit(1); }

    /* Step 2: Bind to a unique path using PID */
    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) {
        perror("bind"); exit(1);
    }

    /* Step 3: Build server address */
    memset(&svaddr, 0, sizeof(struct sockaddr_un));
    svaddr.sun_family = AF_UNIX;
    strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);

    /* Step 4: Send each command-line argument to the server */
    for (j = 1; j < argc; j++) {
        msgLen = strlen(argv[j]);

        if (sendto(sfd, argv[j], msgLen, 0,
                   (struct sockaddr *) &svaddr,
                   sizeof(struct sockaddr_un)) != (ssize_t)msgLen) {
            fprintf(stderr, "sendto failed\n"); exit(1);
        }

        /* Step 5: Receive server's response */
        numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
        if (numBytes == -1) { perror("recvfrom"); exit(1); }

        printf("Response %d: %.*s\n", j, (int) numBytes, resp);
    }

    /* Step 6: Remove the client's socket file when done */
    remove(claddr.sun_path);
    exit(EXIT_SUCCESS);
}
โš  Why must the client call bind()?
In UNIX domain datagram sockets, if a client does NOT bind, it has no address. The server receives the message but cannot reply because it does not know where to send the response. The client binds to a unique path using its PID to avoid conflicts when multiple clients run simultaneously.

โœ‚ Message Truncation โ€” The Silent Danger

When you call recvfrom(), you give it a buffer of a fixed size. If the incoming message is larger than your buffer, the extra bytes are silently discarded. No error is returned. This is called message truncation.

Example: BUF_SIZE = 10, message = “long message” (12 bytes)
l o n g ย  m e s s a g e
โ–ฎ Received (fits in buffer) = “long messa” (10 bytes) โ–ฎ Silently discarded = “ge” (2 bytes lost)

The actual output from the TLPI example confirms this:

$ ./ud_ucase_cl 'long message'
Server received 10 bytes from /tmp/ud_ucase_cl.20151
Response 1: LONG MESSA

The server only got 10 bytes even though the client sent 12. The two extra bytes (ge) were thrown away without any warning.

๐Ÿ›  Best Practice: Always make your receive buffer large enough for the largest possible message. Use MSG_TRUNC flag with recvfrom() on Linux to detect truncation (the return value will be the original message size, not the truncated size).

๐Ÿ“„ Complete Flow: Client-Server Exchange

CLIENT

1. socket(AF_UNIX, SOCK_DGRAM, 0)
2. bind() to /tmp/cl.PID
3. sendto(server_path, “hello”)
โ†“
6. recvfrom() โ† “HELLO”
7. remove(/tmp/cl.PID)

โ‡„
SERVER

1. socket(AF_UNIX, SOCK_DGRAM, 0)
2. remove + bind() to /tmp/sv
3. recvfrom() โ† “hello”, client addr
4. toupper(“hello”) = “HELLO”
5. sendto(client_addr, “HELLO”)
(loops forever)

โ–ถ Running the Example
$ gcc -o ud_ucase_sv ud_ucase_sv.c
$ gcc -o ud_ucase_cl ud_ucase_cl.c

# Start server in background
$ ./ud_ucase_sv &
[1] 20113

# Client sends two words
$ ./ud_ucase_cl hello world
Server received 5 bytes from /tmp/ud_ucase_cl.20150
Response 1: HELLO
Server received 5 bytes from /tmp/ud_ucase_cl.20150
Response 2: WORLD

# Send a message longer than BUF_SIZE (10 bytes)
$ ./ud_ucase_cl 'long message'
Server received 10 bytes from /tmp/ud_ucase_cl.20151
Response 1: LONG MESSA      <-- truncated!

# Stop the server
$ kill %1

๐ŸŽ“ Interview Questions & Answers
Q1. What is the difference between AF_UNIX and AF_INET sockets?

A: AF_UNIX (also called AF_LOCAL) sockets communicate only between processes on the same machine using file system paths as addresses. AF_INET sockets use IP addresses and ports for network communication. UNIX domain sockets are faster and have lower overhead because they bypass the network stack entirely.

Q2. Why must a UNIX domain datagram client call bind() even though it’s not a server?

A: When the client sends a message, the server needs a return address to send the reply. If the client has not called bind(), it has no named path in the file system. The server cannot reply because it has no destination address. By calling bind() with a unique path (like /tmp/cl.PID), the client creates an address the server can use.

Q3. What happens when recvfrom() buffer is smaller than the incoming message in UNIX domain SOCK_DGRAM?

A: The excess bytes are silently truncated. No error is returned. The call succeeds and returns the buffer size (not the original message size). This is different from TCP streams where partial reads are normal. To detect truncation on Linux, use the MSG_TRUNC flag in recvfrom().

Q4. Are UNIX domain datagram sockets reliable?

A: Yes. Unlike UDP over the network, UNIX domain datagram sockets are reliable on the local machine. Messages are not dropped or reordered in normal operating conditions. However, if the receiver’s socket buffer is full, the sendto() call will block.

Q5. How does the server identify which client sent a message?

A: The recvfrom() call fills in the src_addr parameter (a struct sockaddr_un) with the sender’s bound path. The server uses this address in a subsequent sendto() call to send the reply back. This is why the client must have a bound address.

Q6. What is sockaddr_un and what are its important fields?

A: struct sockaddr_un is the address structure for UNIX domain sockets.
Key fields:
โ€ข sun_family: Always set to AF_UNIX
โ€ข sun_path: The file system path (up to 108 bytes on Linux, including null terminator)

struct sockaddr_un {
    sa_family_t sun_family;   /* AF_UNIX */
    char        sun_path[108]; /* socket file path */
};
Q7. Why does the client call remove() at the end?

A: The bind() call creates an actual file in the file system. This file persists even after the process exits. If a new client runs with the same PID (which can happen if the OS reuses PIDs), the bind() will fail because the file already exists. Calling remove(claddr.sun_path) cleans up the socket file when done.

Continue Learning

Next: UNIX Domain Socket Permissions โ€” controlling access with file system permissions

Next: Socket Permissions โ†’ Back to EmbeddedPathashala

Leave a Reply

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