Consuming WebSocket with Java HTTP Client

As software developers, we’re regularly relying on libraries to accomplish things that the programming language or platform doesn’t provide. This is the case for HTTP clients in Java. Since Java 9, there is an HTTP client built-in within Java, which was enhanced in Java 11.

Although this feature is out of incubation since Java 11, only recently I used it to this simple library. In this post, we’ll also learn how to use to consume WebSockets.

The WebSocket service

Providing us with the WebSocket service is Undertow. It allows us to easily start a WebSocket server that will echo anything that we send over. Let’s take a look:

static int undertowPort;
static Undertow undertow;

@BeforeAll
static void prepare() throws Exception {
  var serverSocket = new ServerSocket(0);
  undertowPort = serverSocket.getLocalPort();
  serverSocket.close();

  var listener = new AbstractReceiveListener() {
    @Override
    protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) {
      WebSockets.sendText(message.getData(), channel, null);
    }
  };

  undertow = Undertow.builder()
    .addHttpListener(undertowPort, "localhost")
    .setHandler(path()
      .addPrefixPath("/echo", websocket((exchange, channel) -> {
        channel.getReceiveSetter().set(listener);
        channel.resumeReceives();
      })))
    .build();

  undertow.start();
}

@AfterAll
static void release() {
  undertow.stop();
}

Before we run the tests, we assign a random port to Undertow’s server and set the path which will be consumed by the next section in this post.

The WebSocket client

Creating a WebSocket client also requires the creation of a HttpClient. We can do that by calling HttpClient.newHttpClient(). Once we’ve created the HttpClient, we can create a builder for the WebSocket:

var httpClient = HttpClient.newHttpClient();
var webSocketBuilder = httpClient.newWebSocketBuilder();

From the webSocketBuilder, we’ll use buildAsync to build our WebSocket:

var uri = URI.create("ws://localhost:" + undertowPort + "/echo");
var webSocket = webSocketBuilder
      .buildAsync(uri, listener)
      .get()

The get() method will block the current thread and subscribe itself to the buildAsync, which returns a CompletableFuture<WebSocket>, giving us the newly created WebSocket.

The buildAsync method expects a URI and a WebSocket.Listener instance. The listener will be responsible for handling the messages that we get. We can handle, for example, text data by overriding the onText method or binary data by overriding the onBinary method. Let’s take a look at the text example:

var listener = new WebSocket.Listener() {
      @Override
      public CompletionStage<Void> onText(WebSocket webSocket, CharSequence data, boolean last) {
        webSocket.request(1);
        return CompletableFuture.completedFuture(data)
          .thenAccept(o -> System.out.println("Handling data: " + o));
      }
    };

The onText method accepts three parameters

  1. WebSocket webSocket: our previously created instance;
  2. CharSequence data: the data sent by the server;
  3. boolean last: it tells us if the data is the last piece of the whole data;

The WebSocket class also exposes methods to send data over, such as the sendText method. Let’s send a text:

webSocket.sendText("Spread love. ", true);

The parameter with the value true means that this is our only and final piece of text. If we continuously send text passing false, it will consider a continuation of the message until it gets a true value.

Here is the complete example:

@Test
@DisplayName("Should echo the message")
void echoTheMessage() throws Exception {
  var echoed = new AtomicBoolean(false);
  var listener = new WebSocket.Listener() {
    @Override
    public CompletionStage<Void> onText(WebSocket webSocket, CharSequence data, boolean last) {
      webSocket.request(1);
      return CompletableFuture.completedFuture(data)
        .thenAccept(o -> echoed.set("Spread love. With a wonderful message.".contentEquals(o)));
    }
  };

  var uri = URI.create("ws://localhost:" + undertowPort + "/echo");
  var webSocket = HttpClient.newHttpClient().newWebSocketBuilder()
    .buildAsync(uri, listener)
    .get();

  webSocket.sendText("Spread love. ", false);
  webSocket.sendText("With a wonderful message.", true);
  await().untilTrue(echoed);
}

Alright, that we have it. Our echo service. We’re sending the text message and checking for the content received back. In order to check the validity of the message, the Awaitability library is quite handy for asynchronous tests.

The source code for this post is on GitHub.

Conclusion

It’s notorious the general opinion on the usage of built-in libraries over third parties libraries. We would be naïve to accept that built-in is always better though. In many cases, applications require extra features that aren’t present. In cases like this, it fits perfectly.

See you next time!