1. Overview

System administrators often use inter-process communication without even knowing. Undoubtedly, this knowledge is crucial when working on and diagnosing issues with software that uses Unix sockets. In addition, knowing how to create named Unix sockets can help configure and optimize network settings.

In this tutorial, we’ll look at Unix sockets and how they work. We’ll also learn how to create named Unix sockets using the socket module of the Python programming language.

2. Sockets

Sockets are a mechanism for inter-process communication (IPC) on the same machine or over a network. In essence, a socket is an endpoint for sending and receiving data, identified by an IP address and a port number. Two main types of sockets are Unix sockets and network sockets.

Unix sockets are a type of socket that allows processes running on the same Unix-based system to exchange data. These sockets use the file system as their address space. This indicates that they are linked to a file path. A process creates a socket file in the file system to communicate with another process over a Unix socket. The other process then opens that file to establish the channel.

In short, the sender and receiver create and bind to a Unix socket, and then establish a connection by calling the connect() and accept() functions. After that, they exchange data through the socket by using standard read() and write() functions.

Furthermore, Unix sockets provide fast and efficient data transfers between processes. They don’t have the overhead of network topology. In addition, only processes running on the same machine can access them, making them secure.

3. Create Unix Socket Server

Initially, we’ll set up the Unix server socket and then explain the code.

3.1. Server Code

The first socket is the server, it accepts incoming connections from clients:

import socket
import os

# Set the path for the Unix socket
socket_path = '/tmp/my_socket'

# remove the socket file if it already exists
try:
    os.unlink(socket_path)
except OSError:
    if os.path.exists(socket_path):
        raise

# Create the Unix socket server
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

# Bind the socket to the path
server.bind(socket_path)

# Listen for incoming connections
server.listen(1)

# accept connections
print('Server is listening for incoming connections...')
connection, client_address = server.accept()

try:
    print('Connection from', str(connection).split(", ")[0][-4:])

    # receive data from the client
    while True:
        data = connection.recv(1024)
        if not data:
            break
        print('Received data:', data.decode())

        # Send a response back to the client
        response = 'Hello from the server!'
        connection.sendall(response.encode())
finally:
    # close the connection
    connection.close()
    # remove the socket file
    os.unlink(socket_path)

Now, let’s clarify the server code, line by line.

3.2. Understanding the Server Code

The first two lines of the code import the necessary socket and os modules:

import socket
import os

The socket module provides a networking interface, while os enables access to functions of the operating system. The latter includes file I/O and process management.

Next, the code sets the path for the Unix socket. The socket_path variable holds the path for the Unix socket. The code also tries to remove the socket file if it already exists. If the file can’t be removed, the code raises an OSError:

# Set the path for the Unix socket
socket_path = '/tmp/my_socket'

# remove the socket file if it already exists
try:
    os.unlink(socket_path)
except OSError:
    if os.path.exists(socket_path):
        raise

The script creates a Unix socket server using the socket() function provided by the socket module. It takes the address family as the first parameter, which is set to AF_UNIX for Unix domain sockets, and the socket type as the second parameter, which is set to SOCK_STREAM for a TCP socket. The bind() method is called to bind the socket to the path after creating the server:

# Create the Unix socket server
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

# Bind the socket to the path
server.bind(socket_path)

Then the script calls the listen() method to listen for incoming connections. The parameter we pass to listen() is the maximum number of clients. The server waits for incoming connections using accept(). The accept() method blocks until a client connects, at which point it returns a connection object and the address of the client:

# Listen for incoming connections
server.listen(1)

# accept connections
print('Server is listening for incoming connections...')
connection, client_address = server.accept()

Once a connection is made, the server receives data from the client using the recv() method. In this case, the argument to recv() is the maximum amount of data we can receive. If we get data, we print it to the console. Finally, the server sends a response back to the client using the sendall() method:

try:
    print('Connection from', client_address)

    # receive data from the client
    while True:
        data = connection.recv(1024)
        if not data:
            break
        print('Received data:', data.decode())

        # Send a response back to the client
        response = 'Hello from the server!'
        connection.sendall(response.encode())

At this point, we can close the connection with the close() method. Moreover, we remove the socket file using the os.unlink() method to to clean up any leftover resources:

finally:
    # close the connection
    connection.close()
    # remove the socket file
    os.unlink(socket_path)

Now, we have our server script ready.

4. Create Unix Socket Client

Now, we’ll also set up the Unix client socket and explain the code similarly.

4.1. Client Code

On the other end, we set up a client:

import socket
import os

# Set the path for the Unix socket
socket_path = '/tmp/my_socket'

# Create the Unix socket client
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

# Connect to the server
client.connect(socket_path)

# Send a message to the server
message = 'Hello from the client!'
client.sendall(message.encode())

# Receive a response from the server
response = client.recv(1024)
print(f'Received response: {response.decode()}')

# Close the connection
client.close()

In short, this code creates a Unix socket client that can send and receive data to and from a Unix socket server.

4.2. Understanding the Client Code

The first two lines import the necessary modules (socket and os):

import socket
import os

The socket_path variable sets the path of the Unix socket that the client will connect to. This should match the path used by the Unix socket server, otherwise, the client won’t be able to connect. We create the client with the socket() method. In it, we specify the AF_UNIX family of protocols and the SOCK_STREAM socket type:

# Set the path for the Unix socket
socket_path = '/tmp/my_socket'

# Create the Unix socket client
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

After creating the client, we connect to the Unix socket server with connect(). The method takes the socket_path as its argument. After connecting to the server, the client sends a message to it using the sendall() method. The client encodes the message before sending it:

# Connect to the server
client.connect(socket_path)

# Send a message to the server
message = 'Hello from the client!'
client.sendall(message.encode())

Next, the client waits to receive a response from the server. The code uses the recv() method to wait for incoming data and stores the response in the response variable. After that, it prints the response to the console using the decode() method, which converts the bytes received to a string:

# Receive a response from the server
response = client.recv(1024)
print(f'Received response: {response.decode()}')

Finally, the script closes the connection using the close() method:

# Close the connection
client.close()

We have our client script ready.

5. Testing the Setup

Notably, we need to run both scripts on the same machine for the Unix socket to work properly. To make the connection, we first need to run the server and then the client script:

$ python server.py 
Server is listening for incoming connections...

Afterward, we run the client script on another terminal:

$ python client.py 
Received response: Hello from the server!

Let’s check the terminal that’s running the server script:

$ python server.py 
Server is listening for incoming connections...
Connection from fd=4
Received data: Hello from the client!

We’ve successfully set up Unix sockets for both the server and the client.

6. Conclusion

In this article, we’ve learned about Unix sockets and how they work. We’ve also looked at how to create named Unix sockets by setting up a server and client using the socket Python module.