Sending Messages

This chapter will dive into the details of how to send messages to WebSocket endpoints.

The API for sending messages is the same for annotated as well as programmatic endpoints which in contrast to receiving messages (next chapter) which are handled differently for different endpoints

As already stated, the Java WebSocket API supports binary, text, custom Java objects and ping-pong message payloads. These message types can be sent using various styles/modes

  • Asynchronous

  • Synchronous

  • Partial, and

  • Streaming

As you might guess, this leads to a lot of possible permutations and combinations for sending messages (and can be quite confusing at times). Hopefully, things should be clear by the end of this chapter

Send modes: a quick primer

Before we dive into the nitty gritty, it'll be good to have an overview of the different message transmission modes

Mode

Description

Synchronous

the client sending the message is blocked until the process is completed (or an error occurs)

Asynchronous

client thread is released immediately and it can track the process using a Future object or a callback implementation

Partial

the message is delivered in parts, the client needs to keep track of them and tell the API when its done sending all the parts

Streaming

makes use of Java character/output stream to send messages

Sending text messages

Synchronous

This is the most easy-to-understand method. Just make used of the public void sendText(String msg) in RemoteEndpoint.Basic interface

//synchronous delivery

....
@OnMessage
public void onReceipt(String msg, Session session){
  session.getBasicRemote().sendText("got your message ");
}
....

Asynchronous

This mode is handled by the RemoteEndpoint.Async interface which exposes two methods

  • public Future<Void> sendText(String msg): returns a java.util.concurrent.Future object

  • public void sendText(String msg, SendHandler handler): allows the user to provide a callback handler implementation

//asynchronous text message delivery

....
@OnMessage
public void onReceipt(String msg, Session session){
  Future<Void> deliveryTracker = session.getAsyncRemote().sendText("got your message ");
  deliveryTracker.isDone(); //blocks
}
....
//asynchronous text message delivery using a callback

....
@OnMessage
public void onMsg(String msg, Session session){
  session.getAsyncRemote().sendText("got your message ", new SendHandler() {
  @Override
  public void onResult(SendResult result) {
    pushToDB(session.getID(), msg, result.isOK());
   }
 });
}
....
//Java 8 lambda style

....
session.getAsyncRemote()
.sendText("got your message ",
(SendResult result) -> {pushToDB(session.getId(),msg, result.isOK());}
);
....

Partial

Sending messages in part can be done by using an overloaded version of the sendText method in the RemoteEndpoint.Basic interface. The process is synchronous in nature

//partial message delivery

....
String partialData = fetch(request);
try {
  session.getBasicRemote().sendText(partialData, false);
} catch (IOException ex) {
  throw new RuntimeException(ex);
}
...

Streaming

One can stream textual (character) data to a java.io.Writer provided by the public void getSendWriter() in RemoteEndpoint.Basic. Any of the overloaded write methods in Writer can be used

//streaming strings

....
private Session session;

public void broadcast(String msg){
  session.getBasicRemote().getSendWriter().write(msg);
}
....

Summary

Here is a table summarizing possible message sending combinations for text data

Sending style for text messages

Method signature

Synchronous

public void sendText(String msg)

Asynchronous

public Future<Void> sendText(String msg), void sendText(String msg, SendHandler callback)

Partial

public void sendText(String part, boolean isLast)

Streaming

public void getSendWriter().write(String msg)

Sending binary data

Handling (sending) Binary data is similar to String as far as the API is concerned. The only (obvious) difference being the data type - ByteBuffer in case of binary data as opposed to String for textual data. The supported modes are also the same (as for text data)

Synchronous

//synchronous delivery of an image

....
public void syncImage(byte[] image, Session session){
  ByteBuffer img = ByteBuffer.wrap(image);
  session.getBasicRemote().sendBinary(img);
}
....

Asynchronous

//asynchronous delivery of an image

....
public void syncLargeImage(byte[] image, Session session){
  ByteBuffer img = ByteBuffer.wrap(image);
  Future<Void> deliveryProgress = session.getAsyncRemote().sendBinary(img);
  boolean delivered = deliveryProgress.isDone(); //blocks until completion or failure
}
....

Partial

//partial delivery of binary data

....
ByteBuffer partialData = fetch(request);
try {
  session.getBasicRemote().sendBinary(partialData, false);
} catch (IOException ex) {
  throw new RuntimeException(ex);
}
...

Streaming

Use the getSendStream() method on RemoteEndpoint.Basic to get an OutputStream and use any of the overloaded write methods to transmit binary data

