Websockets with Clojure and http-kit

Table of Contents

Websocket is a relatively new network protocol that enables a connection between client and server to have long-living connections. What this means is that servers can push things to clients and vice-versa through the same connection.

In this post, I'll provide a brief walkthrough to setting up a small dashboard web app using Clojure and http-kit. I am assuming that you are familiar with Clojure and already have Leiningen installed. You can find the final codebase in this Github repo.

1 A (fake) realtime happiness gauge

Lets say that one of your main goals in life is to maximize happiness in this world. Well, you'd want a way to measure what the happiness level in the world is right now so that you can go save the day by making some pissed people happy. Which is why we'll build a happiness meter of sorts.

But this post isn't really about how to go about measuring happiness so we'll just use Clojure's handy rand function to create some random happiness data.

2 Project setup

We'll be sending our data to the browser using JSON which will be parsed using Javascript and drawn into a graph. The first thing you need to do is create a new project:          lein new happy-dashboard

And add some dependencies to your project:          (defproject happy-dashboard "0.1.0-SNAPSHOT"    :description "A dashboard that shows how happy the world is."  :url "http://example.com/FIXME"  :license {:name "Eclipse Public License"            :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.5.1"] [ring/ring-json "0.2.0"] [http-kit "2.0.0"] [ring/ring-devel "1.1.8"] [compojure "1.1.5"] [ring-cors "0.1.0"]] :main happy-dashboard.core)

You are probably already familiar with compojure and cheshire. http-kit might be new to you. http-kit is an alternative to the Ring Jetty adapter(this is what you probably use if you create your web apps using lein new compojure myapp). The main reason I'm using http-kit here is because it provides an easy interface to Websocket.

ring-devel is required for hot code reloading, so that you won't have to restart the server each time you make a change. ring-corsis required to enable CORS, so that the whole world has open access to our happiness data.

3 Websocket server

Because we decided to delegate our happiness-measuring to Clojure's rand function, our program actually turns out to be quite small so we'll just use one namespace; here is our program in its entirety:          (ns happy-dashboard.core    (:use [compojure.core :only (defroutes GET)] ring.util.response ring.middleware.cors org.httpkit.server)  (:require [compojure.route :as route] [compojure.handler :as handler] [ring.middleware.reload :as reload] [cheshire.core :refer :all])) (def clients (atom {})) (defn ws  [req] (with-channel req con (swap! clients assoc con true)    (println con " connected")    (on-close con (fn [status] (swap! clients dissoc con)                    (println con " disconnected. status: " status))))) (future (loop [] (doseq [client @clients] (send! (key client) (generate-string {:happiness (rand 10)})                   false))          (Thread/sleep 5000)          (recur))) (defroutes routes  (GET "/happiness" [] ws)) (def application (-> (handler/site routes)                     reload/wrap-reload (wrap-cors :access-control-allow-origin #".+"))) (defn -main [& args] (let [port (Integer/parseInt (or (System/getenv "PORT") "8080"))] (run-server application {:port port :join? false})))

At this point if you start the server using lein run and point your browser to http://localhost:8080/happiness, you'll see the pushing going on. But note that this isn't Websocket. What happened was because you opened that page in your browser, with a http://http-kit magically used HTTP long-polling instead. Its a similar technology to Websocket that was common before Websocket came along. To use Websocket you have to use the ws:// URI scheme, which usually won't work in your browser's address bar. We'll get to that in just a minute.

The most interesting function is the ws function. When it gets a request it assoc's it into the clients atom and tells us that someone connected. You'll notice it also has an (on-close …) form in which we tell it to dissoc the function when our user closes his/her browser tab.

Besides that the future form simply sends a small piece of JSON every 5 seconds to all connected clients. I think the call to (send! …) is pretty obvious except for the false part, which tells the server to keep the connection open after sending our message. By default, send! closes the connection after it has sent a message.

Note that we are able to send! messages any time, as long as the connection hasn't closed.

4 Front end

Now that we are successfully pushing all of that happiness data around, we can finally represent it in a neat little chart. In the last section, we found that its not possible to open a Websocket connection like we usually open up HTTP connections. The way we usually open Websocket connection from inside a browser is using the Websocket Javascript API, like this:          var socket = new WebSocket("ws://localhost:8080/happiness");

And that will open a Websocket connection instead of an HTTP one. Then, you can tell Javascript what to do with the messages it receives:          socket.onmessage = function(event) {              console.log(JSON.parse(event.data)["happiness"])}

Now to draw those happiness charts: we could couple it with our Clojure app and that would work but since we have CORS enabled we'll instead put it anywhere we want.

Here is our frontend code:          <!DOCTYPE html>  <html> <head> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script type="text/javascript" src="chartkick.js"></script> <script type="text/javascript"> var data = [], timestamps = []; var socket = new WebSocket("ws://localhost:8080/happiness"); socket.onmessage = function(event) { data.push(JSON.parse(event.data)["happiness"]); timestamps.push(new Date); refreshChart();} function refreshChart(){ new Chartkick.LineChart("chart", zip([timestamps,data])); } function zip(arrays) { return arrays[0].map(function(_,i){ return arrays.map(function(array){return array[i];}); }); } </script> </head> <body> <div id="chart" style="height: 300px;"></div> </body> </html>

I'm using chartkick.js with Google Charts to produce a chart but you could use your favorite Javascript chart library instead.

And that's it. If you open the HTML file, it should start getting messages from the Websocket server and you should see a graph that's being updated: