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
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).
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;
}
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
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;
}
accept() loop
Listening socket
A stream socket is a byte pipe โ it has no concept of message boundaries. This has practical consequences:
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.
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.
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.
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.
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.
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).
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.
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.
