IPC – Unix Sockets explained

When I wrote about Inter Process Communication, shared memory and signals I promised I’ll suggest an easier way of sending messages between process in a way that one of them gets notified. Shared memory and signals is great, but it wasn’t the right choice for my example. Today I’ll explain so-called Unix sockets and as always I’ll give you an example.

What is a network socket

You might already be familiar with the concept of a socket, but let’s revise it. A socket is something that let’s revise this knowledge. A socket is an endpoint used for communication – usually network one. So when you open your browser, it uses sockets to read a web page, same applies for servers. In Unix world everything is a file, or to be a bit more precise, OS represents every device as a file. When you run a server, its sockets are “files”. Let’s spawn a listening server with netcat by running nc -l 127.0.0.1 8080 and list its files with lsof:

➜  ~ lsof -c nc
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF                NODE NAME
nc      13437 gonczor  cwd    DIR                1,6      864            20758717 /Users/gonczor
nc      13437 gonczor  txt    REG                1,6   203632 1152921500312810925 /usr/bin/nc
nc      13437 gonczor  txt    REG                1,6  2160672 1152921500312811906 /usr/lib/dyld
nc      13437 gonczor    0u   CHR               16,0  0t22384                1159 /dev/ttys000
nc      13437 gonczor    1u   CHR               16,0  0t22384                1159 /dev/ttys000
nc      13437 gonczor    2u   CHR               16,0  0t22384                1159 /dev/ttys000
nc      13437 gonczor    3u  IPv4 0x19bdae2b0a7886e3      0t0                 TCP localhost:http-alt (LISTEN)

In the last line the file is of IPv4 (notice that directories are also represented as files), with its node and a few other things that are not relevant now. How does writing to such a socket look like? Let’s analyze a simple program in C I’ve mostly copied from here.

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

int main(int argc, char *argv[])
{
    int sockfd = 0, n = 0;
    const char* message = "Hello, world!\n";
    struct sockaddr_in serv_addr; 

    if(argc != 3)
    {
        printf("\n Usage: %s <ip of server> <port>\n",argv[0]);
        return 1;
    }

    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("\n Error : Could not create socket \n");
        return 1;
    } 

    memset(&serv_addr, '0', sizeof(serv_addr)); 

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2])); 

    if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
    {
        printf("\n inet_pton error occured\n");
        return 1;
    } 

    if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
       printf("\n Error : Connect Failed \n");
       return 1;
    } 

    write(sockfd, message, strlen(message)); 

    close(sockfd);

    return 0;
}

At the end of the post you’ll find a link to repository with all the source code. Anyway, what is important here is the write() call at the bottom. We can use other functions like send, but I wanted to show that you can use the same function for writing to files and to the socket. The server should react in the following way receiving the data and displaying it:

➜  IPC git:(master) ✗ nc -l 127.0.0.1 8080
Hello, world!

But what if you wanted to communicate 2 processes running in your memory on the same machine? Can it be done better?

What are Unix sockets

I’m just going to shamelessly copy an answer from serverfault giving a good overall idea of what Unix sockets are:

A UNIX socket, AKA Unix Domain Socket, is an inter-process communication mechanism that allows bidirectional data exchange between processes running on the same machine.
IP sockets (especially TCP/IP sockets) are a mechanism allowing communication between processes over the network. In some cases, you can use TCP/IP sockets to talk with processes running on the same computer (by using the loopback interface).
UNIX domain sockets know that they’re executing on the same system, so they can avoid some checks and operations (like routing); which makes them faster and lighter than IP sockets. So if you plan to communicate with processes on the same host, this is a better option than IP sockets.

An example use case? I remember working for a company that had on premise servers that would run web applications written in Python. For a few reasons we wanted to use Nginx server in front of them – better scaling when spawning new processes, better static files handling and so on. What we did was to spawn Nginx as one process and make it a reverse proxy pointing to sockets. We would then make our python application listen on the same sockets. We can configure server to do this by adding the following line:

listen unix:/var/run/nginx.sock;

Source.

Implementation

Let’s take a look at the client implementation:

#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/un.h>


int main(int argc, char *argv[])
{
    int sockfd = 0, len = 0, socket_path_len = 0;
    const char* message = "Hello, world!\n";
    const char* socket_path = "local.sock";
    struct sockaddr_un serv_addr; 

    if((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
    {
        perror("\nCould not create socket \n");
        return 1;
    } 

    serv_addr.sun_family = AF_UNIX;
    socket_path_len = strlen(socket_path);
    strncpy(serv_addr.sun_path, socket_path, socket_path_len+1); 
    len = socket_path_len + 1 + sizeof(serv_addr.sun_family);
    printf("%s: %d\n", socket_path, socket_path_len);

    if (connect(sockfd, (struct sockaddr *)&serv_addr, len) == -1)
    {
       perror("\nConnection failed \n");
       return 1;
    } 

    if (write(sockfd, message, strlen(message)) == -1)
    {
        perror("\nWriting data unsuccessful\n");
        return 1;
    }

    close(sockfd);

    return 0;
}

There are a few differences worth discussing. First we used AF_UNIX instead of AF_INET to create socket: socket(AF_UNIX, SOCK_STREAM, 0). Second the binding process is a bit different as we don’t need to choose both address and port, only “address” in form of a disk space. Moreover, there is no need to translate the address string into address structure:

The inet_pton() function converts a presentation format address (that is, printable form as held in a character string) to network format (usually a struct in_addr or some other
internal binary representation, in network byte order).

After the initialization part is done, sending data with write() function is exactly the same. One catch I came across is correctly counting the buffer sizes – hence the +1 in strncpy() and len counting.

I’ve also written a simple python server that displays data received over the network:

#!/usr/bin/env python3

import socket
import sys
import os

ADDRESS = "./local.sock"

if os.path.exists(ADDRESS):
    os.unlink(ADDRESS)


print("# Creating server...")
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
    server.bind(ADDRESS)
    server.listen(1)
    print("# Server created")
    while True:
        connection, client = server.accept()
        if data := connection.recv(0x1_000):
            print(f"Received: {data.decode('utf-8')}")
        else:
            print("Error")
            break
finally:
    server.close()

The process here is even more simple. Again I needed to use AF_UNIX instead of AF_INET and point to a file instead of address and port tuple. We can now run the server and the client:

server run and received "Hello, world" message sent by client through Unix socket.
Run example

Summary

That’s all for today. As always you can find working examples on my GitLab (remember that those will only work on Unix systems – macOS/Linux). You may also want to check out the latest posts: