Search code examples
javascriptjavajettyjava-websocket

How can I add a WebSocketServlet to an embedded Jetty server with context path?


I'm trying to test WebSocket support in an embedded Jetty application I'm working on. My goal is to stream data from the server to browser. I haven't worked everything out, as I'm just starting with setting up the WebSocket servlet/handler.

The issue I'm seeing is Chrome won't connect to the WebSocket handler:

WebSocket connection to 'ws://127.0.0.1:8081/stream' failed: Error during WebSocket handshake: Unexpected response code: 404 (anonymous) @ ws-test.js:1

The HTTP 404 is from Jetty because it doesn't have anything registered with '/stream'.

For the life of me I can't figure out how to setup a WebSocketServlet or WebSocketHandler with the URL specified. I've read all the examples and tutorials I can find, but at lot of them are either not embedded Jetty, or are old. I'm willing to be wrong in either case.

I'll start with some code. Here's the main Jetty setup for my server context and handlers:

ResourceHandler resource_handler = new ResourceHandler();
resource_handler.setWelcomeFiles(new String[] { "index.htm" });
resource_handler.setResourceBase('./www');

ServletHandler servletHandler = new ServletHandler();

HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[] { resource_handler, servletHandler, new DefaultHandler() });
server.setHandler(handlers);

// Add the /test servlet mapping
servletHandler.addServletWithMapping(TestServlet.class, "/test/*");

// Add websocket handler
handlers.addHandler(StreamingHandler.getServlet("/stream"));            
server.start();

Here's my StreamingHandler class that extends WebSocketHandler. Note that I'm trying to set the WebSocketHandler's context path. The point is for the WebSocket to handle communication to http://127.0.0.1:8081/stream:

@WebServlet
public class StreamingHandler extends WebSocketHandler
{
    public static ContextHandler getServlet(String url) {
        ContextHandler ctxHandler = new ContextHandler();
        ctxHandler.setContextPath(url);
        ctxHandler.setHandler(new StreamingHandler());
        return ctxHandler;
    }

    protected StreamingHandler() {
        super();
    }

    @Override
    public void configure(WebSocketServletFactory factory)
    {
        factory.getPolicy().setIdleTimeout(10000);
        factory.register(StreamingSocket.class);
    }
}

Here's my basic WebSocket class:

@WebSocket
public class StreamingSocket {

    @OnWebSocketClose
    public void onClose(int statusCode, String reason) {
        System.out.println("Close: statusCode=" + statusCode + ", reason=" + reason);
    }

    @OnWebSocketError
    public void onError(Throwable t) {
        System.out.println("Error: " + t.getMessage());
    }

    @OnWebSocketConnect
    public void onConnect(Session session) {
        System.out.println("Connect: " + session.getRemoteAddress().getAddress());
        try {
            session.getRemote().sendString("Hello!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @OnWebSocketMessage
    public void onMessage(String message) {
    System.out.println("Message: " + message);
}

}

Finally a bit of cheap Javascript. I won't include the basic HTML. It just references this JS:

var ws = new WebSocket("ws://127.0.0.1:8081/stream");

ws.onopen = function() {
    alert("Opened!");
    ws.send("Hello Server");
};

ws.onmessage = function (evt) {
    alert("Message: " + evt.data);
};

ws.onclose = function() {
    alert("Closed!");
};

ws.onerror = function(err) {
    alert("Error: " + err);
};

Any tips are appreciated. Thanks.


Solution

  • In classic form, I've worked through the problem thanks to a link to the Jetty cookbook that Joakim Erdfelt (thanks!) answered in another SO question here: https://stackoverflow.com/a/34008707/924177

    Basically I took a look at the sample WebSocketServerViaFilter example here: https://github.com/jetty-project/embedded-jetty-cookbook/blob/master/src/main/java/org/eclipse/jetty/cookbook/websocket/WebSocketServerViaFilter.java

    I've never seen this usage, and the cookbook is new enough, but it works. Here's my new main server code (note that the DefaultServlet must be added if you expect your resources to be served):

    Path webRootPath = new File(www).toPath().toRealPath();
    
    ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    context.setContextPath("/");
    context.setBaseResource(new PathResource(webRootPath));
    context.setWelcomeFiles(new String[] { "index.html" });
    server.setHandler(context);
    
    // Add the websocket filter
    WebSocketUpgradeFilter wsfilter = WebSocketUpgradeFilter.configureContext(context);
    wsfilter.getFactory().getPolicy().setIdleTimeout(5000);
    wsfilter.addMapping(new ServletPathSpec("/stream"), new StreamingSocketCreator());
    
    // Add the /test servlet mapping
    ServletHolder holderTest = new ServletHolder("test", TestServlet.class);
    holderTest.setInitParameter("dirAllowed","true");
    context.addServlet(holderTest,"/test/*");
    
    // NOTE! If you don't add the DefaultServlet, your 
    // resources won't get served!
    ServletHolder holderDefault = new ServletHolder("default", DefaultServlet.class);
    holderDefault.setInitParameter("dirAllowed", "true");
    context.addServlet(holderDefault, "/");
    
    server.start();
    server.join();
    

    Then I created a WebSocketCreator implementation. This seems like an unnecessary step, but its how the API works:

    public class StreamingSocketCreator implements WebSocketCreator
    {
        @Override
        public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
        {
            return new PacketStreamingSocket();
        }
    }
    

    The same WebSocket code applies:

    @WebSocket
    public class StreamingSocket {
    
        @OnWebSocketClose
        public void onClose(int statusCode, String reason) {
            System.out.println("Close: statusCode=" + statusCode + ", reason=" + reason);
        }
    
        @OnWebSocketError
        public void onError(Throwable t) {
            System.out.println("Error: " + t.getMessage());
        }
    
        @OnWebSocketConnect
        public void onConnect(Session session) {
            System.out.println("Connect: " + session.getRemoteAddress().getAddress());
            try {
                session.getRemote().sendString("Hello!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @OnWebSocketMessage
        public void onMessage(String message) {
        System.out.println("Message: " + message);
    }
    

    Finally, my JavaScript code can find the WebSocket context path!

    var ws = new WebSocket("ws://127.0.0.1:8081/stream");
    
    ws.onopen = function() {
        alert("Opened!");
        ws.send("Hello Server");
    };
    
    ws.onmessage = function (evt) {
        alert("Message: " + evt.data);
    };
    
    ws.onclose = function() {
        alert("Closed!");
    };
    
    ws.onerror = function(err) {
        alert("Error: " + err);
    };
    

    It all work together. Now I can test out server -> client data streaming. Thanks and I hope this helps someone else in the future.