January 4, 2019
Most of web programming involves a browser sending an HTTP request to a server, the server processing the request, and the server returning an HTTP response. This Request-Response model works well when the processing is quick and the payloads are small. However, when service is slow, the user is forced to wait. If that waiting is too long, then a timeout results. This article presents a solution called WebSockets which gives the user continuous feedback and timely feedback on processing using a firewall-friendly protocol.
Unlike plain HTTP, WebSockets are a bi-directional protocol. This means that a connection is retained during an interaction, and the server can continually push updates to the browser. WebSockets are asynchronous. At any point, either connection can send a message to the other end. Although this particular example addresses long-running processes, many other use cases can be supported such as pushing chat messages to connected clients or transmitting video game positions.
The WebSocket API was added to Java EE 7. Every JavaEE app server is required to support it. The code in this example was deployed to WildFly 15 which is a JavaEE 8 compliant app server.
The following video shows a web page "imageInfo.html". The function of imageInfo.html is to find the file sizes of a bunch of images on the Web. A plain HTTP GET retrieves the static web resource from the WildFly 15 app server. Pressing the Run Fetch button opens a WebSocket to WildFly and immediately sends a message to start the processing. WildFly hits each of the specified URLs, retrieving each file size. As each URL is hit, a message is sent back to the browser which is appended to the imageInfo.html document.
There is also a status change. Too fast to see, the status label is marked as "Started" when the button is pressed. As the updates are piped from the server back to the client, the status changes to "Running". Finally, when the last image has been queried, the server sends a terminal message and the label is marked as "Finished".
The code will be developed in the IntelliJ IDE as a multi-module Maven project for portability. Although there is only one module in this example, the structure will be expanded upon in future posts. The module is a WAR to be deployed in WildFly.
This article shows how to develop RESTful Web Services for the WildFly platform. The services will be built using Maven which to support Continuous Integration. The development environment is the IntelliJ IDE which allows for some optimizations to make development more rapid.
This article is on the IntelliJ Ultimate Edition which is the commercial (not free) edition. Ultimate is needed for the JBoss connector.
With wf15-websocket-demo selected in the Project Browser
Next, edit the parent and module poms. The individual edits will be described below. Use the listed pom.xml files for the specific syntax. A few steps are flagged as needed only for this demo and not for WebSocket projects in general.
The Project Browser should show a Module nested in a Project. The source directory structure and Java files will be added in the next section.
The following is the pom.xml for the parent.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.bekwam</groupId>
<artifactId>wf15-websocket-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>wf15-websocket-demo-web</module>
</modules>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>
The following is the pom.xml for the child module.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wf15-websocket-demo</artifactId>
<groupId>org.bekwam</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wf15-websocket-demo-web</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.websocket</groupId>
<artifactId>jboss-websocket-api_1.0_spec</artifactId>
<version>1.0.0.Final</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- not required, but nice to have for data serialization -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- not required, used in the demo code -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<version>4.5.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
The code presented in this article consists of a web page, a server-side component running in WildFly, and some supporting classes for the data transport.
The following diagram shows the internal structure of the demo presented in the earlier video.
The browser initiates the action. A WebSocket connection is made by imageInfo.html and an initial text message is sent to the endpoint. The parameters might seem strange, "Some text" and "dummy". The WebSocket API support three different method types: text, binary, and Pong. Pong is a special WebSocket API message to enable a heartbeat for connection status. There is no "no-parameter" method so something must be passed to initiate the fetch.
The server responds with a START message. From this point on, the server will push updates to the browser. That's just a behavior for this particular demo. The client can send messages back to the server at any time. A cancel message terminating the file loop iteration comes to mind.
Next, the server-side code iterates through a list of files. As each file is retrieved, a WebSocket API Encoder is called to translate the Java object MessageEnvelope into a String. The Encoder itself delegates to some Gson code which implements Serializer.
Finally, the server responds with an END message. When received by the browser, the WebSocket is closed.
The following is the listing for imageInfo.html. You will need to modify the WebSocket constructor argument if your artifactId is different that what I'm using. This is a file that resides in the src/main/webapp folder of the project.
This HTML page is based on an example I found at tutorialspoint.
<!DOCTYPE html>
<html>
<head>
<title>Bekwam Image Info WebSocket Demo</title>
<script>
function fetchImages() {
var myNode = document.getElementById("data");
while (myNode.firstChild) {
myNode.removeChild(myNode.firstChild);
}
if ("WebSocket" in window) {
document.getElementById("status").innerText = "Status: Started";
var ws = new WebSocket("ws://localhost:8080/wf15-websocket-demo-web-1.0-SNAPSHOT/imageInfo");
ws.onopen = function() {
ws.send("Some value");
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
var obj = JSON.parse(received_msg);
if( obj.type == "START" ) {
document.getElementById("status").innerText = "Status: Running";
} else if( obj.type == "UPDATE") {
var newElem = document.createElement("li");
var newText = document.createTextNode(obj.payload.url + " (" + obj.payload.fileSize + " bytes)");
newElem.appendChild(newText);
document.getElementById("data").appendChild( newElem );
} else if( obj.type == "END" ) {
document.getElementById("status").innerText = "Status: Finished";
ws.close();
} else {
alert("Unrecognized type in json=" + received_msg);
}
};
ws.onerror = function(evt) {
document.getElementById("status").innerText = "Status: Error";
ws.close();
}
} else {
alert("WebSocket is NOT supported by your Browser!");
document.getElementById("status").innerText = "Status: Error";
}
}
</script>
</head>
<body>
<h1>Bekwam Image Info WebSocket Demo</h1>
<button type="button" onclick="fetchImages()">Run Fetch</button>
<p id="status">Not Started</p>
<ul id="data"></ul>
</body>
</html>
The next code listing is for the server-side component, ImageInfoEndpoint.
package websocket;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
@ServerEndpoint(value = "/imageInfo",encoders = { MessageEnvelopeEncoder.class })
public class ImageInfoEndpoint {
private Logger logger = Logger.getLogger("ImageInfoEndpoint");
private String[] files = {
"https://courses.bekwam.net/public_tutorials/images/bkcourse_archdemo_mvc_uml.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_archdemo_mvvm_screenshot.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_archdemo_mvvm_uml.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_activity.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_screenshot_cancel.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_screenshot_fetched.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_screenshot_fetching.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_afterspacing.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_final_resize.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_final_sb.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_nostyling.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_posthbox.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_resize_2.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_resize_posthbox.png",
"https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_resized_markedup.png"
};
@OnOpen
public void wsOpen() {
logger.info("opening connection" );
}
@OnClose
public void wsClose() {
logger.info( "closing connection" );
}
@OnMessage
public void fetchImages(Session session, String dummy) throws Exception {
//
// First step of protocol: Respond to a text message (can be send from client immediately
// after opening)
//
logger.info("called fetchImages()");
//
// Second step of protocol: Send a START message
//
session.getBasicRemote().sendObject( new MessageEnvelope(MessageType.START, "") );
//
// Third step of protocol: Send zero or more UPDATE messages
//
doFetchImages()
.map( p -> new MessageEnvelope(MessageType.UPDATE, p) )
.forEach( m -> {
try {
session.getBasicRemote().sendObject( m );
} catch (Exception exc) {
logger.log(Level.SEVERE, "error encoding or sending json=" + m, exc );
}
} );
//
// Fourth step of protocol: Send an END message (closes WS on client side)
//
session.getBasicRemote().sendObject( new MessageEnvelope(MessageType.END, "") );
}
private Stream<Pair<String, Long>> doFetchImages() {
return Arrays.asList( files )
.stream()
.map(
fn -> {
if( logger.isLoggable(Level.INFO) ) {
logger.info("[RETRIEVE] retrieving fn=" + fn);
}
return new ImmutablePair<gt;(fn, getFileSize(fn));
}
);
}
private long getFileSize(String fn) {
try {
HttpResponse r = Request.Get(fn)
.connectTimeout(1000)
.socketTimeout(1000)
.execute()
.returnResponse();
if( r.getStatusLine().getStatusCode() == 200 ) {
return r.getEntity().getContentLength();
} else {
logger.log(Level.SEVERE, "error retrieving fn=" + fn + "; status=" + r.getStatusLine().getStatusCode());
}
} catch(Exception exc) {
logger.log(Level.SEVERE, "error retrieving fn=" + fn, exc);
}
return -1L;
}
}
The previous two code listings implement the bulk of the functionality. The next classes are used to streamline the data transport. These are showing a preferred coding style which lets you work with Java objects instead of raw Strings.
MessageEnvelope communicates a MessageType and a payload from the client to the server.
package websocket;
import java.io.Serializable;
public class MessageEnvelope implements Serializable {
private final MessageType messageType;
private final Object payload;
public MessageEnvelope(MessageType messageType, Object payload) {
this.messageType = messageType;
this.payload = payload;
}
public MessageType getMessageType() {
return messageType;
}
public Object getPayload() {
return payload;
}
}
MessageType is an enumeration that defines the type of message returned by the server to the client. Notice that the values appear in the OnMessage logic in imageInfo.html.
package websocket;
public enum MessageType {
START,
UPDATE,
END
}
An Encoder is a WebSocket API class that can be associated with a ServerEndpoint. This allows me to call sendObject() from the Endpoint so that I can handle the conversion to a String in a consistent manner. Alternatively, I could call sendText() and form the encoded String by hand.
package websocket;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
public class MessageEnvelopeEncoder implements Encoder.Text<MessageEnvelope> {
private Gson gson;
@Override
public void init(EndpointConfig ec) {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(MessageEnvelope.class, new MessageEnvelopeSerializer());
gson = builder.create();
}
@Override
public void destroy() { }
@Override
public String encode(MessageEnvelope messageEnvelope) throws EncodeException {
return gson.toJson(messageEnvelope);
}
}
This is neither required for WebSockets nor WildFly, but I'm using Gson to assist with the JSON formatting. I'm returning a Commons Lang 3 Pair object from my "business logic" (the file size retrieving code).
package websocket;
import com.google.gson.*;
import org.apache.commons.lang3.tuple.Pair;
import java.lang.reflect.Type;
public class MessageEnvelopeSerializer implements JsonSerializer<MessageEnvelope> {
@Override
public JsonElement serialize(MessageEnvelope messageEnvelope, Type type, JsonSerializationContext jsonSerializationContext) {
JsonObject obj = new JsonObject();
obj.add( "type", new JsonPrimitive(messageEnvelope.getMessageType().toString()));
if( messageEnvelope.getMessageType().equals(MessageType.UPDATE) ) {
Pair<String, Long> data = (Pair<String, Long>) messageEnvelope.getPayload();
JsonObject inner_obj = new JsonObject();
inner_obj.add( "url", new JsonPrimitive(data.getKey()) );
inner_obj.add( "fileSize", new JsonPrimitive(data.getValue()) );
obj.add( "payload", inner_obj );
}
return obj;
}
}
This article uses an IntelliJ setup described in this post. You'll deploy your WAR file as an exploded configuration. Additionally, I use the "Update classes and resource" setting for the Update and Frame actions since I'm working with a static web resource in this example. I don't need to redeploy the exploded WAR to affect the class loader if I make a web page-only modification.
The URL for the deployed application -- using my Maven coordinates -- is http://localhost:8080/wf15-websocket-demo-web-1.0-SNAPSHOT/imageInfo.html. If your artifactId is different, modify accordingly.
This article solves a need I frequently have with JavaEE: providing status for long-running processes. Unlike a Request-Response model, this program is able to let the browser and the server operate independently. Yet, the browser is made aware of the back-end processing. There is much more that you can do with WebSockets to produce a robust and iterating interface without inefficient workarounds like long polling.
By Carl Walker
President and Principal Consultant of Bekwam, Inc