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.
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.