UNIX Domain Stream Sockets Server & Client Example

 

UNIX Domain Stream Sockets
Server & Client Example โ€“ Chapter 57 | Linux System Programming
๐Ÿ“ก Topic: IPC via Sockets
๐Ÿ–ฅ๏ธ Type: Stream (SOCK_STREAM)
๐Ÿ“‚ Domain: AF_UNIX

In the previous sections, we understood that UNIX domain sockets let two processes on the same machine talk to each other using the filesystem as an address. In this section, we look at a real working example of a stream socket server and client. A stream socket works like a phone call โ€” you dial, connect, and then data flows both ways in order, reliably.

The example we study transfers data from the client’s standard input to the server, which writes it to standard output. Simple but powerful โ€” it shows every step you need in a real application.

Keywords:

AF_UNIX SOCK_STREAM sockaddr_un socket() bind() listen() accept() connect() read() / write() remove() sun_path socket file

What Does the Example Do?

The example has two programs: us_xfr_sv (server) and us_xfr_cl (client). They share a common header file that defines the socket path and buffer size.

Data Flow Between Client and Server
CLIENT
reads stdin
writes to socket
โžก
UNIX socket
/tmp/us_xfr
SERVER
reads socket
writes to stdout
Both programs run on the same machine. Data stays inside the kernel โ€” no network involved.

The socket file appears on the filesystem at /tmp/us_xfr. You can see it with ls -lF โ€” it shows up with an s type marker and a = suffix. This is just an address; actual data does not pass through the filesystem.

Step 0 โ€“ The Shared Header File

Both server and client include a common header that defines the socket path and the I/O buffer size. Keeping these in one place avoids mistakes โ€” if you change the path, you change it once.

/* us_xfr.h โ€“ shared header for stream socket example */

#include <sys/un.h>       /* struct sockaddr_un */
#include <sys/socket.h>   /* socket(), bind(), listen(), accept(), connect() */
#include "tlpi_hdr.h"     /* errExit(), fatal(), errMsg() */

#define SV_SOCK_PATH  "/tmp/us_xfr"   /* pathname used as server address */
#define BUF_SIZE      100             /* read/write buffer size */

SV_SOCK_PATH is the filesystem path that becomes the socket address. BUF_SIZE controls how many bytes we read/write in each loop iteration. Using /tmp/ makes the path writable by any user for testing purposes.

Step 1 โ€“ The Server Program (us_xfr_sv.c)

The server does five things in order: create socket โ†’ remove old file โ†’ bind โ†’ listen โ†’ loop accepting clients.

Server Lifecycle
socket(AF_UNIX, SOCK_STREAM)
โ†“
remove(SV_SOCK_PATH)
โ†“
bind(sfd, &addr, …)
โ†“
listen(sfd, backlog)
โ†“
for(;;) accept() โ†’ read/write โ†’ close(cfd)
/* us_xfr_sv.c โ€“ UNIX domain stream socket server */

#include "us_xfr.h"

#define BACKLOG  5   /* max pending connections queued by listen() */

int
main(int argc, char *argv[])
{
    struct sockaddr_un addr;
    int sfd, cfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    /* 1. Create the server's listening socket */
    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1)
        errExit("socket");

    /*
     * 2. Remove any leftover socket file from a previous run.
     *    If we skip this, bind() will fail with EADDRINUSE because
     *    the old pathname still exists on the filesystem.
     *    errno == ENOENT is fine โ€” it just means no old file exists.
     */
    if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT)
        errExit("remove-%s", SV_SOCK_PATH);

    /* 3. Fill in the address structure and bind */
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
        errExit("bind");

    /* 4. Mark the socket as passive (listening) */
    if (listen(sfd, BACKLOG) == -1)
        errExit("listen");

    /* 5. Main server loop โ€“ handle one client at a time */
    for (;;) {

        /* accept() blocks until a client connects.
         * It returns a brand-new socket cfd for this connection. */
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1)
            errExit("accept");

        /* Read data from the client and echo it to stdout */
        while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)
            if (write(STDOUT_FILENO, buf, numRead) != numRead)
                fatal("partial/failed write");

        if (numRead == -1)
            errExit("read");

        /* Close the per-connection socket; sfd stays open for next client */
        if (close(cfd) == -1)
            errMsg("close");
    }
}
Key Points to Remember

Why remove() before bind()? A UNIX domain socket creates a real file on disk. When the server shuts down, that file stays behind. The next time you start the server, bind() will fail with EADDRINUSE because the path already exists. Calling remove() first cleans it up. We allow errno == ENOENT (file not found) because on the very first run there is nothing to remove.

