- Java EE 8 High Performance
- Romain Manni Bucau
- 988字
- 2021-06-30 19:14:24
The WebSocket layer
Why use JAX-RS and WebSocket? Don't they serve the same purpose? Not exactly, in fact, it is becoming more and more common to use both in the same application even if WebSocket is still a bit recent.
JAX-RS (and, more generally, HTTP/1 and the brand new HTTP/2) is generally web application oriented. Understand that it is often used for applications with a user interface (which needs to be compatible with all browsers). It is also commonly used in environments where you cannot assume much about the network setup. More particularly, in environments where you cannot assume the network setup, the proxies will let WebSocket connections work properly (either preventing them completely or disconnecting them too early). The last common case where HTTP-based solutions make a lot of sense is to try to target a market where clients can be developed in any language (Java, Python, Ruby, Go, Node.js, and so on). The fact that the technology is today spreading all over the world and works well with stateless connections, makes it easier to get started with, and it is therefore more accessible than WebSocket, which requires some care from client developers.
However, WebSocket will fit cases where you have higher performance or reactivity constraints, a state to maintain in order to handle the business use case, or you simply want to push the information from the server without requiring a client operation (such as polling).
When you start using a connected protocol such as WebSocket, the first thing to define is your own communication protocol: the format of the message you send/receive and the order of the messages (if needed).
Our WebSocket layer will be responsible for enabling a client to quickly access the quote prices. Therefore, we will react on a client's request (it will contain the name of the quote that we want to get the price for) and we will respond with two pieces of information: whether we found the quote and the current price, if existing.
Then, you need to pick a format to prepare the content sent through the WebSocket over the wire. Here, the choice is often guided by a trade-off between the client (consumers of the service), the requirements, the performances, and the ease of implementation. In our case, we will consider that our clients can be written in Java as well as in JavaScript. That is why we will use JSON.
To summarize the protocol, here is a full communication round-trip, as shown in the following diagram:
The communication protocol is based on a single message type in our case, so a full client/server communication looks like these steps:
- The client will connect to the server.
- The client will request the price of a quote N times, based on its symbol (name/identifier).
- Assuming there is no I/O error or timeout, the client will trigger a disconnect, which will end the communication.
In terms of code, we need multiple bricks of Java EE and we need the following to put them together:
- The WebSocket API, obviously
- JSON-B (we could use JSON-P, but it is less friendly) for the Java to JSON conversion
- CDI, to link the WebSocket to the business layer
To start easy, we can modelize our payloads. Our request has only one name attribute, so JSON-B allows us to define it this way:
public class ValueRequest {
private String name;
// getter/setter
}
On the other side (that is, the response), we have to return a value attribute with the price of the quote and a found Boolean marking value as filled or not. Here again, JSON-B allows us to do a direct mapping of this model with a plain POJO:
public static class ValueResponse {
private double value;
private boolean found;
// getters/setters
}
Now, we need to ensure that the WebSocket will be able to deserialize and serialize these objects as required. The specification defines Encoder and Decoder APIs for this purpose. Since we will back our implementation by JSON-B, we can directly implement it using the (I/O) stream flavors of these APIs (called TextStream). Actually, before doing so, we need to get a Jsonb instance. Considering that we have already created one and made it available in CDI, we can then simply inject the instance in our coders:
@Dependent
public class JsonEncoder implements Encoder.TextStream<Object> {
@Inject
private Jsonb jsonb;
@Override
public void encode(final Object o, final Writer writer) throws EncodeException, IOException {
jsonb.toJson(o, writer);
}
// other methods are no-op methods
}
The decoding side is now fast to develop, thanks to the JSON-B API, which fits this usage very well with its fromJson() API. We will just note that this side is specific to ValueRequest, since we need to specify the type to instantiate it (compared with the encoding side, which can determine it dynamically):
@Dependent
public class RequestDecoder implements Decoder.TextStream<ValueRequest> {
@Inject
private Jsonb jsonb;
@Override
public ValueRequest decode(final Reader reader) throws DecodeException, IOException {
return jsonb.fromJson(reader, ValueRequest.class);
}
// other methods are no-op methods
}
Now that we have a way to handle our messages, we need to bind our WebSocket endpoint and implement the @OnMessage method to find the price and send it back to the client relying on our business layer. In terms of implementation, we will react to a ValueRequest message, try to find the corresponding quote, fill the response payload, and send it back to the client:
@Dependent
@ServerEndpoint(
value = "/quote",
decoders = RequestDecoder.class,
encoders = JsonEncoder.class)
public class DirectQuoteSocket {
@Inject
private QuoteService quoteService;
@OnMessage
public void onMessage(final Session session, final ValueRequest request) {
final Optional<Quote> quote = quoteService.findByName(request.getName());
final ValueResponse response = new ValueResponse();
if (quote.isPresent()) {
response.setFound(true);
response.setValue(quote.get().getValue()); // false
}
if (session.isOpen()) {
try {
session.getBasicRemote().sendObject(response);
}
catch (final EncodeException | IOException e) {
throw new IllegalArgumentException(e);
}
}
}
}