GlassFish Web Sockets Sample

A few weeks back I blogged about the impending GlassFish support for the emerging Web Sockets standard. To update, I have some good news and some some ok news. The good news is that the implementation has stabilized enough to show some concrete code. The ok news is that, despite my best efforts and overly persistent petitioning, this support won't show up in 3.0.1. You'll need to use the GlassFish nightly builds to play with it. Hopefully that's OK given that web sockets are still evolving anyway and browser support is spotty at best. With that out of the way, let's look at a simple example. For familiarity's sake, and for simple comparison, I ported the comet chat sample in grizzly to use web sockets. I wanted to see how different the code would look between two similar applications. So if you're familiar with the comet example, the web socket example should be very familiar. At the heart of grizzly's/glassfish's web socket support is the WebSocketApplication. For our chat, it's pretty simple: every time someone sends a message, broadcast it to everyone. The code is pretty simple:

package com.sun.grizzly.samples.websockets;

import com.sun.grizzly.tcp.Request;
import com.sun.grizzly.tcp.Response;
import com.sun.grizzly.websockets.DataFrame;
import com.sun.grizzly.websockets.WebSocket;
import com.sun.grizzly.websockets.WebSocketApplication;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

public class ChatApplication extends WebSocketApplication {
    List sockets = new ArrayList();

    @Override
    public WebSocket createSocket(Request request, Response response) throws IOException {
        final ChatWebSocket socket = new ChatWebSocket(this, request, response);

        sockets.add(socket);
        return socket;
    }

    public void onMessage(WebSocket socket, DataFrame frame) {
        final String data = frame.getTextPayload();
        if (data.startsWith("login:")) {
            login((ChatWebSocket) socket, frame);
        } else {
            broadcast(data);
        }
    }

    public void onConnect(WebSocket socket) {
    }

    private void broadcast(String text) {
        WebSocketsServlet.logger.info("Broadcasting : " + text);
        for (WebSocket webSocket : sockets) {
            send(webSocket, text);
        }

    }

    private void send(WebSocket socket, String text) {
        try {
            socket.send(text);
        } catch (IOException e) {
            WebSocketsServlet.logger.log(Level.SEVERE, "Removing chat client: " + e.getMessage(), e);
            onClose(socket);
        }
    }

    public void onClose(WebSocket socket) {
        sockets.remove(socket);
    }

    private void login(ChatWebSocket socket, DataFrame frame) {
        if (socket.getUser() == null) {
            WebSocketsServlet.logger.info("ChatApplication.login");
            socket.setUser(frame.getTextPayload().split(":")[1].trim());
            broadcast(socket.getUser() + " has joined the chat.");
        }
    }
}

As you can see, we implement some very basic logic here. This, of course, could be as complex as you need it it to be. In a real chat application, for example, we'd have rooms/channels and the like but we're keeping it simple here. The other class to note is ChatWebSocket. There's not much need to provide custom WebSocket implementations but in this case, I chose to store the chat user's user name on the WebSocket. I could've used a Map in ChatApplication and used the default implementation but this seemed a bit more ... OOPish. The implementation there is, again, quite simple:

package com.sun.grizzly.samples.websockets;

import com.sun.grizzly.tcp.Request;
import com.sun.grizzly.tcp.Response;
import com.sun.grizzly.websockets.BaseServerWebSocket;

import java.io.IOException;

public class ChatWebSocket extends BaseServerWebSocket {
    private String user;
    private final ChatApplication app;

    public ChatWebSocket(final ChatApplication listener, Request request, Response response) {
        super(listener, request, response);
        app = listener;
    }

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    @Override
    public void send(String data) {
        super.send( toJsonp(getUser(), data) );
    }

    @Override
    public void close() throws IOException {
        WebSocketsServlet.logger.info("closing : " + getUser());
        app.remove(this);
        super.close();
    }

    private String toJsonp(String name, String message) {
        return "window.parent.app.update({ name: \"" + escape(name) + "\", message: \"" + escape(message) + "\" });\n";
    }

    private String escape(String orig) {
        StringBuilder buffer = new StringBuilder(orig.length());

        for (int i = 0; i < orig.length(); i++) {
            char c = orig.charAt(i);
            switch (c) {
                case '\b':
                    buffer.append("\\b");
                    break;
                case '\f':
                    buffer.append("\\f");
                    break;
                case '\n':
                    buffer.append("");
                    break;
                case '\r':
                    // ignore
                    break;
                case '\t':
                    buffer.append("\\t");
                    break;
                case '\'':
                    buffer.append("\\'");
                    break;
                case '\"':
                    buffer.append("\\\"");
                    break;
                case '\\':
                    buffer.append("\\\\");
                    break;
                case '<':
                    buffer.append("<");
                    break;
                case '>':
                    buffer.append(">");
                    break;
                case '&':
                    buffer.append("&");
                    break;
                default:
                    buffer.append(c);
            }
        }

        return buffer.toString();
    }
}