Two sockets: sfd is the listening socket โ€” it never carries data. cfd is the connected socket returned by accept() for each client โ€” data is read and written on cfd. After the client disconnects, we close cfd but keep sfd open for the next client.

End of file detection: When the client closes its socket, read() on the server side returns 0 (EOF). This exits the inner while loop and the server closes cfd and waits for the next client.

Step 2 โ€“ The Client Program (us_xfr_cl.c)

The client is simpler: create socket โ†’ connect โ†’ loop reading stdin and writing to socket โ†’ exit.

Client Lifecycle
socket(AF_UNIX, SOCK_STREAM)
โ†“
connect(sfd, &addr, …)
โ†“
while read(stdin) โ†’ write(sfd)
โ†“
exit() โ†’ socket closed โ†’ server sees EOF
/* us_xfr_cl.c โ€“ UNIX domain stream socket client */

#include "us_xfr.h"

int
main(int argc, char *argv[])
{
    struct sockaddr_un addr;
    int sfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    /* 1. Create the client socket */
    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1)
        errExit("socket");

    /* 2. Build the server's address and connect */
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
        errExit("connect");

    /*
     * 3. Read from stdin, write to the socket.
     *    When stdin reaches EOF (e.g., end of file input), the loop exits.
     *    exit() closes the socket, and the server's read() returns 0 (EOF).
     */
    while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0)
        if (write(sfd, buf, numRead) != numRead)
            fatal("partial/failed write");

    if (numRead == -1)
        errExit("read");

    exit(EXIT_SUCCESS);    /* socket is closed; server sees EOF */
}
Client Does NOT Call bind()

Unlike a server, the client does not need to bind its socket to any path. The kernel automatically assigns an unnamed address to the client socket. The server uses accept() to get the connected socket and reads from it โ€” the server does not need to know the client’s address for a stream socket.

Note on strncpy: We subtract 1 from the size to ensure the path is always null-terminated. sun_path is a fixed-size array of 108 bytes on Linux.

Running the Example โ€“ Shell Session

Here is a step-by-step shell session that shows the programs working together:

# Step 1: Start the server in the background, redirect output to file 'b'
$ ./us_xfr_sv > b &
[1] 9866

# Step 2: Check the socket file on the filesystem
$ ls -lF /tmp/us_xfr
srwxr-xr-x  1 mtk  users  0 Jul 18 10:48 /tmp/us_xfr=
#  ^                                                  ^
#  's' = socket type                         '=' suffix shown by ls -F

# Step 3: Create a test input file from all .c source files
$ cat *.c > a

# Step 4: Run the client โ€“ it reads from file 'a' and sends to server
$ ./us_xfr_cl < a

# Step 5: Kill the server (it runs forever until signaled)
$ kill %1
[1]+ Terminated   ./us_xfr_sv >b

# Step 6: Verify the output matches the input exactly
$ diff a b
$
# No output = files are identical. Transfer was perfect!
โš  Socket File Persists After Server Exits

Notice that /tmp/us_xfr still exists even after the server terminates. This is different from pipes, which vanish when all processes close them. A UNIX domain socket file stays on disk until explicitly removed. This is exactly why the server calls remove(SV_SOCK_PATH) at startup โ€” to clean up any leftover file from a previous run.

Understanding struct sockaddr_un

All UNIX domain socket addresses use struct sockaddr_un defined in <sys/un.h>. Its structure is straightforward:

/* From <sys/un.h> */
struct sockaddr_un {
    sa_family_t  sun_family;     /* Always AF_UNIX */
    char         sun_path[108];  /* Null-terminated pathname (Linux) */
};

Memory Layout of sockaddr_un
sun_family
2 bytes
= AF_UNIX (1)
sun_path[108]
108 bytes
“/tmp/us_xfr\0…”

Always call memset(&addr, 0, sizeof(...)) before filling in the structure. This zeros out the entire buffer including any padding bytes. Without this, you may pass garbage bytes to the kernel, causing undefined behavior.

/* Correct way to initialize sockaddr_un */
struct sockaddr_un addr;

memset(&addr, 0, sizeof(struct sockaddr_un));   /* zero entire struct */
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
/* -1 ensures space for the null terminator */

Complete Self-Contained Demo (Echo Server)

Below is a clean, self-contained echo server and client that you can compile and run without the TLPI library. The server echoes back every line the client sends.

