Skip to main content

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

Wow! lots of feedback.

Ok, comments in inline.


On Tue, Aug 20, 2013 at 3:55 PM, Brandon Mintern <mintern@xxxxxxxxxxx> wrote:
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.


Jetty 9.0's websocket was preparing for many things.
JSR-356's javax.websocket API
Extensions (such as the various compression and mux extensions)
And more correct support for RFC-6455.
 

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.

While we can expose a Frame with the new API, we cannot eliminate the internal processing of that frame, as that complicates the state tracking.
I have to ask ... WHY do you need this feature?
If its for streaming large messages, know that we have Streaming APIs available in Jetty 9.1

 

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


This was to align the API closer to the JSR-356 javax.websocket.Session concepts.

 

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

This is correct.
Access to the HttpServletRequest is discouraged, as not all mechanisms for creating a WebSocket will even have a HttpServletRequest.
(Various muxed websocket connection techniques like WebSocket over SPDY and even the mux-extension would have a websocket be created without a HttpServletRequest object being created for it)
 


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:

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

Ah, yes, large files over websocket.
What the streaming APIs in Jetty 9.1 were created in mind of.
A lot of these concepts are part of the JSR-356 javax.websocket API now.
Look at javax.websocket.server.ServerEndpoint

@ServerEndpoint("/upload/receiveFiles.ws/{user}/{upload}")
public class ReceiveFilesWebSocket {
   @OnMessage
   public void onMessage(InputStream in, @PathParam("user") String user, @PathParam("upload") String upload)
   {
      // read from the stream here
   }
}

 


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.

This is also part of the JSR-356 javax.websocket API

@ServerEndpoint(value="/myapi", decoders={DataDecoder.class})
public class MySocket {
    @OnMessage
    public void onData(Data data) {

    }
}

Where you create your Decoder like this

public class DataDecoder implements Decoder.Text<Data> {
    public Data decode(String text) {
        return new Data().parse(text);
    }
}
 

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.

Sounds like you want to implement your own javax.websocket.Decoder for TEXT -> Data and leave normal processing for BINARY -> InputStream

 


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.

You should be using the Websocket PING/PONG for this keepalive mechanism.
 


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.

If you choose to use the JSR-356 API, then you want to look at javax.websocket.server.ServerApplicationConfig or even registering a javax.servlet.ServletContextListener and manually adding the endpoints you want to the Path Specs you want via the javax.websocket.server.ServerContainer.

If you choose to use the Jetty API in Jetty 9.1, then you might be better off using the org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter and manually adding entries to it via the #addMapping(PathSpec spec, WebSocketCreator creator) concepts.

Note, the org.eclipse.jetty.websocket.servlet.WebSocketCreator is the probably going to be your best friend :-)
It controls the creation of a WebSocket, regardless if it arrives via a servlet, a filter, or even an extension.

 


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?

Time marches on, the Servlet 3.1 spec + JSR-356/javax.websocket means we have to adapt as well.
The changes are hopefully going to suit your needs.
You probably did not know about what is available.
 

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

No prob, thanks for the feedback.
 

Cheers,
Brandon

_______________________________________________
jetty-users mailing list
jetty-users@xxxxxxxxxxx
https://dev.eclipse.org/mailman/listinfo/jetty-users



Back to the top