Interprocess Communication using Unix Domain Sockets

1.0 Unix Domain Sockets

A socket is a communication endpoint at a host computer. The socket API provides calls for communication between processes. The socket system call is,

int socket (int domain, int type, int protocol);

The first parameter to the socket system call is the domain. The domain is actually the communication domain and it selects the protocol family to be used for communication. For Unix domain sockets, the domain is AF_UNIX and it is for local communication. A process can create a socket of domain AF_UNIX and communicate with other processes using it.

Why use Unix domain sockets? After all, we can use the domain as AF_INET or AF_INET6 for creating a socket of IPv4 or IPv6 Internet protocol families respectively and use the localhost for loopback address for local communication. And, that way we have the flexibility to put processes on the same host or different hosts on the network. The argument for using Unix domain sockets is that they are fast and easy to use. In fact, they are being use abundantly in your Linux system. You can check out the Unix domain sockets on your system by giving the command, "ss -f unix". Also, since the communication is local, you don't have to worry about network byte order for numbers sent between processes.

2.0 Call sequence

Interprocess communication mechanisms like FIFOs are symmetrical because the server and client make the same calls. The socket API is asymmetrical bacause the server and client make different sequence of system calls. The server makes the following calls: socket, bind, listen, accept, and then, read and write. The client makes socket, connect, and then, read and write.

3.0 socket

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

unix_socket = socket (AF_UNIX, type, 0);

The first parameter in the socket call is the domain, AF_UNIX. The second parameter, socket type, can be SOCK_STREAM, for stream-oriented sockets, SOCK_DGRAM, for datagram sockets, or, SOCK_SEQPACKET, for sequenced packet socket. Datagram sockets for Unix domain are reliable and datagrams are not reordered. SOCK_SEQPACKET provides a connection oriented communication, preserving message boundaries and delivering messages in the order they were sent.

4.0 Socket address

The socket address is a very involved structure in network programming. However, it is a little simpler in the case of Unix domain sockets.

struct sockaddr_un {
    sa_family_t sun_family;                /* AF_UNIX */ 
    char        sun_path [108];            /* pathname */
};         

The first member, sun_family is always AF_UNIX. The second member, sun_path, is the pathname of the socket in the filesystem.

5.0 Example: Maintenance System

As an example, consider a maintenence system of an apartment complex. The system comprises of a Complaint Logging Server and clients. People staying in the apartment complex log their problems in the server. The maintenance staff take a complaint from the server, do necessary work and resolve the issue. Once, a complaint is resolved, it is deleted from the server. So, a client can do three things. First, it can create a new complaint. Second, it can ask the server for pending complaints. And, lastly, it can delete a complaint (after resolution).

5.1 Complaint Server

The server and clients communicate over Unix domain sockets. The server has a "public" socket, /tmp/complaint-server.socket. Clients connect to this socket. Once, a client's connect is accepted by the server, a new socket is created for data transmission between the server and the client.

The server code is as follows.