echo_server.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 SOCK_PATH  "/tmp/echo_demo"
#define BUF_SIZE   256
#define BACKLOG    5

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

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

    /* Remove stale socket file */
    if (remove(SOCK_PATH) == -1 && errno != ENOENT)
        { perror("remove"); exit(1); }

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

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

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

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

    for (;;) {
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1) { perror("accept"); continue; }

        printf("Client connected\n");

        /* Echo loop */
        while ((n = read(cfd, buf, BUF_SIZE)) > 0) {
            printf("Received %zd bytes: %.*s", n, (int)n, buf);
            if (write(cfd, buf, n) != n) {
                fprintf(stderr, "write error\n");
                break;
            }
        }
        printf("Client disconnected\n");
        close(cfd);
    }

    close(sfd);
    remove(SOCK_PATH);
    return 0;
}

echo_client.c

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

#define SOCK_PATH  "/tmp/echo_demo"
#define BUF_SIZE   256

int main(void)
{
    int sfd;
    struct sockaddr_un addr;
    char sendbuf[BUF_SIZE], recvbuf[BUF_SIZE];
    ssize_t n;

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

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

    if (connect(sfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
        { perror("connect"); exit(1); }

    printf("Connected to server. Type messages (Ctrl+D to quit):\n");

    while (fgets(sendbuf, BUF_SIZE, stdin) != NULL) {
        n = strlen(sendbuf);
        if (write(sfd, sendbuf, n) != n) {
            fprintf(stderr, "write error\n");
            break;
        }
        n = read(sfd, recvbuf, BUF_SIZE);
        if (n <= 0) break;
        printf("Echo: %.*s", (int)n, recvbuf);
    }

    close(sfd);
    return 0;
}

/* Compile:
   gcc -o echo_server echo_server.c
   gcc -o echo_client echo_client.c

   Run:
   ./echo_server &
   ./echo_client
*/

Interview Questions

Q1. Why does the server call remove() before bind()?

A UNIX domain socket creates a file on the filesystem when bind() is called. When the server exits (normally or by crash), that file is not automatically deleted. If the server restarts and tries to bind() to the same path, the kernel returns EADDRINUSE because the path already exists โ€” even though no process is listening. Calling remove() first cleans up the stale file. We allow errno == ENOENT (no such file) because this is fine on the first ever run.

Q2. What is the difference between sfd and cfd in the server?

sfd is the listening socket โ€” created once at startup. It is bound to the socket pathname and marked passive via listen(). It only accepts incoming connection requests; data is never read from or written to it.

cfd is the connected socket returned by accept() for each individual client. Data is exchanged on cfd. After the client disconnects, cfd is closed. sfd remains open for the next client.

Q3. How does the server know when the client has finished sending data?

When the client process exits (or explicitly closes its socket), the kernel sends a FIN segment on the stream. The server’s next read() call returns 0, which is the standard UNIX signal for end-of-file on a file descriptor. The server uses this as the signal to stop reading and close cfd.

Q4. Does the client need to call bind()?

No. For a stream socket client, binding is optional. The client only needs to call connect() with the server’s address. The kernel automatically assigns the client socket an unnamed (autobind) address. This is different from datagram socket clients, which often need to bind so the server can reply to them.

Q5. What happens if you try to connect() before the server calls listen()?

The connect() call will fail immediately with ECONNREFUSED. The kernel checks if there is a listening socket at the specified address. If no socket is listening at that path, the connection is refused. This is why the server must set up and start listening before any client tries to connect.

Q6. Can multiple clients connect to the same UNIX domain stream server simultaneously?

Yes, but in this simple example the server handles only one client at a time (iterative server). The BACKLOG parameter to listen() controls how many pending connections the kernel will queue while the server is busy with one client. Once the backlog queue is full, new connect() attempts will fail. For concurrent handling, the server would need to fork() or use threads after each accept().

Q7. What does ls -lF show for a UNIX domain socket file?

The ls -l output shows s as the first character of the file type (e.g., srwxr-xr-x). With the -F flag, ls appends = to socket filenames to indicate their type. The file size is always shown as 0 because no data is stored in the socket file itself โ€” it is only a name (address) in the filesystem.

Q8. What is the maximum length of sun_path on Linux?

On Linux, sun_path is 108 bytes including the null terminator. This means pathnames can be at most 107 characters long. Always use strncpy with sizeof(addr.sun_path) - 1 as the size limit to avoid overflowing the field and to ensure null termination.

Next: UNIX Domain Datagram Sockets

Learn how connectionless datagram sockets work in the UNIX domain โ€” faster and simpler for request-reply patterns.

Next Topic โ†’ EmbeddedPathashala Home

Leave a Reply

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