What are Unix Sockets?

Unix Sockets

A UNIX socket, AKA Unix Domain Socket, is an inter-process communication mechanism that allows bidirectional data exchange between processes. It is similar to TCP/IP sockets, but it does not use the network stack, and it is used for communication between processes on the same machine.

Why use Unix Sockets?

Unix sockets have various advantages as mentioned below:

  1. 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.
  2. They are also more secure because they are not exposed to the network.
  3. They are also more reliable to use because they are not dependent on the network.

How to use Unix Sockets in Go?

We will be using the Gin web framework which allows us to route unix sockets pretty similarly like APIs. This makes the code more easy and familiar to work with.

Lets create a Gin server which listens on a Unix Socket and returns a the current ram usage of the process.


package main

import (
	"fmt"
	"net"
	"net/http"
	"runtime"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()

	router.GET("/ram", func(c *gin.Context) {
		var m runtime.MemStats
		runtime.ReadMemStats(&m)

		// Convert bytes to megabytes
		mb := m.Alloc / 1024 / 1024

		c.String(http.StatusOK, fmt.Sprintf("Current RAM usage: %v MB", mb))
	})

	listener, err := net.Listen("unix", "/tmp/demo.sock")
	if err != nil {
		panic(err)
	}

	http.Serve(listener, router)
}

Code Explanation

  1. We are using the net.Listen function to listen on a Unix socket.
  2. The first argument is the network type, which is unix in our case. The second argument is the path to the Unix socket file.
  3. It is preferred to use the /tmp directory for the Unix socket file, as it is a common location for temporary files.
  4. We are using the http.Serve function to serve the requests on the Unix socket.
  5. As per the router, we have a single route /ram which returns the current RAM usage of the process.

Now, lets create the client which connects to the Unix socket and fetches the RAM usage.


package main

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
)

func main() {
	// Dial the Unix socket
	conn, err := net.Dial("unix", "/tmp/demo.sock")
	if err != nil {
		panic(err)
	}

	// Create an HTTP client with the Unix socket connection
	client := http.Client{
		Transport: &http.Transport{
			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
				return conn, nil
			},
		},
	}

	// Send a GET request to the server
	resp, err := client.Get("http://unix/ram")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	// Read the response body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	// Print the response body
	fmt.Println(string(body))
}

Code explanation

  1. We are using the net.Dial function to dial the Unix socket.
  2. The first argument is the network type, which is unix in our case. The second argument is the path to the Unix socket file.
  3. We are creating an HTTP client with the Unix socket connection.
  4. We are sending a GET request to the server using the Unix socket.
  5. We are reading the response body and printing it.

Now let’s run the server and the client and see the output.

cd server
go run main.go
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ram                      --> main.main.func1 (3 handlers)
[GIN] 2024/03/17 - 20:49:55 | 200 |     346.791µs |                 | GET      "/ram"

You can actually disconnect the internet and run the client to see that it still works. For reference, I am running a ping command to google.com and then running the client to show that it still works.

cd client
ping -c 4 google.com
go run main.go
ping -c 4 google.com
go run main.go

ping: google.com: Temporary failure in name resolution
Current RAM usage: 2 MB

Can I use this as a normal TCP/IP server?

The simple answer is YES!

You can use NGINX to proxy pass your unix socket and use it as a simple HTTP server. Here is a NGINX config which can be imported into the nginx.conf file.

upstream demo {
    server unix:/tmp/demo.sock;
}
 
# the nginx server instance
server {
    listen 8081;
    server_name go.dev *.go.dev;
 
    location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_set_header X-NginX-Proxy true;
      proxy_http_version 1.1; # for keep-alive
      proxy_pass http://demo/;
      proxy_redirect off;
    }
}

NOTE: If you want to know more about NGINX and how to use it, you can refer to one of other blogs: Deploy on Linux with Systemctl and Nginx .

You might also need to change the permissions of the socket file to allow NGINX to access it. You can do that by running the following command.

sudo chmod a+rw /tmp/demo.sock

Finally, you can run the NGINX server and access the HTTP server using the NGINX server.

sudo nginx
curl localhost:8081/ram

Use Cases

  1. Microservices: You can use Unix sockets to communicate between microservices running on the same machine.
  2. GUI Applications: You can use Unix sockets to communicate between a GUI application and a backend server. Docker Desktop and Docker CLI uses Unix sockets to communicate with the Docker Daemon.
  3. Security: Unix sockets are more secure than TCP/IP sockets because they are not exposed to the network.