Beej's Guide to Unix IPC — Chapter 11

Unix Sockets

Full-duplex, bidirectional communication between processes — like a two-way pipe, but better.

Prerequisites: Pipes (Ch. 4) + fork() (Ch. 2). That's it.
10
Chapters
4+
Simulations
0
Assumed Knowledge

Chapter 0: Why Unix Sockets?

Remember pipes? They only go one way. You write into one end, read from the other. If you want two-way communication, you need two pipes — and that gets messy fast.

Remember FIFOs? Same problem. One direction only. Plus they're stuck with the limitations of the file interface: open(), read(), write().

Unix domain sockets solve both problems. They give you a two-way communication channel between processes on the same machine. Send data in both directions. Use a proper client-server model. And the API is the same one used for network programming — so everything you learn here transfers directly to Internet sockets.

The mental model: Think of a pipe as a garden hose — water flows one way. A Unix socket is more like a telephone line. Either side can talk, either side can listen. And just like phone lines, one side has to be "the operator" (the server) waiting for calls, and the other side dials in (the client).
Pipe vs Socket

Compare one-way pipes with full-duplex Unix sockets. Click to send messages in each direction.

Click a button
What is the key advantage of Unix sockets over pipes?

Chapter 1: Overview

Unix domain sockets look like special files in the filesystem (just like FIFOs), but you don't use open() and read(). Instead, you use the sockets API: socket(), bind(), listen(), accept(), connect(), send(), recv().

The communication follows a client-server pattern:

Server
Creates socket → binds to a path → listens → accepts connections → recv/send
↓ connection
Client
Creates socket → connects to server's path → send/recv

The "address" of a Unix socket is a filesystem path. Both sides reference the same path to establish a connection. The key data structure is struct sockaddr_un:

c
struct sockaddr_un {
    unsigned short sun_family;  /* AF_UNIX */
    char           sun_path[108]; /* socket path */
};
AF_UNIX vs AF_INET: AF_UNIX means "Unix domain" — communication between processes on the same machine via a filesystem path. AF_INET means "Internet domain" — communication over TCP/IP networks. The API is nearly identical; only the address structure differs.
How does a Unix domain socket identify its "address"?

Chapter 2: socket() — Creating the Endpoint

The first step for both server and client is creating a socket descriptor with socket():

c
#include <sys/socket.h>
#include <sys/un.h>

int s;
s = socket(AF_UNIX, SOCK_STREAM, 0);
ArgumentPurposeValue
domainCommunication domainAF_UNIX (local machine)
typeSocket typeSOCK_STREAM (reliable, ordered) or SOCK_DGRAM (datagrams)
protocolProtocol (usually 0)0 (auto-select)

socket() returns a file descriptor, just like open(). This descriptor is an endpoint — think of it as the phone itself, before you've dialed a number or picked up a ringing call.

SOCK_STREAM vs SOCK_DGRAM: SOCK_STREAM gives you a reliable, ordered byte stream — like TCP. Data arrives in the order sent, no losses. SOCK_DGRAM gives you individual messages (datagrams) with no ordering guarantee — like UDP. For IPC on the same machine, SOCK_STREAM is the common choice.
Error handling: All socket functions return -1 on error and set errno. Always check. The example code in Beej's guide sometimes omits error checking for clarity, but production code should always handle errors.
What does the socket() call return?

Chapter 3: Being a Server

A server does five things: socket, bind, listen, accept, handle. Let's walk through each step.

Step 1: socket()

Create the socket (you already know this from Chapter 2).

Step 2: bind()

Associate the socket with a filesystem path:

c
struct sockaddr_un local;

local.sun_family = AF_UNIX;
strcpy(local.sun_path, "/tmp/echo_socket");
unlink(local.sun_path);  /* remove if exists */

int len = strlen(local.sun_path) + sizeof(local.sun_family);
bind(s, (struct sockaddr *)&local, len);
Critical: You must call unlink() before bind()! If the socket file already exists from a previous run, bind() will fail with EADDRINUSE. Unlinking removes the stale file so you can bind fresh.

Step 3: listen()

Tell the socket to start accepting incoming connections:

c
listen(s, 5);  /* backlog of 5 pending connections */

The second argument is the backlog — how many connections can queue up before you call accept(). Additional clients get ECONNREFUSED.

Step 4: accept()

Block until a client connects, then return a new socket descriptor for that connection:

c
struct sockaddr_un remote;
int t = sizeof(remote);
int s2 = accept(s, (struct sockaddr *)&remote, &t);
Key insight: accept() returns a new socket descriptor s2. The original s keeps listening for more connections. You use s2 to communicate with this specific client. This is how servers handle multiple clients.

Step 5: Handle the connection

Use send() and recv() on the new descriptor s2. When done, close(s2) and loop back to accept().

Server Lifecycle

Step through the server setup process. Each click advances to the next system call.

Step 0/5
Why does the server call unlink() before bind()?

Chapter 4: Being a Client

The client side is much simpler. No bind(), no listen(), no accept(). Just create a socket and connect:

Step 1: socket()
Create a socket descriptor (same as the server)
Step 2: connect()
Connect to the server's socket path
Step 3: send/recv
Exchange data through the connected socket
Step 4: close()
Shut down the connection
c
int s;
struct sockaddr_un remote;

s = socket(AF_UNIX, SOCK_STREAM, 0);

remote.sun_family = AF_UNIX;
strcpy(remote.sun_path, "/tmp/echo_socket");
int len = strlen(remote.sun_path) + sizeof(remote.sun_family);

connect(s, (struct sockaddr *)&remote, len);

/* Now send() and recv() freely */
send(s, "Hello!", 6, 0);
recv(s, buf, 100, 0);
close(s);
Same path, different role: The client references the same sun_path as the server's bind(). This is how the two find each other. The server creates the socket file; the client connects to it. Think of the path as a phone number.
CallServerClient
socket()YesYes
bind()YesNo
listen()YesNo
accept()Yes (returns new fd)No
connect()NoYes
send()/recv()Yes (on accepted fd)Yes (on connected fd)
close()YesYes
What system calls does a client NOT need that a server does?

Chapter 5: echos.c — The Echo Server

Beej's echo server waits for a connection, receives whatever the client sends, and echoes it right back. Here's the full program:

c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCK_PATH "echo_socket"

int main(void) {
    int s, s2, t, len;
    struct sockaddr_un local, remote;
    char str[100];

    s = socket(AF_UNIX, SOCK_STREAM, 0);

    local.sun_family = AF_UNIX;
    strcpy(local.sun_path, SOCK_PATH);
    unlink(local.sun_path);
    len = strlen(local.sun_path)
        + sizeof(local.sun_family);

    bind(s, (struct sockaddr *)&local, len);
    listen(s, 5);

    for(;;) {
        int done, n;
        printf("Waiting for a connection...\n");
        t = sizeof(remote);
        s2 = accept(s, (struct sockaddr *)&remote,
                    &t);
        printf("Connected.\n");

        done = 0;
        do {
            n = recv(s2, str, 100, 0);
            if (n <= 0) {
                if (n < 0) perror("recv");
                done = 1;
            }
            if (!done)
                send(s2, str, n, 0);
        } while (!done);

        close(s2);
    }
    return 0;
}
Anatomy: The server loops forever (for(;;)). Each iteration: accept a client, echo messages until the client disconnects (recv returns 0), close the connection, wait for the next client.
Key detail: The server uses s (the listening socket) only for accept(). All data exchange happens on s2 (the connected socket). After closing s2, the server loops back and accepts on s again. The two descriptors serve completely different purposes.
In echos.c, when does the recv() loop end?

Chapter 6: echoc.c — The Echo Client

The client is simpler — no bind, no listen, no accept. Just connect and talk:

c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCK_PATH "echo_socket"

int main(void) {
    int s, t, len;
    struct sockaddr_un remote;
    char str[100];

    s = socket(AF_UNIX, SOCK_STREAM, 0);

    remote.sun_family = AF_UNIX;
    strcpy(remote.sun_path, SOCK_PATH);
    len = strlen(remote.sun_path)
        + sizeof(remote.sun_family);

    connect(s, (struct sockaddr *)&remote, len);

    while(printf("> "),
          fgets(str, 100, stdin),
          !feof(stdin)) {
        send(s, str, strlen(str), 0);

        t = recv(s, str, 100, 0);
        if (t > 0) {
            str[t] = '\0';
            printf("echo> %s", str);
        } else {
            printf("Server closed\n");
            exit(1);
        }
    }
    close(s);
    return 0;
}
Try it: Compile both programs. Run the server in one terminal (./echos). Run the client in another (./echoc). Type something at the > prompt. The server echoes it back. Press Ctrl-D (EOF) to disconnect.
Terminal 1: Server
$ ./echos
Waiting for a connection...
Connected.
← connection established →
Terminal 2: Client
$ ./echoc
> Hello Unix sockets!
echo> Hello Unix sockets!
Why doesn't the client call bind() or listen()?

