The Simple Guide to Unix Sockets

Josh Weinstein
7 min readAug 28, 2022
Photo by Fré Sonneveld on Unsplash

The backbone of the internet and all networking is the socket. Sockets provide the ability to send and receive data between one address and another , either on the same machine, or completely different ones. Close to a dozen kinds of sockets exist for different types of addresses, and communication. Sockets run different protocols , like TCP, which dictate the style of communication that takes place among them. The address type of a socket determines, in part, if it supports cross-machine or intra-machine communication. Unix sockets, also called unix domain sockets, are a type of intra-machine socket that are supported on unix systems, and modern windows versions.

Unlike a port-based socket, a unix socket is represented by a filesystem path. Yet, similar to how only a single socket can bind to a port at a time, only a single unix socket can bind to a particular filesystem path at a time. A program can connect to a unix socket by connecting to a particular path, rather than an IP address and port, as unix sockets aren’t IP based.

In this tutorial, we will implement a unix socket server that forks a child process and communicates with it over the socket. This is done just for simplicity of compiling and running code. It’s perfectly possible to have separate client and server unix socket programs.

Setting up the server

The first step in being able to listen to and receive connections on a unix socket is creating it. We need to setup a few includes and definitions that will be used in creating the socket. These include some system header files specific to unix, as well as the path we want our socket to be at.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <errno.h>
//--------system headers -------//
#include <unistd.h>
#include <sys/un.h>
#include <sys/socket.h>
const char* SOCKET_FILE = "/tmp/fgfgdgdgff";#define BUFFER_SPACE 100

For experimental purposes, it’s fine to store socket files under /tmp . But any real setup would want a more secure location or setup. Socket files can have any extension, but typically the extension ".sock" is used.

Next, we can create the actual socket. We know we want a unix socket, and for the purposes of this guide, we will create a stream socket. This means the socket undergoes connection based communication. It is also possible to create unix datagram sockets, which communicate without connections.

int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
fprintf(stderr, "Failed to create socket, err=%d\n", errno);
exit(4);
}

If socket() returns -1, it means the socket failed to be created, and thus we should exit and report errno to help diagnose the issue. Now that the socket is created, it must be bound to a unix address.

A unix socket address is represented by the type struct sockaddr_un . This structure holds key information, like the file system path the socket binds to. The next step is to write the socket family and path into the address structure.

struct sockaddr_un addr;if (strlen(SOCKET_FILE) > sizeof(addr.sun_path) - 1) {
fprintf(stderr, "Socket name too long, socket sun path is size %zu\n", sizeof(addr.sun_path) - 1);
exit(1);
}
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_FILE, sizeof(addr.sun_path) - 1);

It’s possible in theory our socket path can be too long to fit into the buffer of the address structure, thus it’s proper to safely check for that. Once the address socket is all set up, we can bind our socket to it.

if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) {
fprintf(stderr, "Failed to bind, err=%d\n", errno);
exit(3);
}

This step associates the socket object in the program with the file system path of the unix address. Thus, it is possible to fail this step and encounter a message like Failed to bind, err=48 . When errno is 48, it means the address is already in use. Similar to ports, the path chosen for a socket must not already be in use.

Once binding is successful, the program can listen on the address for incoming connections.

if (listen(sfd, 5) == -1) {
fprintf(stderr, "Failed to listen err=%d\n", errno);
exit(2);
}

The 5 argument means the number of connections that can queue up from clients before we call accept() . Calling listen() signifies the beginning of connection serving to external clients. If connections queue up beyond the passed in limit, those additional connections will be refused.

To actually receive those connections, accept() needs to be called in some form of loop. accept() is also a blocking call, meaning if there’s no incoming connections, the thread whom calls it will be blocked until a connection is received. Once a connection is received, accept() returns a file descriptor representing that client’s connection. As long as accept does not return -1, this means a connection between server and client has been successfully established, and reading or writing communication can take place.

For simplicity, this server will just read data it receives via a connection and write that data to stdout . While surviving a request, checking the return values of read and write can pick up errors that occur.

