How unicorn talks to nginx - an introduction to unix sockets in Ruby

 

Ruby application servers are typically used together with a web server like nginx. When user requests a page from your Rails app, nginx delegates the request to the application server. But how exactly does that work? How does nginx talk with unicorn?

One of the most efficient options is to use unix sockets. Let's see how they work! In this post we'll start with the basics of sockets, and end by creating our own simple application server that is proxied by nginx.

Sockets allow programs to talk with each other as if they were writing to or reading from a file. Sockets allow programs to talk with each other as if they were writing to or reading from a file. In this example, Unicorn creates the socket and monitors it for connections. Nginx can then connect to the socket and talk with Unicorn.

What's a unix socket?

Unix sockets let one program talk to another in a way that kind of resembles working with files. They're a type of IPC, or inter-process communication.

To be accessible via a socket, your program first creates a socket and "saves" it to disk, just like a file. It monitors the socket for incoming connections. When it receives one, it uses standard IO methods to read and write data.

Ruby provides everything you need to work with unix sockets via a couple of classes:

  • UNIX*Server* - It creates the socket, saves it to disk,  and lets you monitor it for new connections.

  • UNIX*Socket* - Open existing sockets for IO.

NOTE: Other kinds of sockets exist. Most notably TCP sockets. But this post only deals with unix sockets. How do you tell the difference? Unix sockets have file names.

The Simplest Socket

We're going to look at two little programs.

The first is is the "server." It simply creates an instance of the UnixServer class, then uses server.accept to wait for a connection. When it receives a connection, it exchanges a greeting.

It's worth noting that both the accept and readline methods block program execution until they receive what they're waiting for.

require "socket"

server = UNIXServer.new('/tmp/simple.sock')

puts "==== Waiting for connection"
socket = server.accept

puts "==== Got Request:"
puts socket.readline

puts "==== Sending Response"
socket.write("I read you loud and clear, good buddy!")

socket.close

So we have a server. Now all we need is a client.

In the example below, we open the socket created by our server. Then we use normal IO methods to send and receive a greeting.

require "socket"

socket = UNIXSocket.new('/tmp/simple.sock')

puts "==== Sending"
socket.write("Hello server, can you hear me?\n")

puts "==== Getting Response"
puts socket.readline 

socket.close

To demonstrate, we first need to run the server. Then we run the client. You can see the results below:

Example of a simple UNIX socket client/server interaction Example of a simple UNIX socket client/server interaction. The client is on the left. The server is on the right.

Interfacing with nginx

Now that we know how to create a unix socket "server" we can easily interface with nginx.

Don't believe me? Let's do a quick proof-of-concept. I'm going to adapt the code above to make it print out everything it receives from the socket.

require "socket"

# Create the socket and "save it" to the file system
server = UNIXServer.new('/tmp/socktest.sock')

# Wait until for a connection (by nginx)
socket = server.accept

# Read everything from the socket
while line = socket.readline
  puts line.inspect
end

socket.close

Now if I configure nginx to forward requests to the socket at /tmp/socktest.sock   I can see what data nginx is sending. (Don't worry, we'll discuss configuration in a minute)

When I make a web request, nginx sends the following data to my little server:

http request

Pretty cool! It's just a normal HTTP request with a few extra headers added.  Now we're ready to build a real app server. But first, let's discuss nginx configuration.

Installing and Configuring Nginx

If you don't already have nginx installed on your development machine, take a second and do that now. It's really easy on OSX via homebrew:

brew install nginx

Now we need to configure nginx to forward requests on localhost:2048 to a upstream server via a socket named /tmp/socktest.sock. That name isn't anything special. It just needs to match the socket name used by our web server.

You can save this configuration to /tmp/nginx.conf and then run nginx with the command nginx -c /tmp/nginx.conf to load it.

# Run nginx as a normal console program, not as a daemon
daemon off;

# Log errors to stdout
error_log /dev/stdout info;

events {} # Boilerplate

http {

  # Print the access log to stdout
  access_log /dev/stdout;

  # Tell nginx that there's an external server called @app living at our socket
  upstream app {
    server unix:/tmp/socktest.sock fail_timeout=0;
  }

  server {

    # Accept connections on localhost:2048
    listen 2048;
    server_name localhost;

    # Application root
    root /tmp;

    # If a path doesn't exist on disk, forward the request to @app
    try_files $uri/index.html $uri @app;

    # Set some configuration options on requests forwarded to @app
    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://app;
    }

  }
}

This configuration causes nginx to run like a normal terminal app, not like a daemon. It also writes all logs to stdout. When you run nginx it should look something like so:

Nginx running in non-daemon mode. Nginx running in non-daemon mode.

A DIY Application Server

Now that we've seen how to connect nginx to our program, it's a pretty simple matter to build a simple application server. When nginx forwards a request to our socket it's a standard HTTP request. After a little tinkering I was able to determine that if the socket returns a valid HTTP response, then it'll be displayed in the browser.

The application below takes any request and displays a timestamp.

require "socket"

# Connection creates the socket and accepts new connections
class Connection

  attr_accessor :path

  def initialize(path:)
    @path = path
    File.unlink(path) if File.exists?(path)
  end

  def server
    @server ||= UNIXServer.new(@path)
  end

  def on_request
    socket = server.accept
    yield(socket)
    socket.close
  end
end


# AppServer logs incoming requests and renders a view in response
class AppServer

  attr_reader :connection
  attr_reader :view

  def initialize(connection:, view:)
    @connection = connection
    @view = view
  end

  def run
    while true
      connection.on_request do |socket|
        while (line = socket.readline) != "\r\n"
          puts line 
        end
        socket.write(view.render)
      end
    end
  end

end

# TimeView simply provides the HTTP response
class TimeView
  def render
%[HTTP/1.1 200 OK

The current timestamp is: #{ Time.now.to_i }

]
  end
end


AppServer.new(connection: Connection.new(path: '/tmp/socktest.sock'), view: TimeView.new).run

Now if I fire up nginx as well as my script, I can go to localhost:2048. Requests are sent to my app. And responses are rendered by the browser. Pretty Cool!

HTTP requests are logged to STDOUT by our simple app server HTTP requests are logged to STDOUT by our simple app server

And here is the glorious fruit of our labors. Behold! A Timestamp!

The server returns a timestamp which is displayed in the browser The server returns a timestamp which is displayed in the browser

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required