In Part 1 we learned how to create and bind a UNIX domain socket. Now we build a real client-server system using stream sockets (SOCK_STREAM) in the UNIX domain. Stream sockets give you a reliable, ordered, connection-oriented byte stream โ similar to TCP but limited to the local machine.
We will walk through the complete server code from the TLPI book, understand each call, and see how to write the matching client. By the end you will understand the full server lifecycle: socket โ bind โ listen โ accept โ read/write โ close.
Stream Sockets vs Datagram Sockets
UNIX domain supports two socket types. Choose based on what your application needs:
| Feature | SOCK_STREAM | SOCK_DGRAM |
|---|---|---|
| Connection | Required (connect/accept) | Connectionless |
| Reliability | Guaranteed delivery & order | Best-effort |
| Boundaries | Byte stream (no boundaries) | Message boundaries preserved |
| Analog | Like TCP | Like UDP |
For this tutorial we use SOCK_STREAM. A client connects, sends data, and the server reads everything until the connection closes.
Server Lifecycle โ The Full Picture
Notice that the server has two file descriptors: the listening socket (sfd) and the connected socket (cfd) returned by accept(). This is important โ the listening socket stays open for future clients while data is exchanged on cfd.
The listen() Call โ Making a Socket Passive
After bind(), the socket is just an address with no behavior. listen() turns it into a passive socket โ one that waits for incoming connections instead of actively initiating them.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/* Returns 0 on success, -1 on error */
The backlog parameter controls the connection queue:
The TLPI example uses BACKLOG = 5. This is a reasonable small value. In production, busy servers set backlog to 128 or even SOMAXCONN (the system’s maximum, checked via /proc/sys/net/core/somaxconn on Linux).
Complete Server Code โ Line by Line
Here is the complete UNIX domain stream socket server from TLPI Chapter 57, with added comments explaining every decision:
/* us_xfr_sv.c โ UNIX domain stream socket server
* Accepts connections and writes received data to stdout */
#include <stdio.h>
#include <string.h>
#include <stdlib.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 100
#define BACKLOG 5
int main(int argc, char *argv[])
{
struct sockaddr_un addr;
int sfd, cfd;
ssize_t numRead;
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 stale socket file if present ----
If server crashed last time, old socket file might exist.
remove() returns -1 with ENOENT if file doesn't exist โ that's fine.
Any other error from remove() means something is wrong. */
if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT) {
perror("remove"); exit(EXIT_FAILURE);
}
/* ---- Step 3: Fill sockaddr_un 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) {
perror("bind"); exit(EXIT_FAILURE);
}
/* ---- Step 4: Mark socket as passive (listening) ---- */
if (listen(sfd, BACKLOG) == -1) {
perror("listen"); exit(EXIT_FAILURE);
}
/* ---- Step 5: Accept loop โ handles one client at a time ----
This is an ITERATIVE server. It fully serves client N before
accepting client N+1. For concurrent handling, you would
fork() or use threads after accept(). */
for (;;) {
/* accept() blocks until a client calls connect().
It returns a NEW socket fd (cfd) for this connection.
The original sfd keeps listening for future clients.
We pass NULL for addr/addrlen โ we don't need client address. */
cfd = accept(sfd, NULL, NULL);
if (cfd == -1) { perror("accept"); exit(EXIT_FAILURE); }
/* ---- Step 6: Read data until client closes connection ----
read() returns 0 when the client calls close() (EOF).
read() returns -1 on error. */
while ((numRead = read(cfd, buf, BUF_SIZE)) > 0) {
/* Write everything we read to stdout */
if (write(STDOUT_FILENO, buf, numRead) != numRead) {
fprintf(stderr, "partial/failed write\n");
exit(EXIT_FAILURE);
}
}
if (numRead == -1) { perror("read"); exit(EXIT_FAILURE); }
/* ---- Step 7: Close the connected socket, go back to accept ---- */
if (close(cfd) == -1)
perror("close"); /* non-fatal, keep going */
}
/* Note: sfd and the socket file /tmp/us_xfr are never cleaned up
in this infinite loop server. In a real server you would catch
signals (SIGTERM, SIGINT) and call unlink(SV_SOCK_PATH) on exit. */
}
- Created by
socket() - Bound with
bind() - Marked passive with
listen() - Stays open the whole time
- Never used for data transfer
- Created by
accept() - One per client connection
- Used for
read()/write() - Closed after client disconnects
- New cfd for each new client
Complete Client Code
The client reads from its standard input and sends everything to the server. When stdin reaches EOF (user presses Ctrl+D), the client closes the socket, which signals EOF to the server.
/* us_xfr_cl.c โ UNIX domain stream socket client
* Reads from stdin and sends to server */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SV_SOCK_PATH "/tmp/us_xfr"
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
struct sockaddr_un addr;
int sfd;
ssize_t numRead;
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: Fill server address and connect ----
Client does NOT need to call bind() โ the kernel assigns
an automatic address to the client socket.
Client also does NOT call listen() or accept(). */
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) {
perror("connect"); exit(EXIT_FAILURE);
}
/* ---- Step 3: Read from stdin, write to socket ----
read() from STDIN_FILENO returns 0 at EOF (Ctrl+D). */
while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
if (write(sfd, buf, numRead) != numRead) {
fprintf(stderr, "partial/failed write\n");
exit(EXIT_FAILURE);
}
}
if (numRead == -1) { perror("read"); exit(EXIT_FAILURE); }
/* ---- Step 4: Close socket โ server sees EOF ---- */
exit(EXIT_SUCCESS); /* close(sfd) happens automatically */
}
Why doesn’t the client call bind()? A client does not need a named address. When the client calls connect(), the kernel automatically assigns an unnamed address to the socket. Only servers need to bind to a well-known path so clients can find them.
Compiling and Running the Example
# Compile both programs
gcc -o us_xfr_sv us_xfr_sv.c
gcc -o us_xfr_cl us_xfr_cl.c
# Terminal 1 โ start server (it blocks at accept())
./us_xfr_sv
# Terminal 2 โ run client, type some text, press Ctrl+D to end
./us_xfr_cl
Hello from client
This is a test
^D
# Back in Terminal 1, you should see:
# Hello from client
# This is a test
After the client disconnects, the server loops back to accept() and waits for the next client. This is the iterative server pattern โ simple, single-threaded, handles one client completely before moving to the next.
Iterative Server vs Concurrent Server
The server above is an iterative server. It processes client 1 completely, then client 2, and so on. This is fine for quick tasks but a slow client will block all other clients.
The TLPI example is deliberately kept simple as an iterative server. Chapter 60 of TLPI covers concurrent server design in detail using fork().
Detecting EOF โ How the Server Knows the Client Left
The server reads data in a loop:
while ((numRead = read(cfd, buf, BUF_SIZE)) > 0) {
write(STDOUT_FILENO, buf, numRead);
}
Three possible return values from read():
When the client calls close(sfd) (or exits), the kernel sends a FIN on the connection. The server’s next read() returns 0. The server then exits the loop, closes cfd, and waits for the next client.
Interview Questions
sfd is the listening socket โ created once, bound to a path, and kept open the entire lifetime of the server. It is never used for data transfer. cfd is the connected socket returned by accept() โ a new cfd is created for each client and closed when that client disconnects. Data flows only through cfd.
The client does not need a well-known address โ only the server does, so clients can find it. When the client calls connect(), the kernel automatically assigns an unnamed (autobind) address to the client socket. The server does not need to know the client’s address to send data back โ it uses the cfd returned by accept().
It sets the maximum number of pending (unaccepted) connections that can queue up. If the queue is full when a new client tries to connect, the connection is refused or dropped. A busy server should set backlog high (128 or SOMAXCONN) and call accept() quickly. The TLPI example uses 5, which is fine for a simple demo.
An iterative server processes one client completely before accepting the next. It is simple and uses no extra processes or threads. It is appropriate when each client interaction is short (milliseconds). It is not appropriate when clients hold connections for a long time or do heavy processing โ a slow client would block all other clients. Use a concurrent server (fork, threads, or event-driven I/O with select/poll/epoll) for those cases.
When the client calls close() or exits, the kernel sends EOF on the connection. The server’s read() on cfd returns 0. The server exits its read loop and calls close(cfd), then goes back to accept().
If the server crashed previously, its socket file is still on the filesystem. A direct bind() would fail with EADDRINUSE. Calling remove(SV_SOCK_PATH) first cleans up the stale entry. The errno != ENOENT check allows the case where the file simply doesn’t exist โ that is not an error.
Yes, using I/O multiplexing โ select(), poll(), or epoll(). The server adds both the listening socket and all connected sockets to the watch set. When select()/epoll() reports activity on the listening socket, the server calls accept(). When it reports activity on a connected socket, the server reads data. This is the event-driven (reactor) pattern used by nginx, Redis, and many others.
The connected socket file descriptor leaks. Each iteration of the loop creates a new cfd without closing the old one. Eventually the process hits the open file descriptor limit (ulimit -n, typically 1024 by default) and accept() starts returning EMFILE errors. Always close(cfd) after finishing with a client.
Go back to Part 1 to review socket addresses, or explore more IPC and socket topics on EmbeddedPathashala.
