UNIX Domain Stream Sockets Connection-oriented IPC with SOCK_STREAM

 

UNIX Domain Stream Sockets
Part 2 of 6 โ€” Connection-oriented IPC with SOCK_STREAM
๐Ÿ“‚ File 2 of 6
๐Ÿ”Œ SOCK_STREAM
๐Ÿ“– TLPI ยง57.3

How Stream Sockets Work

UNIX domain stream sockets work exactly like a TCP connection โ€” but stay entirely inside the kernel of one machine. They provide a byte-stream channel: you write bytes at one end, they come out at the other end in the same order, with no boundaries. One write() of 100 bytes might be read as a single 100-byte chunk, or as 4 smaller reads โ€” the stream gives no guarantees about chunk sizes.

The server follows the classic socket โ†’ bind โ†’ listen โ†’ accept pattern. The client does socket โ†’ connect. After that, both sides communicate using the normal read() / write() calls (or send() / recv()).

Key Terms

listen() accept() connect() backlog byte stream half-close shutdown() ECONNREFUSED concurrent server

The Server-Side API Calls

socket(AF_UNIX, SOCK_STREAM, 0)
โ†“ Returns fd (listening socket)
bind(fd, path)
โ†“ Creates socket file in FS
listen(fd, backlog)
โ†“ Marks socket as passive
accept(fd, &client_addr, &len)
โ†“ Returns new fd (connected socket)
Blocks until client connects

After accept() returns, the server has two file descriptors: the original listening socket (used to accept more connections) and the new connected socket (used to communicate with this specific client).

/* Key system call signatures for reference */
#include <sys/socket.h>

/* Mark socket as passive (listening) */
/* backlog = max pending connections in queue */
int listen(int sockfd, int backlog);

/* Accept next pending connection */
/* Returns new fd for the connection */
/* peer_addr and addrlen receive the client's address */
int accept(int sockfd,
           struct sockaddr *peer_addr,
           socklen_t *addrlen);

For UNIX domain sockets, the peer_addr from accept() gives you the client’s socket path โ€” but only if the client called bind(). If the client did not bind (which is common), sun_path[0] will be '\0' and addrlen will equal sizeof(sa_family_t).

Complete Server Example (us_xfr_sv.c)

This server receives bytes from clients and writes them to stdout. It serves one client at a time (iterative server). This is equivalent to the example from TLPI Listing 57-3.

/* us_xfr_sv.c โ€” UNIX domain stream socket server
 * Reads data from a client and writes it to stdout.
 * Usage: ./us_xfr_sv
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SV_SOCK_PATH "/tmp/us_xfr"
#define BUF_SIZE     256
#define BACKLOG      5

int main(void)
{
    struct sockaddr_un addr;
    int                sfd, cfd;
    ssize_t            num_read;
    char               buf[BUF_SIZE];

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

    /* Step 2: Remove old socket file if it exists */
    if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT) {
        perror("remove");
        exit(EXIT_FAILURE);
    }

    /* Step 3: Build the address and bind */
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    /* Step 4: Mark socket as passive */
    if (listen(sfd, BACKLOG) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on %s\n", SV_SOCK_PATH);

    /* Step 5: Accept connections in a loop */
    for (;;) {
        /* accept() blocks until a client connects */
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1) {
            perror("accept");
            exit(EXIT_FAILURE);
        }

        printf("Client connected\n");

        /* Step 6: Read all data from client and write to stdout */
        while ((num_read = read(cfd, buf, BUF_SIZE)) > 0) {
            if (write(STDOUT_FILENO, buf, num_read) != num_read) {
                fprintf(stderr, "write error\n");
                exit(EXIT_FAILURE);
            }
        }

        if (num_read == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        /* Step 7: Close the connected socket (client disconnected) */
        printf("\nClient disconnected\n");
        if (close(cfd) == -1) {
            perror("close");
            exit(EXIT_FAILURE);
        }
    }

    /* Never reached in this example, but good practice: */
    unlink(SV_SOCK_PATH);
    return 0;
}

Complete Client Example (us_xfr_cl.c)

The client reads from stdin and sends the data to the server. This matches TLPI Listing 57-4.

