At Binary Noggin, we frequently use Phoenix Channels to build applications. Although Phoenix Channels are cool, they aren’t always needed when integrating with clients. Sometimes a raw websocket connection is all you need to get real-time communication up and running. This is useful for integrating with systems, frameworks and/or languages that:

  • Don’t have a well-supported Phoenix Channels Client (this ironically includes Elixir at the time of writing)
  • Don’t want the added (small) overhead of the Channels abstraction
  • Simply don’t want to use Channels

Whatever the use case, I was surprised to find that adding a handler for raw websockets in Phoenix wasn’t well documented, which seemed odd because Phoenix uses Cowboy under the hood. In this quick post, I’ll give an example of how to add a raw websocket handler to a Phoenix application.

Setup

I’m going to generate a new project to help keep track of names. All the steps below this section also apply to existing applications.

First, generate a new project. I’m explicitly disabling the things that aren’t needed here, but obviously tune this accordingly.

mix phx.new socks --no-ecto --no-webpack

 

Configuring the Application

Whether or not you’ve just generated a new project or are integrating an existing one, we need to configure the Cowboy Endpoint Adapter‘s dispatch routes.

First, open your config.exs and create or modify the Endpoint entry:

config :socks, SocksWeb.Endpoint,
  http: [
    dispatch: [
      {:_,
       [
         {"/websocket", SocksWeb.EchoSocket, []},
         {:_, Phoenix.Endpoint.Cowboy2Handler, {SocksWeb.Endpoint, []}}
       ]}
    ]
  ]

Note the dispatch key is nested inside the http key. This means we’ll also need to enable it for https.

dispatch = [
  _: [
    {"/websocket", SocksWeb.EchoSocket, []},
    {:_, Phoenix.Endpoint.Cowboy2Handler, {SocksWeb.Endpoint, []}}
  ]
]

config :socks, SocksWeb.Endpoint,
  http:  [dispatch: dispatch],
  https: [dispatch: dispatch]

 

Next, we’ll look at the implementation of the SocksWeb.EchoSocket module. For now, all we care about is the dispatch values. The first key, :_ is a wildcard host. You can read the official Cowboy documentation for more information on this structure. For now, we only want a single host, so we put a wildcard here. The list inside that first :_ host are paths. This works similar to any other HTTP router, so we don’t need to dwell on this section.

The left-hand side is the path. For example, if we started our webserver locally, the newly added path would look like: ws://localhost:4000/websocket. The second element of the tuple is the handler implementation we’ll look at next, as well as cowboy options to be passed in.

Our First Handler: Echo

Now that the server is configured correctly, we can actually implement our shiny new handler.

Create a new file in the project in the socks_web folder. I like to maintain the naming convention of Phoenix Channels, but you can name this file anything you want. I’ll use lib/socks_web/echo_socket.ex:

 

 

Client-Side Implementation: Echo

In this section, we’ll write a very simple client in JavaScript because it’s already available. Open app.js, and add the following snippet to it somewhere:

// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:4000/websocket');

// Connection opened
socket.addEventListener('open', function (event) {
    socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
    console.log('Message from server ', event.data);
});

 

Bonus Client Implementation: Elixir Echo

If we need a client in Elixir, we can use Gun.

defmodule SockClient do
  @host 'localhost'
  @port 4000
  @path '/websocket'

  use GenServer
  require Logger

  def start_link(args) do
    GenServer.start_link(__MODULE__, args)
  end

  @impl GenServer
  def init(_args) do
    connect_opts = %{
      connect_timeout: :timer.minutes(1),
      retry: 10,
      retry_timeout: 100,
      protocols: [:http]
    }
    with {:ok, gun}      <- :gun.open(@host, @port, connect_opts),
         {:ok, protocol} <- :gun.await_up(gun),
         stream          <- :gun.ws_upgrade(gun, @path, []) 
    do
      :ok = :gun.ws_send(gun, stream, {:text, "Hello Server!"})
      state = %{gun: gun, protocol: protocol, stream: stream}
      {:ok, state}
    end
  end

  def handle_info({:gun_ws, gun, stream, {:text, data}}, %{gun: gun, stream: stream} = state) do
    Logger.info ["Message from server ", data]
    {:noreply, state}
  end
end

 

Bonus Round: Nginx Config

This section isn’t meant to be a full tutorial on deploying a Phoenix application behind a Nginx proxy, but there’s a snippet required for allowing websocket connections when using proxy_pass.

In the http section of nginx.conf, add:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

Then add the following to the server section:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

 

Conclusion

Setting up raw websocket handlers in a standard Phoenix application is a simple fix for establishing real-time communication. If you want to learn more about websockets, I recommend reading Cowboy Websocket Handlers, Gun Websocket Client and JavaScript Websocket API.

 

Connor Rigby is a software engineer with a decade of experience in software development. He specializes in Elixir and Ruby, and is a core contributor to the Nerves and NervesHub projects.

Founded in 2007, Binary Noggin is a team of software engineers who serve as a trusted extension of your team, helping your company succeed through collaboration. We forge customizable solutions using Agile methodologies and our mastery of Elixir, Ruby and other open-source technologies. Share your ideas with us on Facebook and Twitter.