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.
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.
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.
/* 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");
}
}
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.
/* 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 */
}
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!
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) */
};
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
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.
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.
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.
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.
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.
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().
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.
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.
Learn how connectionless datagram sockets work in the UNIX domain โ faster and simpler for request-reply patterns.