/* us_xfr_cl.c โ€” UNIX domain stream socket client
 * Reads from stdin and sends to the server.
 * Usage: ./us_xfr_cl < some_file.txt
 *    or: echo "hello" | ./us_xfr_cl
 */
#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/us_xfr"
#define BUF_SIZE     256

int main(void)
{
    struct sockaddr_un addr;
    int                sfd;
    ssize_t            num_read;
    char               buf[BUF_SIZE];

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

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

    /* Step 3: Connect to the server */
    /* connect() returns ECONNREFUSED if server is not listening */
    if (connect(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    printf("Connected to server\n");

    /* Step 4: Read from stdin and send to server */
    while ((num_read = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
        if (write(sfd, buf, num_read) != num_read) {
            fprintf(stderr, "partial/failed write\n");
            exit(EXIT_FAILURE);
        }
    }

    if (num_read == -1) {
        perror("read");
        exit(EXIT_FAILURE);
    }

    /* Step 5: Close socket โ€” sends EOF to server */
    if (close(sfd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("Data sent, connection closed\n");
    return 0;
}

To test:

# Terminal 1: Start server
./us_xfr_sv

# Terminal 2: Send some text
echo "Hello from EmbeddedPathashala" | ./us_xfr_cl

# Terminal 2: Or pipe a file
./us_xfr_cl < /etc/hostname

Concurrent Server: Handling Multiple Clients

The simple server above handles one client at a time. To handle multiple clients simultaneously, the classic approach is to fork a child for each connection. The child handles communication while the parent goes back to accept().

/* Concurrent server โ€” handle each client in a child process */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>

#define SV_SOCK_PATH "/tmp/us_concurrent"
#define BUF_SIZE     256
#define BACKLOG      10

/* Reap zombie child processes */
static void sigchld_handler(int sig)
{
    (void)sig;
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

static void handle_client(int cfd)
{
    char    buf[BUF_SIZE];
    ssize_t n;

    while ((n = read(cfd, buf, BUF_SIZE)) > 0) {
        /* Echo back to client */
        if (write(cfd, buf, n) != n) {
            perror("write");
            break;
        }
    }
    close(cfd);
}

int main(void)
{
    struct sockaddr_un addr;
    int                sfd, cfd;
    struct sigaction   sa;

    /* Set up SIGCHLD to reap children automatically */
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    remove(SV_SOCK_PATH);

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
        perror("bind"); exit(EXIT_FAILURE);
    }

    if (listen(sfd, BACKLOG) == -1) {
        perror("listen"); exit(EXIT_FAILURE);
    }

    printf("Concurrent echo server on %s\n", SV_SOCK_PATH);

    for (;;) {
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1) {
            if (errno == EINTR)  /* Interrupted by signal (SIGCHLD) */
                continue;
            perror("accept");
            exit(EXIT_FAILURE);
        }

        switch (fork()) {
        case -1:
            perror("fork");
            close(cfd);
            break;

        case 0:             /* Child: handle the client */
            close(sfd);     /* Child doesn't need the listening socket */
            handle_client(cfd);
            exit(EXIT_SUCCESS);

        default:            /* Parent: go back to accept() */
            close(cfd);     /* Parent doesn't need the connected socket */
            break;
        }
    }

    unlink(SV_SOCK_PATH);
    return 0;
}

Concurrent server โ€” process flow:

Parent
accept() loop
Listening socket
client1 โ†’ client2 โ†’ client3 โ†’
Child 1 โ€” handles client1
Child 2 โ€” handles client2
Child 3 โ€” handles client3

Important Stream Behavior: Partial Reads & Writes

A stream socket is a byte pipe โ€” it has no concept of message boundaries. This has practical consequences:

โš ๏ธ A single write() may not send all bytes. If the socket buffer is nearly full, write(fd, buf, 1000) might only write 600 bytes and return 600. You must loop until all bytes are sent.
/* Safe write โ€” keep writing until all bytes are sent */
ssize_t write_all(int fd, const void *buf, size_t count)
{
    const char *p   = buf;
    size_t      rem = count;
    ssize_t     n;

    while (rem > 0) {
        n = write(fd, p, rem);
        if (n == -1) {
            if (errno == EINTR)
                continue;   /* Interrupted by signal, retry */
            return -1;      /* Real error */
        }
        p   += n;
        rem -= n;
    }
    return (ssize_t)count;
}

/* Safe read โ€” read exactly 'count' bytes */
ssize_t read_all(int fd, void *buf, size_t count)
{
    char   *p   = buf;
    size_t  rem = count;
    ssize_t n;

    while (rem > 0) {
        n = read(fd, p, rem);
        if (n == 0)
            return (ssize_t)(count - rem); /* EOF โ€” short read */
        if (n == -1) {
            if (errno == EINTR)
                continue;
            return -1;
        }
        p   += n;
        rem -= n;
    }
    return (ssize_t)count;
}

For a stream socket, detecting end-of-data relies on EOF: when the peer calls close() on its end, read() on our side returns 0.

Graceful Close with shutdown()

close() closes both directions of a stream socket. Sometimes you want a half-close โ€” signal EOF to the peer while still being able to read data coming back. Use shutdown():

#include <sys/socket.h>

int shutdown(int sockfd, int how);

/* how can be: */
/* SHUT_RD   โ€” no more reads  (sends nothing to peer) */
/* SHUT_WR   โ€” no more writes (sends FIN/EOF to peer) */
/* SHUT_RDWR โ€” both           (same effect as close()) */

/* Example: client finishes sending, signals EOF, then reads response */
write_all(sfd, request, request_len);
shutdown(sfd, SHUT_WR);   /* Tell server: no more data coming */

/* Now read the server's response */
n = read(sfd, response, sizeof(response));
close(sfd);

This pattern is useful when using UNIX domain sockets as a request-response protocol: the client sends its full request, signals EOF to the server, then reads the response.

๐ŸŽฏ Interview Questions โ€” Stream Sockets
Q1. What is the difference between the listening socket and the connected socket returned by accept()?

The listening socket (created by socket() and passed to listen()) is a passive socket. Its job is to sit and wait for incoming connection requests. Each time accept() is called on the listening socket, the kernel takes the next pending connection from the backlog queue and creates a brand new connected socket specifically for that client. The server then uses the connected socket to communicate with that client, while the listening socket stays open to accept further connections.

Q2. What does the backlog parameter to listen() mean?

The backlog limits how many pending but not yet accepted connections the kernel will queue up. If the server is busy (e.g., in fork()) and a burst of clients connect, the kernel holds them in this queue up to the backlog limit. Clients beyond the limit get ECONNREFUSED. The kernel may silently cap the value at the system maximum (/proc/sys/net/core/somaxconn). Common values are 5, 10, or 128. For high-traffic servers, use a larger value.

Q3. Why must you loop when calling write() or read() on a stream socket?

Because stream sockets transfer a byte stream, not message-sized chunks. The kernel can return from write() or read() with fewer bytes than requested if the socket buffer is full (write) or if data has not arrived yet (read) or if a signal interrupted the call (EINTR). If you do not loop, you silently lose data or stop reading early.

Q4. How does a stream socket server detect that a client has closed the connection?

When the client calls close() (or the client process exits), the kernel sends an EOF indication to the server. The server’s read() on the connected socket returns 0. This is the standard EOF signal. After that, writing to the socket will fail with EPIPE or ECONNRESET (and generate SIGPIPE if not ignored).

Q5. What is the difference between close() and shutdown() on a socket?

close(fd) decrements the file descriptor’s reference count. The socket is only truly closed (connection terminated) when the count reaches zero. shutdown(fd, how) immediately affects the underlying connection regardless of reference counts โ€” it sends EOF in the specified direction. This is useful with dup() or fork() where multiple descriptors refer to the same socket. shutdown(fd, SHUT_WR) also enables half-close, which close() cannot do.

Q6. In the concurrent server using fork(), why must the parent close cfd and the child close sfd?

After fork(), both parent and child have copies of all open file descriptors. The parent closes cfd because it does not need to communicate with the client โ€” the child will do that. If the parent keeps cfd open, the client’s socket will not get EOF when the child closes it (because the parent still holds a reference). Similarly, the child closes sfd (the listening socket) because the child does not need to accept new connections โ€” only the parent does. Not closing these leads to resource leaks and incorrect EOF behavior.

Leave a Reply

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