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.

13 comments:

  1. Hi Andrej!
    Thanks a lot for this, it was very important for my current project.
    I can run it, but I seem to need to deploy it after JBoss has started, otherwise I get the message
    No MBean found with name jboss.as:socket-binding-group=standard-sockets,socket-binding=http

    Sorry for the noob question, but is there a way to tell Jetty to start after JBoss has started?
    Спасибо )

    ReplyDelete
  2. Hi xeper,
    I'm glad it could help you. Do you start JBoss in standalone or domain mode? In my example I use the name of the MBean of the standalone configuration. If you start JBoss in domain mode, the name of the MBean differs. I think it is:
    jboss.as:socket-binding-group=full-sockets,socket-binding=http. And which JBoss version you are using? I have just tested my example application with JBoss 7.1.1. And it works fine. I don't see the error message you mention.
    Не за что )

    ReplyDelete
  3. Hi Andrej,

    Thanks for your reply!
    I used 7.1.3 in standalone. I tried this on 7.1.1 and the result is the same as before.
    I don't think the problem lies in the name of the MBean, since if I deploy the application after JBoss has started, I don't get the error message and the application works well.

    ReplyDelete
  4. Not working. I keep geting an error:

    10:03:32,258 WARN [org.jboss.as.ee] (MSC service thread 1-4) JBAS011006: Not installing optional component org.eclipse.jetty.continuation.Servlet3Continuation$2 due to e
    xception: org.jboss.as.server.deployment.DeploymentUnitProcessingException: JBAS011054: Could not find default constructor for class org.eclipse.jetty.continuation.Servle
    t3Continuation$2
    at org.jboss.as.ee.component.ComponentDescription$DefaultComponentConfigurator.configure(ComponentDescription.java:606)
    at org.jboss.as.ee.component.deployers.EEModuleConfigurationProcessor.deploy(EEModuleConfigurationProcessor.java:81)
    at org.jboss.as.server.deployment.DeploymentUnitPhaseService.start(DeploymentUnitPhaseService.java:113) [jboss-as-server-7.1.1.Final.jar:7.1.1.Final]
    at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1811) [jboss-msc-1.0.2.GA.jar:1.0.2.GA]
    at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1746) [jboss-msc-1.0.2.GA.jar:1.0.2.GA]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110) [rt.jar:1.7.0_07]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) [rt.jar:1.7.0_07]
    at java.lang.Thread.run(Thread.java:722) [rt.jar:1.7.0_07]

    ReplyDelete
    Replies
    1. Same here, is there any fix for this yet?

      Delete
    2. It just a warning and can be safely ignored. I use my solution already in the production and it works perfectly. To avoid this message in the log file you may increase the log level for the category "org.jboss.as.ee" from WARN to ERROR.

      Delete
  5. Tested it - it works fine. I have a question... In order for this to work, i need to have two ports opened - in this case 8181, which handles all traffic (http + websocket), and 8080 which is the default web port on JBOSS and it handles all HTTP traffic but not web socket traffic. This solution is not good enough in my case because i can have only one port opened (8080). Sure, i could configure jboss web connector to listen on another port and start the Jetty instance on port 8080, but this is not acceptable because i do not have access to jboss configuration on the production environment.
    Do you think that it would be possible to use a servlet filter that would intercept all traffic on port 8080 and if it's web socket traffic then reroute it to Jetty (listening on port 8181)?

    ReplyDelete
    Replies
    1. Hi Tihomir, with my solution you must use two ports. And I doubt that a servlet filter would help you. In the upcoming JBoss version (now known as WildFly) there will be a new web server (http://undertow.io) which will provide the full support for WebSockets.

      Delete
    2. Thanks for the answer Andrej.
      Yes, i know that the next version of jboss will have web socket support (all j2ee servers will support it sooner or later, since the API is being standardized and it will be a part od J2EE 1.7 spec). The thing is, i am stuck with JBOSS 7.1.1 and i need a solution that works on that version. Why do you think the servlet filter approach would not work? It seems like a good idea to me, but i'm not sure if the filter can handle websocket traffic (intercept it and reroute it to Jetty).

      Delete
  6. What version of jetty are you using? Seems like in newer versions packages are restructured.

    ReplyDelete
    Replies
    1. I'm using Jetty 8. Yes, in Jetty9 a lot of packages and API's were changed. So if you want use Jetty 9, then you must change the code. I have no plans to update the code to use Jetty 9 as WildFly will bring full support for WebSockets.

      Delete
  7. It possible to jboss with 8443 port?

    ReplyDelete
  8. My application already run in Jboss (Spring + hibernate), There also listener as

    org.springframework.web.context.ContextLoaderListener


    can I implement web socket?

    ReplyDelete