Again, it's pretty simple. There's some extra code for escaping the text since in our javascript, we'll simply eval() the text and let the browser work its magic. With these two pieces in place, we're almost done on the server. In order to let the web sockets system know of our application, we need to register it. (This is an optional step that we'll cover later.) In our case, we have a servlet in our web app. The servlet actually doesn't do anything. It doesn't override doGet() or doPost(). It only really exists for this one method:

@Override
    public void init(ServletConfig config) throws ServletException {
        WebSocketEngine.getEngine().register(config.getServletContext().getContextPath() + "/chat", app);
    }

This line registers the request URI with the WebSocketEngine so that it knows that when it sees a web socket request come in that matches that URI, to hand things off to the appropriate application. For our chat sample, we need an application to handle the logic related to running a chat system. However, if you don't need such logic, you don't even have to register (or write) and application class at all.

When a web socket request comes in, grizzly will look for any registered application for that URI. If it can be found, handling is handed off and grizzly's request handling is done. However, if no application can be found, grizzly can still process the web socket request by deferring the request to whatever the ultimate endpoint is. Grizzly will still frame anything written back to the client from, say, the servlet such that your web socket client gets a proper response. This scenario doesn't support bidirectional conversations, but it does allow you to expose any URL to a web socket client with no additional work.

There is one final step before we can try out the sample. Web socket support is disabled by default in grizzly and in glassfish so we must enable it first. In glassfish, we can do this by issuing this command:

asadmin set configs.config.server-config.network-config.protocols.protocol.http-listener-1.http.websockets-support-enabled=true

You will need to adjust the name of the protocol element to match your system if you changed any of that, but if you use the standard configuration, that's all you need. If you're using grizzly outside of glassfish, you have neither asadmin nor a domain.xml. For that, you'll need to enable web socket support on your SelectorThread:

        SelectorThread st = new SelectorThread();

        /*  whatever other settings you might need */
        st.setAsyncHandler(new DefaultAsyncHandler());
        st.setEnableAsyncExecution(true);
        st.getAsyncHandler().addAsyncFilter(new WebSocketAsyncFilter());

That does it for the server side. On the client side we have the usual mix of HTML and javascript. I don't want to go in to depth on most of that but I do want to take a look at one particular piece. In application.js, we have this code:

var app = {
    url: 'ws://localhost:8080/grizzly-websockets-chat/chat',
    initialize: function() {
        if ("WebSocket" in window) {
            $('login-name').focus();
            app.listen();
        } else {
            $('missing-sockets').style.display = 'inherit';
            $('login-name').style.display = 'none';
            $('login-button').style.display = 'none';
            $('display').style.display = 'none';
        }
    },
    listen: function() {
        $('websockets-frame').src = app.url + '?' + count;
        count ++;
    },
    login: function() {
        name = $F('login-name');
        if (! name.length > 0) {
            $('system-message').style.color = 'red';
            $('login-name').focus();
            return;
        }
        $('system-message').style.color = '#2d2b3d';
        $('system-message').innerHTML = name + ':';

        $('login-button').disabled = true;
        $('login-form').style.display = 'none';
        $('message-form').style.display = '';

        websocket = new WebSocket(app.url);
        websocket.onopen = function() {
            // Web Socket is connected. You can send data by send() method
            websocket.send('login:' + name);
        };
        websocket.onmessage = function (evt) {
            eval(evt.data);
            $('message').disabled = false;
            $('post-button').disabled = false;
            $('message').focus();
            $('message').value = '';
        };
        websocket.onclose = function() {
            var p = document.createElement('p');
            p.innerHTML = name + ': has left the chat';

            $('display').appendChild(p);

            new Fx.Scroll('display').down();
        };
    },
    post: function() {
        var message = $F('message');
        if (!message > 0) {
            return;
        }
        $('message').disabled = true;
        $('post-button').disabled = true;

        websocket.send(message);
    },
    update: function(data) {
        if (data) {
            var p = document.createElement('p');
            p.innerHTML = data.name + ': ' + data.message;

            $('display').appendChild(p);

            new Fx.Scroll('display').down();
        }
    }
};

Most of that is prototype/scriptaculous code for managing the UI but notice in login() how we connect to the server and define our callbacks for when we receive various events. In post() we send our entry to the server and in update() we take what the server gives us and update the UI accordingly. It's a very simple and naive system to be sure but serves to illustrate the basics without getting mired down in details of building a large scale, fault tolerant system. Yes, I know there are things that could be improved with this or that detail. But I'm not building a replace for Yahoo! chat. :) If you want to see the code in full, you can browse the grizzly subversion repository here.

Now, there *is* some bad news but it's temporary at least. This code is in the trunk of grizzly's subversion repository and will be included in the 1.9.19-beta2 release. This hasn't been integrated into glassfish 3.1 yet but will be soon. Until then, you'll need to build your own grizzly tree and glassfish distribution with the root pom updated to use 1.9.19-SNAPSHOT. This is, indeed, quite ugly but we should be integrating a new 1.9.19 beta soon and then you can simply use the nightly builds.

If you have any questions, please leave a comment or, better yet, join the dev and/or users mailing lists here. There's still some tweaking left to do here and there but things are shaping up quite nicely, I think. But I won't know what's missing or wrong unless you tell me. So, please, play. Comment. Let me know what you think.