Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[jetty-users] Migration path for Jetty 8 -> Jetty 9 advanced WebSocket usage

We built some non-trivial WebSocket abstractions around Jetty 8's WebSockets. I found the API to be intuitive since it matched RFC 6455 pretty closely. We have been delaying an upgrade to Jetty 9 because it appears to be a lot of effort to rewrite our WebSocket library based on the new Jetty WebSocket abstractions.

If you don't mind helping out, I would love to share a bit of how we use Jetty and to try to come up with a reasonable path for migration to Jetty 9. I apologize in advance for the length of this email, but this is a pretty large system.

The quick summary is that there are three changes that are giving us the most trouble:

* The WebSocket.OnFrame interface, with the ability to return false in order to delegate to Jetty's usual handling. There appears to be no comparable mechanism in Jetty 9's WebSockets' annotated methods.

* Changing WebSocket.Connection to a Session callback parameter. It's unclear whether I can use the Session outside the scope of a callback method.

* The WebSocketServlet's doWebSocketConnect(HttpServletRequest req, String protocol) method. We actually passed off the `req` to Spring in order to make our mapped method calls, and we can no longer do this very easily, as Jetty 9 no longer passes an HttpServletRequest.

The (simplified) detailed usage follows:

We use Spring @RequestMapping-annotated methods for our HTTP requests, and I created a similar abstraction for WebSocket. To use a WebSocket, one can define a method like this in a @Controller-annotated class:

public WebSocketHandler receiveFiles(
        @ModelAttribute("user") User user,
        @RequestParam("upload") int uploadId) {
    return new FileUploadHandler(...);

A WebSocketHandler is internally defined, providing the following fields and methods:

WSConnection connection;
void onOpen();
void onClose();
interface HandleData {
    void onDataMessage(Data data);
interface HandleText {
    void onTextMessage(String msg);
interface HandleBinary {
    void onBinaryMessage(byte[] data, int start, int end);

All in all, this is pretty similar to Jetty 8's callback methods, and we'd like to retain them. WSConnection is a thin wrapper around Jetty 8's WebSocket.Connection (which is also gone now), providing:

void sendData(Object obj);
void sendText(Object msg);
void sendBinary(byte[] data, int start, int end);

You'll notice a separate type, that we call "Data". This allows us to use Google's Gson library to serialize an object to JSON, and then to deserialize it into a _javascript_ object on the frontend, and vice versa in the other direction.

We accomplish this by having a "WebSocketAdapter" that maps our WebSocket interface to Jetty 8's, implementing WebSocket, WebSocket.OnTextMessage, and WebSocket.OnFrame.

Our WebSocketAdapter.onFrame method is the most interesting and is the hardest to replicate using Jetty 9. We handle Binary messages in the obvious way: by simply calling WebSocketHandler.onBinaryMessage and returning true. Text message processing, however, proceeds as follows:

* Initial state: The message must be either "TEXT" or "DATA", continuing to the associated state and returning true (we've handled the message).

* TEXT state: Returns false for all of the frames of the next text message so that Jetty will call onMessage. Of course, the onMessage implementation calls our  WebSocketHandler.onTextMessage callback). When the message is complete, return to Initial.

* JSON state: Aggregate all frames efficiently so that they can later be deserialized from the resulting JSON. When the message is complete, we return to Initial state and call the WebSocketHandler.onDataMessage with the aggregated data.

So, it is important to us to be able to distinguish between two different types of text messages, and moreover, we use this mechanism to send very large JSON objects over the WebSocket.

I don't expect this particular change to be very difficult to implement, but the interface has taken a small step back for my usage in this case.

Incidentally, we also send periodic KEEPALIVE text messages from a server thread to the client. Our client-side WebSocket library just drops them, but they ensure that the connection stays open during long computations. I haven't looked a lot into the replacement for WebSocket.Connection (Session), but I'm really hoping that I can send data across the Session outside the scope of the callback method. If not, the KEEPALIVE mechanism becomes much more difficult to implement.

The harder change to deal with is our WebSocketServlet implementation. At server startup, we register all the methods annotated with @WebSocketMapping, binding them to the appropriate address. When doWebSocketConnect(HttpServletRequest req) is called, we fetch the appropriate method, and we use Spring's

    HandlerMethodInvoker.invokeHandlerMethod(method, controller, req, modelMap)

in order to actually call the method. This is really nice because it allows us to use the same @RequestParam and @ModelAttribute parameters on our @WebSocketMapping-annotated methods that we use on normal Spring @RequestMapping methods.

Now that Jetty 9 does not provide the HttpServletRequest to the WebSocketServlet, it no longer appears to be possible to leverage Spring in order to call our @WebSocketMapping-annotated methods.

In general, Jetty 9's WebSocket API seems to be an improvement for using the API directly. Using annotations and things is clearly in the vein of Spring and Hibernate and other popular Java libraries. When trying to build on top of it, though, I find myself wishing for an API more like Jetty 8, where I can just implement some interfaces and build my own abstractions on top of WebSocket.

Is there any hope going forward for a similar low-level, RFC 6455-like API that was provided in Jetty 8? Alternatively, am I doing things in a way that would be much more easily implemented by following another pattern?

Thank you very much to anyone who may have made it this far.


Back to the top