Wednesday, April 11, 2012

JBoss 7 and WebSockets

As far as I know there is now no WebSockets support for web applications in JBoss 7. JBoss 7 uses JBoss Web as its servlet container. JBoss Web is based on Tomcat. Tomcat developers added only recently the support for WebSockets. Mike Brock from RedHat started some days ago to work on the WebSockets support for JBoss 7. But his solution is far away from being production ready and it is targeted for JBoss 7.1.2+ only. So if you have only one single JEE application which needs WebSockets support and you want deploy your application on JBoss 7 and you wish to have a single port (e.g. 8080) for your application,  then maybe I have a possible solution for you.

OK let me summurize the requirements for my solution:
  • our application must be deployed on JBoss 7.
  • all content (JSP, HTML, JavaScript, WebSockets, Images, etc.) of the application must be accessed through one single port (e.g. 8080).
  • it must be possible to receive and send data through WebSockets.
For my solution I'm going to use the wonderful, embeddable servlet container Jetty. For older JBoss versions (I mean really old like JBoss 4.2.3) there is a Jetty module, which can be used to replace JBoss Web. But it does not work with JBoss 7 and Jetty team has no plans to provide a new module for JBoss 7.  On the other hand Jetty has a lot of other cool things which allow us to use WebSockets in JBoss 7.

The main idea of the solution is to embed Jetty inside the WEB application and to ensure that Jetty delegates all plain HTTP requests to JBoss Web and handles all WebSockets requests itself. The first thing that we have to do is to determine at runtime on which port and host JBoss Web is running. This is needed to avoid hard-coding of host and port in our application. OK let's do it using JMX API:
String jbossWebHost;
Integer jbossWebPort;

MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
try {
    ObjectName http =
        new ObjectName("jboss.as:socket-binding-group=standard" +
            "-sockets,socket-binding=http");
    jbossWebHost =
        (String) mBeanServer.getAttribute(http, "boundAddress");
    jbossWebPort =
        (Integer) mBeanServer.getAttribute(http, "boundPort");
} catch (Exception e) {
    throw new Error(e);
}
The next step is to create a transparent proxy, which will delegate all plain HTTP requests to JBoss Web. For this purpose I will use ProxyServlet from Jetty:
/**
 * @author Andrej Golovnin
 */
final class TransparentProxy extends ProxyServlet {

    // Fields *************************************************************

    private String jbossWebHost;
    private Integer jbossWebPort;


    // Instance Creation **************************************************

    TransparentProxy(String jbossWebHost, Integer jbossWebPort) {
        this.jbossWebHost = jbossWebHost;
        this.jbossWebPort = jbossWebPort;
    }


    // Overriding default behavior ****************************************

    @Override
    protected HttpURI proxyHttpURI(String scheme, String serverName,
        int serverPort, String uri) throws MalformedURLException
    {
        try {
            URI dstURI = new URI(
                scheme + "://" + jbossWebHost + ":" + jbossWebPort + uri)
                .normalize();
            if (!validateDestination(dstURI.getHost(), dstURI.getPath())) {
                return null;
            }
            return new HttpURI(dstURI.toString());
        } catch (URISyntaxException e) {
            throw new MalformedURLException(e.getMessage());
        }
    }

} 
To handle WebSockets requests in Jetty you must create a subclass of WebSocketServlet and implement one of the subinterfaces of WebSocket. Instead of writing my own example of WebSockets usage in Jetty I have used a modified Jetty Test WEB application from the Jetty project. The test WEB application provides a small chat application, which uses WebSockets for the communication between clients and server. The test application from the Jetty project has a servlet named WebSocketChatServlet. I will use this servlet for the demonstration of my solution.

The next step is to register the WebSocketChatServlet and the TransparentProxy with a Jetty instance. For this purpose I will use ServletContextHandler. The TransparentProxy should be registered for the root context path:
ServletContextHandler proxy = new ServletContextHandler(
    ServletContextHandler.SESSIONS);
proxy.setContextPath("/");
proxy.addServlet(
    new ServletHolder(new TransparentProxy(jbossWebHost, jbossWebPort)),
    "/*");
The WebSocketChatServlet is registered to a defined context path, which is then used in the application to establish the WebSocket connection:
ServletContextHandler ws = new ServletContextHandler(
    ServletContextHandler.SESSIONS);
ws.setContextPath("/test/ws/chat");
ws.addServlet(new ServletHolder(new WebSocketChatServlet()), "/*");
ws.setAllowNullPathInfo(true);
It is important to note that you must allow null path info for your WebSocket servlet. If you don't do it, Jetty will redirect the request "/test/ws/chat" to "/test/ws/chat/" and the WebSocket connection won't be established. Registering of both ServletContextHandlers with Jetty server is very simple:
server = new Server();
.....
ContextHandlerCollection contexts = new ContextHandlerCollection();
contexts.setHandlers(new Handler[] {proxy, ws});

server.setHandler(contexts);
Starting and stopping of the Jetty server is done in an implementation of a ServletContextListener which is registered in the web.xml of the WEB application. You can find the full code of the solution on GitHub. I have also created a ready to deploy example. You can just put it into the deployments directory and go to http://localhost:8181/test to see it in action. Everything else on your JBoss installation should be also accessible on port 8181, e.g. the start page of JBoss 7. The port 8181 were chosen to simplify the deployment of the test application on a plain JBoss 7 installation.

The presented solution has some disadvantages:
  • it works only if you have a single application which needs WebSockets.
  • you can not use web.xml to deploy WebSocket servlets.
  • there is additional overhead to process plain HTTP request as Jetty must establish a connection to JBoss Web.
The advantage of this solution is that it can be used theoretically with any application server which does not provide WebSockets support for now.