Using WebSockets on Heroku with Clojure and Immutant
Last updated December 16, 2019
Table of Contents
In this tutorial you’ll learn how to build a Clojure application with Immutant that uses a WebSocket. Then you’ll learn how to deploy that application to Heroku.
Sample code for the demo application is available on GitHub. Edits and enhancements are welcome. Just fork the repository, make your changes and send us a pull request.
Prerequisites
- Java, Leiningen, and the Heroku CLI (as described in the Heroku CLI setup article)
- A Heroku user account. Signup is free and instant.
Create the WebSocket app
Begin by generating the project scaffolding with Leiningen like so:
$ lein new demo
Generating a project called demo based on the 'default' template.
The default template is intended for library projects, not applications.
To see other templates (app, plugin, etc), try `lein help new`.
Then add the Immutant dependencies to the project.clj
file, and prepare it for Heroku by adding these settings:
(defproject demo "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.6.0"]
[org.immutant/web "2.0.0-beta2"]
[compojure "1.1.8"]
[ring/ring-core "1.3.0"]
[environ "1.0.0"]]
:main demo.core
:uberjar-name "demo-standalone.jar"
:profiles {:uberjar {:aot [demo.core]}}
:min-lein-version "2.4.0")
Now you’re ready to add some behavior to the application.
Functionality
The sample application renders a simple web page that opens a WebSocket to the backend. The client sends a payload containing a string over the WebSocket. The server reverses the string and sends the result back to the client.
There are two important pieces to the interaction: the server-side callback functions and a JavaScript file that opens the WebSocket.
Server
In the project’s src/demo/core.clj
file, add the following dependencies to the top of the file (replacing the existing namespace statement):
(ns demo.core
(:require
[immutant.web :as web]
[immutant.web.async :as async]
[immutant.web.middleware :as web-middleware]
[compojure.route :as route]
[environ.core :refer (env)]
[compojure.core :refer (ANY GET defroutes)]
[ring.util.response :refer (response redirect content-type)])
(:gen-class))
Below that, define the WebSocket callback functions:
(def websocket-callbacks
"WebSocket callback functions"
{:on-open (fn [channel]
(async/send! channel "Ready to reverse your messages!"))
:on-close (fn [channel {:keys [code reason]}]
(println "close code:" code "reason:" reason))
:on-message (fn [ch m]
(async/send! ch (apply str (reverse m))))})
The websocket-callbacks
function defines a map of three functions. These functions will be executed for opening a socket, closing a socket and receiving a message respectively.
Next, define the web routes for the application:
(defroutes routes
(GET "/" {c :context} (redirect (str c "/index.html")))
(route/resources "/"))
Finally, create a main function as the entry point to the application:
(defn -main [& {:as args}]
(web/run
(-> routes
(web-middleware/wrap-session {:timeout 20})
;; wrap the handler with websocket support
;; websocket requests will go to the callbacks, ring requests to the handler
(web-middleware/wrap-websocket websocket-callbacks))
(merge {"host" (env :demo-web-host), "port" (env :demo-web-port)}
args)))
The server side is ready to handle messages from the client.
Client
The client side will use a simple HTML page to control the demo. Create a resources/public/index.html
file and put the following code in it:
<html>
<head>
<meta charset="utf-8">
<title>WebSocket Demo</title>
</head>
<body>
<h1>WebSocket Demo</h1>
<div>
<input type="text" id="input" value="Enter text to reverse!" />
</div>
<div>
<button type="button" id="open">Open</button>
<button type="button" id="send">Send</button>
<button type="button" id="close">Close</button>
</div>
<div id="messages"></div>
<script src="js/app.js"></script>
</body>
</html>
This page has a text box, an open button, a send button, a close button and an area for some messages. But they don’t do anything yet. To add behavior to them, you’ll need some Javascript. Create a resources/public/js/app.js
file, and put the following code in it:
window.onload = function() {
var input = document.getElementById('input');
var openBtn = document.getElementById('open');
var sendBtn = document.getElementById('send');
var closeBtn = document.getElementById('close');
var messages = document.getElementById('messages');
var socket;
};
This defines the window.onload
function and a few variables that represent the textbox contents, the buttons, the message area, and the socket you’ll use to communicate.
Now add a function to the window.onload
body (that is, a function inside of the function) to update the messages
variable:
function output(style, text){
messages.innerHTML += "<br/><span class='" + style + "'>" + text + "</span>";
}
This will be used as a callback when communicating with the socket. Below this function (but still inside of the window.onload
function) add the follow code to define the behavior of the open button:
openBtn.onclick = function(e) {
e.preventDefault();
if (socket !== undefined) {
output("error", "Already connected");
return;
}
var uri = "ws://" + location.host + location.pathname;
uri = uri.substring(0, uri.lastIndexOf('/'));
socket = new WebSocket(uri);
socket.onerror = function(error) {
output("error", error);
};
socket.onopen = function(event) {
output("opened", "Connected to " + event.currentTarget.url);
};
socket.onmessage = function(event) {
var message = event.data;
output("received", "<<< " + message);
};
socket.onclose = function(event) {
output("closed", "Disconnected: " + event.code + " " + event.reason);
socket = undefined;
};
};
When the open button is clicked, this function will execute. It creates a new websocket, and then defines the callback functions for each of the socket operations. In all cases, it posts a message to the messages
text area.
Next, add the following code (still inside of the window.onload
function) to define the behavior of the send button:
sendBtn.onclick = function(e) {
if (socket == undefined) {
output("error", 'Not connected');
return;
}
var text = document.getElementById("input").value;
socket.send(text);
output("sent", ">>> " + text);
};
In a similar fashion as opening, this function invokes the send
method on the socket and invokes the output
function with the value that was sent.
Finally, add the following code before the end of the window.onload
function to define the behavior of the close button:
closeBtn.onclick = function(e) {
if (socket == undefined) {
output('error', 'Not connected');
return;
}
socket.close(1000, "Close button clicked");
};
This will close the socket when the close button is clicked.
Now the application is ready to run.
Running the app locally
To run the app locally, you’ll first need to compile it by running this command:
$ lein uberjar
This will produce an executable JAR file that can be launched with this command:
$ java -jar target/demo-standalone.jar host 0.0.0.0 port 5000
Run the command shown above, and then open a browser to http://localhost:5000
. You will see the WebSockets page with the open, send and close buttons. Enter some text and test it out. Now you’re ready to deploy to it to the cloud.
Deploying the app to Heroku
Create a Procfile
in the project root with the following contents:
web: java $JVM_OPTS -jar target/demo-standalone.jar host 0.0.0.0 port $PORT
Heroku needs this file to know what command to run to launch your application. As you’ll notice, it’s very similar to the command you ran locally.
Now, add all of your code to a Git repository:
$ git init
$ git add .
$ git commit -m "first commit"
Create the Heroku app to which you will deploy:
$ heroku create
Then deploy your code:
$ git push heroku master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (20/20), 7.03 KiB | 0 bytes/s, done.
Total 20 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Clojure (Leiningen 2) app detected
remote: -----> Installing OpenJDK 1.8...done
remote: -----> Installing Leiningen
remote: Downloading: leiningen-2.5.0-standalone.jar
remote: Writing: lein script
remote: -----> Building with Leiningen
remote: Running: lein uberjar
remote: Retrieving org/clojure/clojure/1.6.0/clojure-1.6.0.pom from central
...
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing... done, 58.2MB
remote: -----> Launching... done, v3
remote: https://still-hamlet-4310.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/immutant-feature-demo.git
* [new branch] master -> master
Congratulations! Your web app should now be up and running on Heroku. Visit the application to see it in action:
$ heroku open
Be aware that the heroku open
command will open an HTTPS URL. If you are using Firefox, you will get a security error in your browser when you attempt to open the Websocket. You must either change the URL to an HTTP URL or use another browser.
For more information on using WebSockets with Immutant, see the Immutant API documentation. But be aware that not all Immutant features can be used on Heroku. Any feature that requires cluster, such as singleton services and session replication, will not behave correctly. Instead, it is recommend that you use other solutions such as the Heroku Scheduler add-on and the Memcached add-on for caching .