What Are UNIX Domain Sockets?
UNIX domain sockets are a way for two programs running on the same machine to talk to each other. Unlike internet sockets (which use IP addresses and ports), UNIX domain sockets use a file path on disk as their address.
There are two types: stream sockets (like a phone call โ reliable, ordered, connection-based) and datagram sockets (like sending a letter โ each message is independent). This tutorial focuses on datagram sockets using SOCK_DGRAM.
Unlike internet UDP, UNIX domain datagram sockets are reliable on the same machine. Messages are not lost or reordered between local processes.
Both the client and server bind to a socket file path. The server listens on a well-known path. The client binds to a unique path (often using its PID) so the server knows where to reply.
/tmp/ud_ucase_cl.PID
/tmp/ud_ucase_sv
The server creates a socket, binds it to a known path, and waits for messages. When a message arrives, it knows the sender’s address (from recvfrom) and sends a reply back.
In the uppercase server example: the server reads a string, converts it to uppercase using toupper(), and sends the result back to the client.
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define SV_SOCK_PATH "/tmp/ud_ucase_sv"
#define BUF_SIZE 10
int main(void)
{
struct sockaddr_un svaddr, claddr;
socklen_t len;
char buf[BUF_SIZE];
ssize_t numBytes;
int sfd, j;
/* Step 1: Create a UNIX domain datagram socket */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) { perror("socket"); exit(1); }
/* Step 2: Remove any old socket file, then bind */
remove(SV_SOCK_PATH);
memset(&svaddr, 0, sizeof(struct sockaddr_un));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &svaddr,
sizeof(struct sockaddr_un)) == -1) {
perror("bind"); exit(1);
}
/* Step 3: Loop - receive messages and send uppercase replies */
for (;;) {
len = sizeof(struct sockaddr_un);
/* recvfrom fills claddr with the sender's address */
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numBytes == -1) { perror("recvfrom"); exit(1); }
printf("Server received %zd bytes from %s\n",
numBytes, claddr.sun_path);
/* Convert to uppercase in-place */
for (j = 0; j < numBytes; j++)
buf[j] = toupper((unsigned char) buf[j]);
/* Send the reply back to the client using sendto */
if (sendto(sfd, buf, numBytes, 0,
(struct sockaddr *) &claddr, len) != numBytes) {
perror("sendto"); exit(1);
}
}
exit(EXIT_SUCCESS);
}
recvfrom()fills incladdrwith the sender’s address automatically.- The server does NOT need to bind a client address manually โ it learns it at runtime.
remove(SV_SOCK_PATH)cleans up any leftover socket file from a previous run.
The client also needs to bind() โ unlike TCP clients, UNIX datagram clients must bind so the server has an address to reply to. The client uses its PID in the socket path to make it unique.
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define SV_SOCK_PATH "/tmp/ud_ucase_sv"
#define BUF_SIZE 10
int main(int argc, char *argv[])
{
struct sockaddr_un svaddr, claddr;
int sfd, j;
size_t msgLen;
ssize_t numBytes;
char resp[BUF_SIZE];
if (argc < 2) {
fprintf(stderr, "Usage: %s msg...\n", argv[0]);
exit(1);
}
/* Step 1: Create socket */
sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sfd == -1) { perror("socket"); exit(1); }
/* Step 2: Bind to a unique path using PID */
memset(&claddr, 0, sizeof(struct sockaddr_un));
claddr.sun_family = AF_UNIX;
snprintf(claddr.sun_path, sizeof(claddr.sun_path),
"/tmp/ud_ucase_cl.%ld", (long) getpid());
if (bind(sfd, (struct sockaddr *) &claddr,
sizeof(struct sockaddr_un)) == -1) {
perror("bind"); exit(1);
}
/* Step 3: Build server address */
memset(&svaddr, 0, sizeof(struct sockaddr_un));
svaddr.sun_family = AF_UNIX;
strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);
/* Step 4: Send each command-line argument to the server */
for (j = 1; j < argc; j++) {
msgLen = strlen(argv[j]);
if (sendto(sfd, argv[j], msgLen, 0,
(struct sockaddr *) &svaddr,
sizeof(struct sockaddr_un)) != (ssize_t)msgLen) {
fprintf(stderr, "sendto failed\n"); exit(1);
}
/* Step 5: Receive server's response */
numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
if (numBytes == -1) { perror("recvfrom"); exit(1); }
printf("Response %d: %.*s\n", j, (int) numBytes, resp);
}
/* Step 6: Remove the client's socket file when done */
remove(claddr.sun_path);
exit(EXIT_SUCCESS);
}
In UNIX domain datagram sockets, if a client does NOT bind, it has no address. The server receives the message but cannot reply because it does not know where to send the response. The client binds to a unique path using its PID to avoid conflicts when multiple clients run simultaneously.
When you call recvfrom(), you give it a buffer of a fixed size. If the incoming message is larger than your buffer, the extra bytes are silently discarded. No error is returned. This is called message truncation.
The actual output from the TLPI example confirms this:
$ ./ud_ucase_cl 'long message'
Server received 10 bytes from /tmp/ud_ucase_cl.20151
Response 1: LONG MESSA
The server only got 10 bytes even though the client sent 12. The two extra bytes (ge) were thrown away without any warning.
MSG_TRUNC flag with recvfrom() on Linux to detect truncation (the return value will be the original message size, not the truncated size).1. socket(AF_UNIX, SOCK_DGRAM, 0)
2. bind() to /tmp/cl.PID
3. sendto(server_path, “hello”)
โ
6. recvfrom() โ “HELLO”
7. remove(/tmp/cl.PID)
1. socket(AF_UNIX, SOCK_DGRAM, 0)
2. remove + bind() to /tmp/sv
3. recvfrom() โ “hello”, client addr
4. toupper(“hello”) = “HELLO”
5. sendto(client_addr, “HELLO”)
(loops forever)
$ gcc -o ud_ucase_sv ud_ucase_sv.c
$ gcc -o ud_ucase_cl ud_ucase_cl.c
# Start server in background
$ ./ud_ucase_sv &
[1] 20113
# Client sends two words
$ ./ud_ucase_cl hello world
Server received 5 bytes from /tmp/ud_ucase_cl.20150
Response 1: HELLO
Server received 5 bytes from /tmp/ud_ucase_cl.20150
Response 2: WORLD
# Send a message longer than BUF_SIZE (10 bytes)
$ ./ud_ucase_cl 'long message'
Server received 10 bytes from /tmp/ud_ucase_cl.20151
Response 1: LONG MESSA <-- truncated!
# Stop the server
$ kill %1
A: AF_UNIX (also called AF_LOCAL) sockets communicate only between processes on the same machine using file system paths as addresses. AF_INET sockets use IP addresses and ports for network communication. UNIX domain sockets are faster and have lower overhead because they bypass the network stack entirely.
A: When the client sends a message, the server needs a return address to send the reply. If the client has not called bind(), it has no named path in the file system. The server cannot reply because it has no destination address. By calling bind() with a unique path (like /tmp/cl.PID), the client creates an address the server can use.
A: The excess bytes are silently truncated. No error is returned. The call succeeds and returns the buffer size (not the original message size). This is different from TCP streams where partial reads are normal. To detect truncation on Linux, use the MSG_TRUNC flag in recvfrom().
A: Yes. Unlike UDP over the network, UNIX domain datagram sockets are reliable on the local machine. Messages are not dropped or reordered in normal operating conditions. However, if the receiver’s socket buffer is full, the sendto() call will block.
A: The recvfrom() call fills in the src_addr parameter (a struct sockaddr_un) with the sender’s bound path. The server uses this address in a subsequent sendto() call to send the reply back. This is why the client must have a bound address.
A: struct sockaddr_un is the address structure for UNIX domain sockets.
Key fields:
โข sun_family: Always set to AF_UNIX
โข sun_path: The file system path (up to 108 bytes on Linux, including null terminator)
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* socket file path */
};
A: The bind() call creates an actual file in the file system. This file persists even after the process exits. If a new client runs with the same PID (which can happen if the OS reuses PIDs), the bind() will fail because the file already exists. Calling remove(claddr.sun_path) cleans up the socket file when done.
Next: UNIX Domain Socket Permissions โ controlling access with file system permissions