/* 
 *    complaint-server.c: Log and manage complaints
 *
 *    Copyright (c) 2020 SoftPrayog.in
 *
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/select.h>
#include <sys/stat.h>

#define COMPLAINT_SOCKET       "/tmp/complaint-server.socket"

#define BACKLOG                   10

#define LOG_COMPLAINT             1
#define GIVE_ME_A_COMPLAINT       2
#define GIVE_COMPLAINT4APT        3
#define RESOLVE_COMPLAINT         4
#define QUIT                      0

#define NEXT_COMPLAINT            11
#define NO_MORE_COMPLAINTS        12
#define NO_COMPLAINT4THIS_APT     13
#define COMPLAINT_ADDED           14
#define COMPLAINT_DELETED         15

#define MAX_CLIENTS 200

struct message {
    int32_t message_id;
    char apartment_id [8];
    char remarks [128]; 
};

struct element {
    char *apartment_id;
    char *remarks; 
    bool accessed;
    struct element *next;
};

struct element *tail;

struct message recv_message, send_message;

void add_to_complaint_q (char *source_apartment_id, char *source_remarks, char *dest_apartment_id);
int give_next_complaint (char *apartment_id, char *remarks);
int give_complaint (char *source_apartment_id, char *dest_apartment_id, char *remarks);
int del_complaint (char *source_apartment_id, char *dest_apartment_id);
void reset_accessed_all (void);
void error (char *msg);

int main (int argc, char **argv)
{
    printf ("Complaint-server: Hello, World!\n");
    // initialize the complaint queue
    tail = NULL;

    // create a unix domain socket, COMPLAINT_SOCKET
    // unlink, if already exists
    struct stat statbuf;
    if (stat (COMPLAINT_SOCKET, &statbuf) == 0) {
        if (unlink (COMPLAINT_SOCKET) == -1)
	    error ("unlink");
    }
    
    int listener;

    if ((listener = socket (AF_UNIX, SOCK_SEQPACKET, 0)) == -1)
	error ("socket");

    struct sockaddr_un socket_address;

    memset (&socket_address, 0, sizeof (struct sockaddr_un));
    socket_address.sun_family = AF_UNIX;
    strncpy (socket_address.sun_path, COMPLAINT_SOCKET, sizeof(socket_address.sun_path) - 1);

    if (bind (listener, (const struct sockaddr *) &socket_address, sizeof (struct sockaddr_un)) == -1)
        error ("bind");

    // Mark socket for accepting incoming connections using accept
    if (listen (listener, BACKLOG) == -1)
        error ("listen");

    fd_set fds, readfds;
    FD_ZERO (&fds);
    FD_SET (listener, &fds);
    int fdmax = listener;

    printf ("Complaint-server: Waiting for a message from a client.\n");
    while (1) {
        readfds = fds;
        // monitor readfds for readiness for reading
        if (select (fdmax + 1, &readfds, NULL, NULL, NULL) == -1)
            error ("select");
        
        // Some sockets are ready. Examine readfds
        for (int fd = 0; fd < (fdmax + 1); fd++) {
            if (FD_ISSET (fd, &readfds)) {  // fd is ready for reading 
                if (fd == listener) {  // request for new connection
                    int fd_new;
                    if ((fd_new = accept (listener, NULL, NULL)) == -1)
                        error ("accept");

                    FD_SET (fd_new, &fds); 
                    if (fd_new > fdmax) 
                        fdmax = fd_new;
		    fprintf (stderr, "complaint-server: new client\n");
                }
                else  // data from an existing connection, receive it
                {
                    memset (&recv_message, '\0', sizeof (struct message));
                    ssize_t numbytes = read (fd, &recv_message, sizeof (struct message));
   
                    if (numbytes == -1)
                        error ("read");
                    else if (numbytes == 0) {
                        // connection closed by client
                        fprintf (stderr, "Socket %d closed by client\n", fd);
                        if (close (fd) == -1)
                            error ("close");
                        FD_CLR (fd, &fds);
                    }
                    else 
                    {
                        // data from client
			memset (&send_message, '\0', sizeof (struct message));
                        switch (recv_message.message_id) {
                            case LOG_COMPLAINT:
                                   add_to_complaint_q (recv_message.apartment_id, recv_message.remarks, send_message.apartment_id);
				       send_message.message_id = COMPLAINT_ADDED;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");
                                   break;

			    case GIVE_ME_A_COMPLAINT: 
                                   if (give_next_complaint (send_message.apartment_id, send_message.remarks) == -1) {
				       // error: No more complaints
				       send_message.message_id = NO_MORE_COMPLAINTS;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");
				   }
				   else
				   {
				       send_message.message_id = NEXT_COMPLAINT;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");

				   }
				   break;

			    case GIVE_COMPLAINT4APT:
                                   if (give_complaint (recv_message.apartment_id, send_message.apartment_id, send_message.remarks) == -1) {
				       // no complaint for this apartment
				       send_message.message_id = NO_COMPLAINT4THIS_APT;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");
				   }
				   else
				   {
				       send_message.message_id = NEXT_COMPLAINT;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");
			           }
				   break;

                            case RESOLVE_COMPLAINT:
                                   if (del_complaint (recv_message.apartment_id, send_message.apartment_id) == -1) {
				       // error: No complaint found for this apartment
				       send_message.message_id = NO_COMPLAINT4THIS_APT;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");
				   }
				   else
				   {
				       // complaint deleted
				       send_message.message_id = COMPLAINT_DELETED;
                                       if (write (fd, &send_message, sizeof (struct message)) == -1)
                                           error ("write");
				   }
                                   break;

			    case QUIT:
                                  if (close (fd) == -1)
                                      error ("close");
                                  FD_CLR (fd, &fds);
				   break;

			    default: fprintf (stderr, "Complaint-server: Unexpected message from client\n"); 
                        }
                    }
                }
            } // if (fd == ...
        } // for
    } // while (1)
    exit (EXIT_SUCCESS);
} // main

// add a new complaint to the complaint queue
void add_to_complaint_q (char *source_apartment_id, char *source_remarks, char *dest_apartment_id)
{
    struct element *ptr;
    char *cp1, *cp2;

    if ((ptr = (struct element *) malloc (sizeof (struct element))) == NULL)
        error ("malloc");
    if ((cp1 = (char *) malloc (strlen (source_apartment_id) + 1)) == NULL)
        error ("malloc");

    strcpy (cp1, source_apartment_id);
    strcpy (dest_apartment_id, source_apartment_id);
    ptr -> apartment_id = cp1;

    if ((cp2 = (char *) malloc (strlen (source_remarks) + 1)) == NULL)
        error ("malloc");

    strcpy (cp2, source_remarks);
    ptr -> remarks = cp2;
    ptr -> accessed = false;

    if (tail == NULL) {
        ptr -> next = ptr;
    }
    else
    {
        ptr -> next = tail -> next;
        tail -> next = ptr;
    }
    tail = ptr;
}

int del_complaint (char *source_apartment_id, char *dest_apartment_id) // returns -1 on error
{
    struct element *ptr, *prev;
    char *cp;

    strcpy (dest_apartment_id, source_apartment_id);

    if (!tail) {
        fprintf (stderr, "del_complaint: Queue is empty\n");
        return -1;
    }
    // get the head
    prev = tail;
    ptr = tail -> next;
    while (1) {
        if (strcmp (source_apartment_id, ptr -> apartment_id) == 0)
	    break;

	if (ptr == tail) 
	    return -1;

	prev = ptr;
	ptr = ptr -> next;
    }

    free (ptr -> apartment_id);
    free (ptr -> remarks);

    if (ptr == tail) {
        if (ptr == prev) {
	    tail = NULL;
            free (ptr);
	    return 0;
        }
	else
	{
            tail = prev;
	}
    }
    prev -> next = ptr -> next;
    free (ptr);
    return 0;
}

int give_next_complaint (char *apartment_id, char *remarks)
{
    struct element *ptr;

    if (!tail) {
        fprintf (stderr, "give_next_complaint: Queue is empty\n");
        return -1;
    }
    // get the head
    ptr = tail -> next;
    while (1) {
        if (!ptr -> accessed) 
	    break;

	if (ptr == tail) {
	    reset_accessed_all ();
            ptr = tail -> next;
	    continue;
	}
	ptr = ptr -> next;
    }

    strcpy (apartment_id, ptr -> apartment_id);
    strcpy (remarks, ptr -> remarks);
    ptr -> accessed = true;

    return 0;
}

void reset_accessed_all (void)
{
    struct element *ptr;

    if (!tail) {
        fprintf (stderr, "reset_accessed_all: Queue is empty\n");
        return;
    }
    // get the head
    ptr = tail -> next;
    while (1) {
        ptr -> accessed = false;

	if (ptr == tail)
	    return;

	ptr = ptr -> next;
    }
}

int give_complaint (char *source_apartment_id, char *dest_apartment_id, char *remarks)
{
    struct element *ptr;
    char *cp;

    strcpy (dest_apartment_id, source_apartment_id);
    if (!tail) {
        fprintf (stderr, "give_complaint: Queue is empty\n");
        return -1;
    }
    // get the head
    ptr = tail -> next;
    while (1) {
        if (strcmp (source_apartment_id, ptr -> apartment_id) == 0)
	    break;

	if (ptr == tail) 
	    return -1;

	ptr = ptr -> next;
    }
    strcpy (remarks, ptr -> remarks);

    return 0;
}

void error (char *msg)
{
    perror (msg);
    exit (1);
}

5.2 Clients

The client has commands to record a new complaint, get next complaint from the queue, get complaint for a specific apartment and clear a complaint. The client code is as follows.

/* 
 *    complaint-client.c: client for logging and managing complaints
 *
 *    Copyright (c) 2020 SoftPrayog.in
 *
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/select.h>
#include <sys/stat.h>

#define COMPLAINT_SOCKET       "/tmp/complaint-server.socket"

#define MAX_MESSAGE_SIZE          256

#define LOG_COMPLAINT             1
#define GIVE_ME_A_COMPLAINT       2
#define GIVE_COMPLAINT4APT        3
#define RESOLVE_COMPLAINT         4
#define QUIT                      0

#define NEXT_COMPLAINT            11
#define NO_MORE_COMPLAINTS        12
#define NO_COMPLAINT4THIS_APT     13
#define COMPLAINT_ADDED           14
#define COMPLAINT_DELETED         15

struct message {
    int32_t message_id;
    char apartment_id [8];
    char remarks [128]; 
};

struct message message;
int sock_fd;

int get_input (void);
void error (char *msg);

int main (int argc, char **argv)
{

    if ((sock_fd = socket (AF_UNIX, SOCK_SEQPACKET, 0)) == -1)
	error ("socket");

    struct sockaddr_un socket_address;

    memset (&socket_address, 0, sizeof (struct sockaddr_un));
    socket_address.sun_family = AF_UNIX;
    strncpy (socket_address.sun_path, COMPLAINT_SOCKET, sizeof (socket_address.sun_path) - 1);

    if (connect (sock_fd, (const struct sockaddr *) &socket_address, sizeof (struct sockaddr_un)) == -1)
	error ("connect");

    int option;
    bool over = false;

    while (!over) {
         option = get_input ();

         // send request to server
         if (write (sock_fd, &message, sizeof (struct message)) == -1)
             error ("write");
         memset (&message, '\0', sizeof (struct message));
         // receive response from server
         if (read (sock_fd, &message, sizeof (struct message)) == -1)
             error ("read");

         // process server response 
         switch (message.message_id) {
             case COMPLAINT_ADDED: 
		    printf ("\nComplaint for Apartment id: %s has been added.\n\n", message.apartment_id);
                    break;

             case NEXT_COMPLAINT: 
		    printf ("\nCOMPLAINT\n\tApartment id: %s\n\tRemarks: %s\n\n", message.apartment_id, message.remarks);
                    break;

             case NO_MORE_COMPLAINTS: 
		    printf ("\nNo more complaints\n\n");
                    break;

             case NO_COMPLAINT4THIS_APT: 
		    printf ("\nThere is no existing Complaint for Apartment id: %s.\n\n", message.apartment_id);
                    break;

             case COMPLAINT_DELETED: 
		    printf ("\nComplaint for Apartment id: %s has been deleted.\n\n", message.apartment_id);
                    break;

             case QUIT: over = true;
                    break;

             default: printf ("\nUnrecongnized message from server\n\n");
         }
    }

    exit (EXIT_SUCCESS);
}

char inbuf [512];

int get_input (void)
{
    int option;

    while (1) {
        printf ("COMPLAINTS\n\n");
        printf ("\tRecord a new complaint\t1\n");
        printf ("\tGet next complaint\t2\n");
        printf ("\tGet complaint\t3\n");
        printf ("\tClear complaint\t4\n");
        printf ("\tQuit\t\t0\n\n");
        printf ("Your option: ");
        if (fgets (inbuf, sizeof (inbuf),  stdin) == NULL)
            error ("fgets");
        sscanf (inbuf, "%d", &option);

        int len;

        switch (option) {

            case 1: message.message_id = LOG_COMPLAINT;
                    printf ("Apartment id: ");
                    if (fgets (inbuf, sizeof (inbuf),  stdin) == NULL)
                        error ("fgets");
                    len = strlen (inbuf);
                    if (inbuf [len - 1] == '\n')
                        inbuf [len - 1] = '\0';
                    strcpy (message.apartment_id, inbuf);
                    printf ("Description: ");
                    if (fgets (inbuf, sizeof (inbuf),  stdin) == NULL)
                        error ("fgets");
                    len = strlen (inbuf);
                    if (inbuf [len - 1] == '\n')
                        inbuf [len - 1] = '\0';
                    strcpy (message.remarks, inbuf);
                    break;

            case 2: message.message_id = GIVE_ME_A_COMPLAINT;
                    break;

            case 3: message.message_id = GIVE_COMPLAINT4APT;
                    printf ("Apartment id: ");
                    if (fgets (inbuf, sizeof (inbuf),  stdin) == NULL)
                        error ("fgets");
                    len = strlen (inbuf);
                    if (inbuf [len - 1] == '\n')
                        inbuf [len - 1] = '\0';
                    strcpy (message.apartment_id, inbuf);
                    break;

            case 4: message.message_id = RESOLVE_COMPLAINT;
                    printf ("Apartment id: ");
                    if (fgets (inbuf, sizeof (inbuf),  stdin) == NULL)
                        error ("fgets");
                    len = strlen (inbuf);
                    if (inbuf [len - 1] == '\n')
                        inbuf [len - 1] = '\0';
                    strcpy (message.apartment_id, inbuf);
                    break;

            case 0: message.message_id = QUIT;
                    break;

            default: printf ("Illegal option, try again\n\n");
                     continue;

        }

        return option;
    }
}

void error (char *msg)
{
    perror (msg);
    exit (1);
}

We can compile and run the server and client programs.

$ gcc complaint-server.c -o complaint-server
$ gcc complaint-client.c -o complaint-client
$ ./complaint-server &
$ Complaint-server: Hello, World!
Complaint-server: Waiting for a message from a client.

$ ./complaint-client
COMPLAINTS

	Record a new complaint	1
	Get next complaint	2
	Get complaint	3
	Clear complaint	4
	Quit		0

Your option: 

A screenshot of server running along with three clients is given below.

Complaint server and three clients