Monday, February 07, 2011

Clojure: Web Socket Introduction

Web Sockets may sound intimidating, but if you use Clojure and jQuery it's actually quite simple.

The first thing you'll want to do is create a new project. I use lein for simplicity's sake.
jfields$ lein new ws-intro
Created new project in: /Users/jfields/src/ws-intro
Look over project.clj and start coding in ws_intro/core.clj
Next we'll need to get Clojure, Webbit (a WebSocket capable server), and clojure.data.json. My project.clj file looks like the following example.
(defproject ws-intro "1.0.0-SNAPSHOT"
  :description "FIXME: write description"
  :dependencies [[org.clojure/clojure "1.3.0"]
                 [org.webbitserver/webbit "0.4.3"]
                 [org.clojure/data.json "0.1.2"]])
Once the project.clj file has been updated, a quick command-line run of "lein deps" will get all the jars you need. A quick ls of the lib dir should look something like this:
-rw-r--r--  1 jfields  domain.users  3390414 Feb 20 15:25 clojure-1.3.0.jar
-rw-r--r--  1 jfields  domain.users     5177 Feb 20 15:25 data.json-0.1.2.jar
-rw-r--r--  1 jfields  domain.users   801614 Feb 20 15:25 netty-3.2.7.Final.jar
-rw-r--r--  1 jfields  domain.users   154735 Feb 20 15:25 webbit-0.4.3.jar
Next, I converted the Hello World example available in the Webbit README to Clojure (ws-intro/core.clj).
(ns ws-intro.core
  (:require [clojure.data.json :as json]
            [clojure.string :as s])
  (:import [org.webbitserver WebServer WebServers WebSocketHandler]
           [org.webbitserver.handler StaticFileHandler]))

(defn -main []
  (doto (WebServers/createWebServer 8080)
    (.add "/websocket"
          (proxy [WebSocketHandler] []
            (onOpen [c] (println "opened" c))
            (onClose [c] (println "closed" c))
            (onMessage [c j] (println c j))))

    (.add (StaticFileHandler. "."))
    (.start)))
You'll notice that we defined a "-main" function. This is a convention that we can take advantage of by adding a :main option to our project.clj.
(defproject ws-intro "1.0.0-SNAPSHOT"
  :main ws-intro.core
  :description "FIXME: write description"
  :dependencies [[org.clojure/clojure "1.3.0"]
                 [org.webbitserver/webbit "0.4.3"]
                 [org.clojure/data.json "0.1.2"]])
With the addition of the :main option, we can start listening to our web socket by running "lein run" from the command-line.

At this point we have a webserver running on port 8080 and a websocket available at localhost:8080/websocket; however, we have no way to access our new websocket.

Again, the examples already on the internet give us 90% of what we need. We're going to use jquery-websocket, and the example at the bottom of the page is just about exactly what we need. The following code is a slightly modified version that should suit our purposes.
<html>
<body>
<h1>WebSocket Demo</h1>
<input id="message" type="text"/>
<section id="content"></section>
<script src="http://www.google.com/jsapi"></script>
<script>google.load("jquery", "1.3")</script>
<script src="http://jquery-json.googlecode.com/files/jquery.json-2.2.min.js">
</script>
<script src="http://jquery-websocket.googlecode.com/files/jquery.websocket-0.0.1.js">
</script>
<script>
  var ws = $.websocket("ws://127.0.0.1:8080/websocket", {
    events: { 
      upcased: function(e) { $("#content").html(e.message); }}});

  $('#message').change(function(){
    ws.send('message', {type: "downcase", message: $("#message").val()});});
</script>
</body>
</html>
Our client isn't much, but it does connect to a web socket and it sends a message to the web socket when the text in the input is changed.

Assuming you still have the server running you should be able to load up your page once you save your index.html in the ws-intro project directory (the path specified to our StaticFileHandler).
Is your page not loading? =(
What URL did you use? I've been told http://localhost:8080/ doesn't work as well as http://127.0.0.1:8080/
What browser did you use? Everything works for me in Chrome (version 17.0.963.56)
If your page loads, your server must be up and running. You should see something similar to the following line in your server console.
opened #<NettyWebSocketConnection webbit.netty.NettyWebSocketConnection@8c5488>
You might as well type something into the input and tab out (or whatever you prefer to do that fires the "change" event). I typed in "hello" and got the following output in my server console.
#<NettyWebSocketConnection webbit.netty.NettyWebSocketConnection@8c5488> 
  {"type":"message","data":{"type":"downcase","message":"hello"}}
Okay, everything is working. Let's add a little behavior to our server. Upon receiving a message, our server is going to take the text, upcase it, and send it back to the client.

Here's an updated version of core.clj.
(ns ws-intro.core
  (:require [clojure.data.json :as json]
            [clojure.string :as s])
  (:import [org.webbitserver WebServer WebServers WebSocketHandler]
           [org.webbitserver.handler StaticFileHandler]))

(defn on-message [connection json-message]
  (let [message (-> json-message json/read-json (get-in [:data :message]))]
    (.send connection (json/json-str
                       {:type "upcased" :message (s/upper-case message) }))))

(defn -main []
  (doto (WebServers/createWebServer 8080)
    (.add "/websocket"
          (proxy [WebSocketHandler] []
            (onOpen [c] (println "opened" c))
            (onClose [c] (println "closed" c))
            (onMessage [c j] (on-message c j))))

    (.add (StaticFileHandler. "."))
    (.start)))
The new version of core.clj takes advantage of clojure.data.json support. The net result of this is that we can work with Clojure maps and basically ignore json throughout our application.

After making the above changes to core.clj we can restart our server (ctrl+c & 'lein run' again), refresh our webpage, enter some text, tab out of the input, and then we should see our text on the webpage as upper-case.

And, we're done. We have working client-server interaction. We're ready to put this into production. It's that easy.

You might have noticed a few things that make the magic happen. On the server we sent a map that has :type "upcased". This type corresponds to the events that are defined in our client. The jquery-websocket takes care of routing our new message to the function associated with upcased. Extending on this idea, you can send messages from the server with different types and handle each one on the ui as a different event.

That's it. The app should be working, and you should have everything you need to begin expanding the capabilities of the application. If you run into any trouble, the documentation for webbit and jquery-websocket should get you through.
Post a Comment