A simple real-time chat server using Clojure and Aleph
I’ve been exploring some asynchronous servers and have recently been enamored by Clojure, a modern lisp with brilliant built in concurrency primitives. In the Clojure world, your only real choice for an async server is Aleph. It’s built on top of an event-driven abstraction library called Lamina (from the same authors) which makes for a handy non-blocking server ready for realtime web apps powered by websockets, UDP, or TCP.
For this example I was mainly interested in a simple websockets web app with url routes using Compojure, an un-opinionated web framework in Clojure. The premise is simple; a public chatroom around a url (i.e /chat/hello has it’s own room and messages added there don’t show up on /chat/another and visa versa). I borrowed some of the code and patterns from another aleph example app you also might want to check out.
You can get the full working(ish) source here: https://github.com/alexkehayias/clojure-aleph-chat
(ns core.main
(:use lamina.core
aleph.http
compojure.core
(hiccup core page))
; This sets the correct file type for js includes used by hiccup
(ring.middleware resource file-info)
core.views
core.templates)
(:require [compojure.route :as route])
(:gen-class))
(defn page []
"HTML page rendered using Hiccup. Includes the js we need for websockets."
(html5
[:head
(include-css "/static/stylesheets/master.css")]
[:body
[:div.container
[:div.row
[:div.columns.twelve
[:p [:h1#headline "Chat"]]
[:form
[:input#message {:type "text"}]
[:input.nice.large.blue.button {:type "submit"}]]
[:div#messages]]]]
(include-js "http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js")
(include-js "/static/javascripts/web_socket.js")
(include-js "/static/javascripts/app.js")]))
(defn sync-app [request]
"Rendered response of the chat page"
{:status 200
:headers {"content-type" "text/html"}
:body (page)})
(def wrapped-sync-app
"Wraps the response with static files"
(-> sync-app
(wrap-resource "public")
(wrap-file-info)))
(defn chat-init [ch]
"Initialize a new chat channel"
(receive-all ch #(println "message: " %)))
(defn chat-handler [ch room]
"Relays messages into a chat room. If it doesn't
exist create a new channel"
(let [chat (named-channel room chat-init)]
(siphon chat ch)
(siphon ch chat)))
(defn chat [ch request]
"View handler that handles a chat room. If it's not
a websocket request then return a rendered html response."
(let [params (:route-params request)
room (:room params)]
(if (:websocket request)
(chat-handler ch room)
(enqueue ch (wrapped-sync-app request)))))
(defroutes app-routes
"Routes requests to their handler function. Captures dynamic variables."
(GET ["/chat/:room", :room #"[a-zA-Z]+"] {}
(wrap-aleph-handler chat))
(GET ["/"] {} "Hello world!")
;;Route our public resources like css and js to the static url
(route/resources "/static")
;;Any url without a route handler will be served this response
(route/not-found "Page not found"))
(defn -main [& args]
"Main thread for the server which starts an async server with
all the routes we specified and is websocket ready."
(start-http-server (wrap-ring-handler app-routes)
So what’s happening here?
We’re taking a request to the URI /chat/<room> and creating a channel which other visitors to /chat/<room> subscribe to via a websocket. When a message is received to that URI it goes to the chat-handler, realizes it’s a websocket request, and adds the message to the named-channel that has that chatroom name. The message is pushed to all the subscribers of that channel (everyone on the page /chat/<room>) and the message is rendered on the page using javascript. There is only one channel that is persisted here that everyone is sharing.
All we need is some javascript to set up the websocket and handle messages. This is in the directory resources/public/javascripts:
// Note: this was generated from coffeescript and ƒ stands for function (lol emacs)
(ƒ() {
$(ƒ() {
window.socket = new WebSocket(window.location.href.replace("http://", "ws://"));
socket.onopen = ƒ() {
return console.log("socket opened");
};
socket.onmessage = ƒ(msg) {
return $("#messages").append("<p>" + msg.data + "</p>");
};
return $("form").on("submit", ƒ(e) {
e.preventDefault();
socket.send($("#message").val());
return $("#message").val("");
});
});
}).call(this);
There you have it, a simple, no-frills public chat room based on the page you’re on. This was just a test, but you can see how easy it is to set up a non-blocking async server with a little help from Aleph. Combined with the concurrency of Clojure itself, you can have an async, concurrent, functional programming love fest. Boners.
1 Notes/ Hide
-
lessfunny likes this
-
philcollins likes this
-
alexkehayias posted this
