Apr
08
2010

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<webSocket> sockets = new ArrayList<webSocket>();
 
    @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("<br />");
                    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("&lt;");
                    break;
                case '>':
                    buffer.append("&gt;");
                    break;
                case '&':
                    buffer.append("&amp;");
                    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:

?View Code JAVASCRIPT
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.

Share and Enjoy:
  • Print
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Blogplay

Technorati Tags: , , ,

14 Comments

  • mcahornsirup says:

    Nice… after painfull months with Icefaces, I want something stable. WebSockets could bridge the gap and make it easy to use common ui libs in a bidirectional way!

  • [...] Servlet 3.0 Async API.  The exact same pattern is now happening, e.g. Jetty, Grizzly/GlassFish and Resin now support WebSocket, and again there is no portability across WebServer. Hence if you [...]

  • Ido Ran says:

    Hi,
    I’m developing an a RESTful service and have several clients, windows, web, no-interface – all working with the RESTful service.

    I’m looking for way to keep the clients up-to-date with the server. It’s does has to be “transaction up-to-date” or even “in-order” up-to-date just to have the server send some kind of notification to the client that a resource has either add/remove/update. I want to keep the RESTful architecture as much as possible.

    WebSocket looks like good solution (all of the solution just poll which is not good).

    Can you please explain which version of Glassfish I need to use in order to have WebSocket on the server side?

    Thank you,
    Ido.

  • Quintesse says:

    @jlee

    Shouldn’t the release 3.0.1-b20 that’s included with Netbeans 6.9rc2 support websockets as well? It uses grizzly 1.9.18-10 which according to the website should include websockets, or a I mistaken?

    Anyway, I haven’t been able to get your example running on either version (3.0.1 or nightly build). The application never get’s activated as far as I can see. Any ideas?

    • jlee says:

      Websockets support is only available in 3.1 nightly builds. I had hoped to get support into the 3.0 stream but couldn’t for number of reasons. If anything still says 3.0 supports websockets, then that’s a bug somewhere.

  • Quintesse says:

    @jlee
    Ok, I’ve got the latest installed as well and I tried to recreate the situation you describe in this article as much as possible but still no success, the websocket on the client side never connects (onclose is immediately called) and nothing appears in the server logs. Any ideas? Or could you tell me where I could get some more help to figure out what I’m doing wrong? Cheers.

    • jlee says:

      Make sure the url paths match up correctly otherwise the protocol will fail to validate the request. It’s a slight shortcoming of that sample that paths are hardcoded. I should use js to fetch path to build the request with. I just haven’t had time to address that one yet.

  • Quintesse says:

    I’ve configured the servlet as “/chat” and it configures the websocket application as “/chat” as well (as per your code above) and hardcoded the js url as ws://localhost:8080/ws-test-server2/chat (ws-test-server2 being the name of the project). The servlet initializes and registers the application, so I know the WebSocket gets created and tries to connect, but from there on I don’t get anything else.

    • jlee says:

      OK. I’ll try to take a look at that this week. I’m finally getting dug out from all the other tasks for the time being so I’ll have some spare cycles I can use.

  • Quintesse says:

    Great. Any mailing list where I can follow the progress? I really want to do something with WebSockets and so far I’m using Jetty (which seems a bit simpler to use, implementing WebSockets as a subclass of HttpServlet) but I’m missing the integration with Netbeans and Glassfish :)

  • Quintesse says:

    PS: Building the complete Grizzly package (latest) and copying the resulting WAR from the samples/chat/target directory to Glassfish (latest) results in very strange behaviour from Chromium: http://imagebin.ca/view/Zw1Y4Y.html

    This does not happen with any of the other WebSockets demos I’ve been trying on the net, nor does it happen with the local versions I have using jWebsockets or Jetty.

    • jlee says:

      Wow. I have not seen that one. It worked fine on Chrome on OS X last I tried it. I’ll see if I get the same response here. I wonder if it’s related to the WebSockets protocol rev. The new version is incompatible with the version I based the grizzly code on. Seems unlikely at this stage but you never know…

RSS feed for comments on this post.


Theme based on: Aeros 2.0 by TheBuckmaker.com