DEV Community

Cover image for Unix Domain Socket(IPC), how to make two programs communicate with each other
Marcell Cruz
Marcell Cruz

Posted on

Unix Domain Socket(IPC), how to make two programs communicate with each other

TLDR;

If you're already familiar with unix domain sockets and just want a working example take a look here

This is part 2 of a series of posts exploring techniques on how to make two different programs written in different languages send messages to each other, also known as IPC(inter-process communication).

In the previous post that you can find here I talked about using Unix piping to manage the communication, now we're going to use the networking capabilities of the system to do it.

Most people know how a TCP connection works, we're not going to use TCP here but it follows the same pattern, which basically goes as follows, Computer 1(The Server) creates a sockets and binds this socket to a specific port for new connections, Computer 2(The Client) also creates a socket and connects to this port, a connection is established and they can send and receive messages to each other, we can make a analogy with regular physical sockets here, the established connection is the cable, the sockets are sockets, and the port is the location of the socket.
All this is well and good, but it's also a little bit too much when both programs are running in the same computer, When the programs are running in different machines you need a lot of different protocols layers to establish this connection, you need routers, the Ethernet protocol, internet protocol etc..., but in our case we just want to establish communication between two programs running in the same machine, that's a perfect use case for Unix domain sockets, We can use the same process and methods that we use to connect two different computers to connect two different programs.

The Client

In this case the client is in Ruby and the server in C, the following is the code for client



require "socket"

close = false 
socket = UNIXSocket.new('/tmp/mysocket')
while close == false do
  puts ">"
  message = gets
  begin
    socket.write(message)
    response = socket.readline
    puts response
  rescue Errno::EPIPE
    puts "lost server connection!"
    close = true
    socket.close
  end
end


Enter fullscreen mode Exit fullscreen mode

The main difference here is that instead of binding the socket to a port we bind the socket to a file, the rest of the process is pretty much the same, create the socket send a message to the counter part, wait for a answer, close the connection or not depending of what you want to do, in the code above we only close the connection if the lost connection to the server otherwise we keep sending messages to the servet.

The Server

The server code is pretty much the same thing, is much bigger since it's written in C, and we're managing any possible errors.



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>

static const char* socket_path = "/tmp/mysocket";
static const unsigned int nIncomingConnections = 5;

int main() {
  //create server side
  int s = 0;
  int s2 = 0;
  struct sockaddr_un local, remote;
  int len = 0;

  s = socket(AF_UNIX, SOCK_STREAM, 0);
  if( -1 == s ) {
    printf("Error on socket() call \n");
    return 1;
  }

  local.sun_family = AF_UNIX;
  strcpy(local.sun_path, socket_path);
  unlink(local.sun_path);
  len = strlen(local.sun_path) + sizeof(local.sun_family);
  if(bind(s, (struct sockaddr*)&local, len) != 0) {
    printf("Error on binding socket \n");
    return 1;
  }

  if( listen(s, nIncomingConnections) != 0 ) {
    printf("Error on listen call \n");
  }

  int bWaiting = 1;
  while (bWaiting) {
    unsigned int sock_len = 0;
    printf("Waiting for connection.... \n");
    if( (s2 = accept(s, (struct sockaddr*)&remote, &sock_len)) == -1 ) {
      printf("Error on accept() call \n");
      return 1;
    }

    printf("Server connected \n");

    int data_recv = 0;
    char recv_buf[100];
    char send_buf[200];
    do{
      memset(recv_buf, 0, 100*sizeof(char));
      memset(send_buf, 0, 200*sizeof(char));
      data_recv = recv(s2, recv_buf, 100, 0);
      if(data_recv > 0) {
        printf("Data received: %d : %s \n", data_recv, recv_buf);
        strcpy(send_buf, "Got message: ");
        strcat(send_buf, recv_buf);

        if(strstr(recv_buf, "quit")!=0) {
          printf("Exit command received -> quitting \n");
          bWaiting = 0;
          break;
        }

        if( send(s2, send_buf, strlen(send_buf)*sizeof(char), 0) == -1 ) {
          printf("Error on send() call \n");
        }
      } else {
        printf("Error on recv() call \n");
      }
    } while(data_recv > 0);

    close(s2);
  }

  return 0;
}


Enter fullscreen mode Exit fullscreen mode

Don't be intimidated by the code, the code is as simple as the Ruby one, the main different is that we first create a socket then we bind the socket to an address just like we do for the internet socket, but in this case the address is the file location and not a port, the main reason why the C code is that much bigger is because of all the string manipulation and memory management that we need to do which we get for free in Ruby, under the hood Ruby is using the same libraries to create the socket written in C, if we run the client and the server we can send messages from the client to the server and then get the server response which in this case is the same message.

Image description

The top terminal is running the server and the bottom one the client, we type hello! in the client and hit enter to send the message to the server, we could close the connection after sending the message but we kept alive that's why we have a while loop running.

FIFO and Bidirectional piping vs Unix Domain Socket

I've already talked about FIFO and bidirectional piping here , Both of the solutions are used for IPC(inter-process communication), they both use FIFO but through different mechanisms.
The main different from an user perspective is reliability, while creating FIFO and piping data is very raw, hacky and easy to understand, it's not very controlled and reliable.

Using Unix domain sockets we can leverage all OS level functions live recv, bind, send and connect, all these functions throw errors so we have a much more controlled way of writing code that works, but if you just want to script some solution for your own personal use I don't see a problem using FIFO directly like in the bidirectional piping example, you just have to keep in mind that reliability and portability might suffer or even don't work at all if you try to run it in a different system.

Top comments (0)