Chapter 7: socketpair() — Quick Full-Duplex Pipes

What if you want a two-way pipe between a parent and child process, without all the server/client setup? That's what socketpair() is for. It creates two connected sockets in one call — no bind, no listen, no accept, no connect.

c
#include <sys/socket.h>

int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

After this call, sv[0] and sv[1] are connected to each other. Write to sv[0], read from sv[1]. Write to sv[1], read from sv[0]. Bidirectional.

Compare with pipe(): pipe(fd) gives you two descriptors, but one is read-only and one is write-only. socketpair(sv) gives you two descriptors that are both read-write. And since they're sockets, you can use send/recv flags.

The typical pattern: call socketpair(), then fork(). The parent uses sv[0], the child uses sv[1]:

c
int sv[2];
char buf;

socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

if (!fork()) {
    /* Child: use sv[1] */
    read(sv[1], &buf, 1);
    printf("child: read '%c'\n", buf);
    buf = toupper(buf);
    write(sv[1], &buf, 1);
} else {
    /* Parent: use sv[0] */
    write(sv[0], "b", 1);
    read(sv[0], &buf, 1);
    printf("parent: read '%c'\n", buf);
    wait(NULL);
}
Parent (sv[0])
write(sv[0], "b") → child reads 'b'
↓ full duplex
Child (sv[1])
write(sv[1], "B") → parent reads 'B'
Key insight: You can use read()/write() instead of send()/recv() on socket descriptors — they're just file descriptors. Beej uses read/write in the socketpair example for simplicity, since the flags argument to send/recv is usually 0 anyway.
How does socketpair() differ from pipe()?

Chapter 8: Echo Server Simulator

Watch the full client-server interaction play out step by step. Type a message, watch it travel through the socket, and see the echo come back.

Echo Server & Client

Click "Connect" to establish a connection, then send messages. The server echoes each one back.

Server: listening
socketpair() Simulator

Parent and child communicate over a socketpair. Send a character from parent, watch the child uppercase it and send it back.

socketpair() ready
In an echo server, what happens after send() echoes the data back to the client?

Chapter 9: Beyond Unix Sockets

You've now seen every IPC mechanism in the Unix toolkit. Let's put them all in perspective:

MechanismDirectionSyncData ModelBest For
Pipes (Ch. 4)One-wayBuilt-inByte streamParent-child, shell pipelines
FIFOs (Ch. 5)One-wayBuilt-inByte streamUnrelated processes, simple IPC
Msg Queues (Ch. 7)One-wayBuilt-inTyped messagesPriority messages, typed data
Shared Memory (Ch. 9)AnyManualRaw memoryHigh-perf data sharing
mmap (Ch. 10)AnyManualFile-backed memoryFile access, persistent sharing
Unix Sockets (Ch. 11)Two-wayBuilt-inByte stream / datagramClient-server, full-duplex
The big picture: Each mechanism trades off differently between simplicity, speed, and flexibility. Pipes are the simplest. Shared memory is the fastest. Unix sockets are the most flexible. There's no single "best" mechanism — the right choice depends on what you're building.
Where to go next: The same socket API works for network programming with AF_INET. Everything you learned here — socket, bind, listen, accept, connect, send, recv — transfers directly to TCP/IP programming. The only difference is the address structure (struct sockaddr_in instead of struct sockaddr_un). Check out Beej's Guide to Network Programming for the full story.
When you need...Use...
Simple parent-child byte streampipe()
Byte stream between unrelated processesFIFOs (mkfifo)
Discrete typed messagesMessage queues (msgget)
Zero-copy shared data (volatile)Shared memory (shmget)
Zero-copy shared data (persistent)mmap (MAP_SHARED)
Two-way communicationUnix sockets
Quick full-duplex parent-child pipesocketpair()
Network communicationInternet sockets (AF_INET)

"Wouldn't it be grand if you could send data in both directions like you can with a socket? Well, hope no longer, because the answer is here: Unix Domain Sockets!" — Beej

Which IPC mechanism provides both full-duplex communication AND built-in synchronization?