while (1) {
cfd = accept(sfd, NULL, NULL);
if (cfd == -1) {
fprintf(stderr, "Cannot accept, err=%d\n", errno);
exit(5);
}
while ((numRead = read(cfd, buf, BUFFER_SPACE)) > 0) {
if (write(STDOUT_FILENO, buf, numRead) != numRead) {
fprintf(stderr, "Cannot write to stdout, err=%d\n", errno);
exit(6);
}
}
if (numRead == -1) {
fprintf(stderr, "Error while reading, err=%d\n", errno);
exit(8);
}
if (close(cfd) == -1) {
fprintf(stderr, "Cannot close connected fd, err=%d\n", errno);
exit(9);
}
}

The first step in the loop is calling accept() and checking if it fails. Then a while loop begins to read data from the connected file descriptor while there’s still data to read. For every chunk of data read, that data is written to stdout. After all the loop terminates, we do one last check if reading failed, then close the connection

The Client

Now that the server portion of this tutorial has been shown, its time to discuss the client and it’s implementation. For simplicity, the client will be forked from the server process and communicate to the server on it’s on. To do that, we need a separate function where the forking will happen. Additionally, we want the server process to not execute any of the code intended for the child process. To accomplish this requirement, the fork() function and it’s return value can be used.

static void do_client(void) {
pid_t child = fork();
if (child != 0) {
return;
}
...........
}

When fork() is called, a process splits it’s state into its own and a child process. However, the return value for fork() will be different. In the parent process, it returns the pid of the child, in the child process, it returns 0. Thus we can reliably control the flow of execution by checking this return value.

There’s a few different places in the execution of the server we can call our do_client function. If it’s called before the server’s socket is bound and listening, the client must handle the possibility the connection will be refused when it tries to connect to the unix address. If the client is forked after the server begins listening on the socket, there’s no need for the client to be concerned about the socket address not existing yet. In general, it’s important to keep in mind without any locks, the behavior between two processes is not kept in sync.

Here’s the code to handle the possibility of connection refusal while connecting.

struct sockaddr_un addr;int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
fprintf(stderr, "Could not create socket, err=%d\n", errno);
exit(4);
}
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_FILE, sizeof(addr.sun_path) - 1);
while (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) {
if (errno == ECONNREFUSED) {
fprintf(stderr, "connection refused, retrying\n");
continue;
}
fprintf(stderr, "Failed to connect to socket, err=%d\n", errno);
exit(4);
}

The setup is similar to the server, both clients and server for unix sockets have to create a socket object. The difference is servers bind and listen to addresses, clients connect to addresses.

After the client process establishes a connection, it can write some data to the server, then exit gracefully.

if (write(sfd, "Foody", 5) != 5) {
fprintf(stderr, "Failed to write to socket, err=%d\n", errno);
exit(5);
}
if (write(sfd, "Foody", 5) != 5) {
fprintf(stderr, "Failed to write to socket, err=%d\n", errno);
exit(5);
}
if (write(sfd, "Foody", 5) != 5) {
fprintf(stderr, "Failed to write to socket, err=%d\n", errno);
exit(5);
}
exit(0);

Each of the calls to write() just check that the number of bytes intended to be written were actually written.

Putting it Altogether

There’s a few more parts one should add before this sample program is considered done. Unix sockets exist at specific file system paths. But that means that means once the socket is bound to an address, even after the server exits, the file is still there. Thus, to ensure the program will work after running more than once, remove the socket path if it exists before binding the server.

Lastly, this program is not setup to handle SIGPIPE , which is a signal a process sends to another process connected to the same socket if the pipe breaks. So as might as well ignore it

signal(SIGPIPE, SIG_IGN);
// Make sure to handle removal of socket
if (remove(SOCKET_FILE) == -1 && errno != ENOENT) {
fprintf(stderr, "Failed to remove socket path %s\n", SOCKET_FILE);
exit(4);
}

The final , full code, is below

--

--

Josh Weinstein

I’m an engineer on a mission to write the fastest software in the world.