//binary data - streaming style

....
ByteBuffer data = fetch(request);
try {
session.getBasicRemote().getSendStream().write(data.array());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
...

Summary

Here is the gist

Sending style for binary messages

Method signature

Synchronous

public void sendBinary(ByteBuffer data)

Asynchronous

public Future<Void> sendBinary(ByteBuffer data) ,public void sendBinary(ByteBuffer data, SendHandler callback)

Partial

public void sendBinary(ByteBuffer part, boolean isLast)

Streaming

public void getSendStream().write(byte[] data)

Sending Java objects

More often than not, your business logic will internally deal with objects rather than their raw binary or textual representations. As mentioned earlier, WebSocket supports text and binary data as it on-wire format. Thus, Java objects within your code would need to be transformed into their text or binary forms for them to be sent over a WebSocket connection.

Similar to what we saw in case of native (binary and text) messages, the RemoteEndpoint.Basic and RemoteEndpoint.Async interfaces contain appropriate methods which support Java object transmission - both synchronously and asynchronously.

Here is a quick peek

Sending style for (Java) objects

Method signature

Synchronous

public void sendObject(Object obj)

Asynchronous

public Future<Void> sendObject(Object obj), public void sendObject(Object obj, SendHandler callback)

The question is -

how does a java.lang.Object representation get converted into text or binary format ?

This is done with the help of an Encoder. A javax.websocket.Encoder encapsulates the logic to convert a message from a Java object into an on-wire format supported by the WebSocket protocol (i.e. text or binary). Before diving into Encoders, let's look at a code sample for sending stock prices (represents as a StockQuote java object) asynchronously

The synchronous counterpart is very simple - you just need to use the sendObject method on RemoteEndpoint.Basic

....
private Session client;

public void broadcast(String msg) {
  Set<String> subscriptions = (Set<String>) client.getUserProperties().get("TICKER_SUBSCRIPTIONS");
  StockQuote quote = null; //the Java object
  for (String subscription : subscriptions) {
   try {
    quote = fetchQuote(subscription);
    //sending stock quotes with a Java 8 lambda style callback
    peer.getAsyncRemote().sendObject(quote,
    (SendResult result) -> {audit(session.getId(),quote, result.isOK());}
    );
      }
   catch (Exception e) {
    //log and continue...
    }
  }
}
....

WebSocket Encoders: the details

Take a look at the diagram below to visualize how Encoders work at runtime. It should be relatively easy to grasp what's going on

A summary of Encoders and their types

Encoder Type

Description

Encoder

The top level interface for different types of Encoders

Encoder.Text<T>

Transforms a custom Java object (of type T) to a textual (java.lang.String) format

Encoder.Binary<T>

Transforms to transform a custom Java object (of type T) to a binary (java.nio.ByteBuffer) format

A sample to demonstrate how an Encoder implementation would look like for a Java object which represents stock prices

//encoding a 'StockQuote' Java object to a JSON string

public class StockQuoteJSONEncoder implements Encoder.Text<StockQuote> {
  @Override
  public void init(EndpointConfig config) {
    //for custom initialization logic (details omitted)
  }
  @Override
  public String encode(StockQuote stockQuoteObject) throws EncodeException {
    //using the JSON processing API (JSR 353)
    return Json.createObjectBuilder()
    .add("quote", stockQuoteObject.getQuote())
    .add("ticker", stockQuoteObject.getTicker())
    .toString();
  }
  @Override
  public void destroy() {
    //close resources (details omitted)
  }
}

Streaming your Java objects

Native text and binary messages can be sent as streams - as explained in the previous sections. Java objects can also be transmitted in a streaming style. The respective methods are not directly exposed via RemoteEndpoint.Basic or RemoteEndpoint.Async. It's actually handled via the Encoder implementation

Encoder Type

Description

Encoder.TextStream<T>

Converts a custom Java object (of type T) and transmit it as a character stream (using java.io.Writer)

Encoder.BinaryStream<T>

Converts to transform a custom Java object (of type T) and transmit it as a binary stream (using java.io.OutputStream)

This example should help

//sending Java objects as character stream

public class StockQuoteJSONEncoder implements Encoder.TextStream<StockQuote> {
  @Override
  public void init(EndpointConfig config) {
    //for custom initialization logic (details omitted)
  }
  @Override
  public void encode(StockQuote stockQuoteObject, Writer writer) throws EncodeException {
    //using the JSON processing API (JSR 353)
    String jsonStockQuote = Json.createObjectBuilder()
    .add("quote", stockQuoteObject.getQuote())
    .add("ticker", stockQuoteObject.getTicker())
    .toString();
    writer.write(jsonStockQuote);
  }
  @Override
  public void destroy() {
    //close resources (details omitted)
  }
}

Thus, if you need to send your Java objects as a binary or character stream, you would need implement and register an appropriate Encoder corresponding to your Java type and the rest will be handled by the WebSocket runtime (your Encoder will be automatically invoked)

What about Java primitives ?

A WebSocket implementation provides default encoders for Java primitive (int-Integer, long-Long, double-Double etc.) data types. It is possible to write a custom encoder for any of these in order to override the default ones

Exchanging health status (ping-pong) messages

Exchaning ping-pong messages is a way to check up on the health status of the connection b/w a pair of WebSocket peers

Ping/Pong

Description

Ping

A health check request message. The API does not provide an object corresponding to this message (its a byte buffer)

Pong

Response to a health check status, represented by javax.websocket.PongMessage. It can also be used as a one-way heartbeat message (without the ping message being involved)

Common semantics for Ping-Pong messages

  • They are nothing but binary data and take the form of a ByteBuffer (as explained earlier) as far the API is concerned

  • They cannot be larger than 125 bytes (as is the case with WebSocket Control Frames in general) - these are just health-check messages and not meant for core/application level data exchange b/w WebSocket endpoints

Capability to send ping and pong messages are defined by the top level javax.websocket.RemoteEndpoint interface which means that they are inherited by its RemoteEndpoint.Basic and RemoteEndpoint.Async

This means that both these messages can be sent in a sync and async manner

Sending style for Ping and Pong messages

Method signature (ping)

Method signature (pong)

Synchronous & Asynchronous

void sendPing(ByteBuffer ping)

void sendPong(ByteBuffer pong)

//sending a ping (health check request)
.....
private Session s;

public void healthCheck(){
  s.getBasicRemote().sendPing(ByteBuffer.wrap("health-check".getBytes()));
}
.....

Additional notes

  • A Ping message is only meant to be sent (not recieved) as opposed to Pong, which can be sent and recieved

  • One does not need to write logic to explicitly return a pong message in response to a ping - the Java WebSocket API implementation will do that for you automatically

  • A Pong message can also be used as a self inititated heart beat message (not just in response to ping)

//sending a pong (as a one-way heart beat)

s.getBasicRemote().sendPong(ByteBuffer.wrap("health-check".getBytes()));

Asynchronous timeouts

Throughout this lesson, we have seen strategies of being able to send messages in an asynchronous manner which avoid blocking the sending thread. This is a great where your solution needs to scale in order to support a large number of clients.

But, is there limit on how long can we wait for the asynchronous process to complete ?

The answer is yes

Timeout support in the API

  • first and foremost, there is a notion of a timeout and this can be configured using the setSendTimeout method in the RemoteEndpoint.Async interface

  • secondly, the failure result manifests itself using the Future object or SendResult

How do timeouts manifest ?

In case you are using the SendHandler i.e. the callback handler route, the timeout exception details will be available via SendResult.getException()

//bail out if the message is not sent in 1 second

....
public void broadcast(Session s, String msg){
  RemoteEndpoint asyncHandle = s.getRemoteAsync();
  asyncHandle.setSendTimeout(1000); //1 second
  asyncHandle.sendText(msg,
  new SendHandler(){
    @Override
    public void onResult(SendResult result) {
      if(!result.isOK()){
      System.out.println("Async send failure: "+ result.getException());
      }
    }
  }); //will timeout after 2 seconds
}
....

If you chose to use Future to track the completion, calling it's get method will result in a java.util.concurrent.ExecutionException

//bail out if the message is not sent in 2 seconds

....
public void broadcast(Session s, String msg){
  RemoteEndpoint asyncHandle = s.getRemoteAsync();
  asyncHandle.setSendTimeout(2000); //2000 ms
  Future<Void> tracker = asyncHandle.sendText(msg); //will timeout after 2 seconds
  tracker.get(); //will throw java.util.ExecutionException if the process had timed out
}
....

Before we proceed

.. a quick refresher. The below table provides a quick preview of which mode is supported for which message type

Sending style

Text

Binary

Java object

Pong

Ping

Synchronous

y

y

y

y

y

Asynchronous

y

y

y

y

y

Partial

y

y

n

n

n

Streaming

y

y

y

n

n

The next lesson ...

... will dive into the other half of the message exchange process i.e. Receiving Messages

Last updated