Brief
This is the first on a 2-part blog about how to assemble an application that is using WebSockets on WildFly in a clustered environment on Amazon EC2. There will be five main sections covered here and they can be broken down into the following:- Architecture set up
- WildFly clustering configuration on EC2
- WebSocket application on WildFly
- Storing and ensuring high availability of data using Infinispan
- Apache load balancer configuration
This first part will be focusing on the first three points listed above. In the second part I will detail how to set up Infinispan and configure the Apache load balancer.
Introduction
To quote the lovely people at Mozilla, 'WebSockets is an advanced technology that makes it possible to open an interactive communication session between the user's browser and a server'. In a more technical sense, it means that it allows two-way communication between a client and a server over a single TCP connection. It is primarily designed to be used in a web application, i.e. with a browser-based front-end. However, it is possible to use it in any client-server application as well.
WebSockets are one of the new features in Java EE 7 (JSR-356), so we thought it would be a nice idea to build a simple web application that made use of WebSockets in a clustered environment.
For this example, we will use the WildFly Application Server, which is the free open source Java EE 7 platform available from JBoss. We will be running this entire demo on Amazon EC2.
WebSockets are one of the new features in Java EE 7 (JSR-356), so we thought it would be a nice idea to build a simple web application that made use of WebSockets in a clustered environment.
For this example, we will use the WildFly Application Server, which is the free open source Java EE 7 platform available from JBoss. We will be running this entire demo on Amazon EC2.
Architecture set-up
The following is the set up that we intend to use for the purposes of this application.
Both of the WildFly instances and the httpd one are set up to be on their own individual virtual machines on EC2. There are three main reasons for this set up:
- We only need to expose the public IP address of the httpd server, thus we don't need to get a public IP for any of the WildFly instances.
- We can add or remove more WildFly servers to the cluster if needed and not have to worry about the public facing IP address of the application.
- Since there is one application running, we don't need to run the server in domain mode, and standalone is sufficient.
WildFly clustering configuration
Below is an example of how to configure JGroups for clustering on WildFly, you would have to add this into either the standalone-ha.xml or standalone-full-ha.xml.
1 | <subsystemxmlns="urn:jboss:domain:jgroups:2.0"default-stack="tcp"> |
There are a few points to note here as compared to the default configuration that ships with the WildFly zip:
- On EC2, and other similar cloud providers, UDP multicast is disabled. Enabling multicast on a public cloud would mean a significantly higher number of messages being sent across the cloud infrastructure, resulting in a large performance hit to the service being provided. As a result, for the JGroups configuration on WildFly, we will use the TCP stack by default, and configure TCPPING.
- To configure TCPPING, we would need to add in the IP addresses of the Wildfly instances that will be present on start-up. This way we can allow for members to join the cluster. We also specify the port to be 7600.
- Finally, add in the number of initial members to 2.
WebSocket application
Below is a snippet of the main index.html page, which will produce two forms. These forms are used to either input a key/value pair or to simply get a value using a key.
<!-- Input form -->
<form>
<fieldset>
<label>Store:</label>
<inputid="putKey"type="text">
<inputid="putValue"type="text">
<inputtype="submit"id="store"value="StoreMe">
</fieldset>
</form>
<h4>Result from the submit operation:</h4>
<spanid="result"></span>
<br/>
<br/>
<!-- Get form -->
<form>
<fieldset>
<label>Get:</label>
<inputid="getKey"type="text">
<inputtype="submit"id="getValue"value="GetMe">
</fieldset>
</form>
<h4>Result from the get operation:</h4>
<spanid="getResult"></span>
The inputs from these forms will be processed by some JavaScript code, which does the following:
- Create two different WebSocket objects; one for storing and one for getting. These will have different endpoints associated with them - i.e. one has the resource 'store' and the other has 'getter'.
- Take the input from the forms, and then send that input using the WebSockets to the backend.
- Take the output from the server, and attach that message to the HTML. (See ws.onmessage and wsGet.onmessage)
<script>
var port = "";
var storerUrl = 'ws://' + window.location.host + port + window.location
.pathname + 'store';
var ws = new WebSocket(storerUrl);
var getterUrl = 'ws://' + window.location.host + port + window
.location.pathname + 'getter'
var wsGet = new WebSocket(getterUrl);
ws.onconnect = function(e) {
console.log("Connected up!");
};
ws.onerror = function(e) {
console.log("Error somewhere: " + e);
};
ws.onclose = function(e) {
console.log("Host has closed the connection");
console.log(e);
};
ws.onmessage = function(e) {
document.getElementById("result").innerHTML = e.data;
};
wsGet.onconnect = function(e) {
console.log("Connected up!");
};
wsGet.onerror = function(e) {
console.log("Error somewhere: " + e);
};
wsGet.onclose = function(e) {
console.log("Host has closed the connection");
console.log(e);
};
wsGet.onmessage = function(e) {
console.log("Received message from WebSocket backend");
document.getElementById("getResult").innerHTML = e.data;
};
document.getElementById("store").onclick = function(event){
event.preventDefault();
var key = document.getElementById("putKey").value;
var value = document.getElementById("putValue").value;
var objToSend = '{"key": ' + key + ', "value": ' + value + '}';
ws.send(objToSend);
};
document.getElementById("getValue").onclick = function(event) {
event.preventDefault();
var key = document.getElementById("getKey").value;
var objToGet = '{"key": ' + key + '}';
wsGet.send(objToGet);
};
</script>
That's all we require in order to set up the front end for this application. Again, it's important to note that this is a simple app which is demonstrating how to integrate WebSockets into your Java EE application. The next part is where we will look at how we can make use of WebSockets in our Java backend to deal with this input. First, let's look at the Storer class.
@ServerEndpoint("/store")
publicclassStorer {
privatestatic Logger storerLogger = Logger.getLogger(Storer.class.getName());
/**
* Method that will store some basic data using the application platform's
* clustered caching mechanism.
*
* @param data - The information, as a JSON from the js/html front-end.
* @param client - the client session
*/
@OnMessage
publicvoidstore(String data, Session client) {
String returnText = null;
// Let's get the Json object first.
JsonParserFactory factory = JsonParserFactory.getInstance();
JSONParser parser = factory.newJsonParser();
Map jsonMap = parser.parseJson(data);
if (jsonMap.containsKey("key") && jsonMap.containsKey("value")) {
// Store the data in the cache now that we have validated it.
String key = (String) jsonMap.get("key");
String value = (String) jsonMap.get("value");
// TODO: Not actually storing anything yet!
// Creating some string returns to go back to the front-end along
// with some logging statements.
StringBuilder sb = new StringBuilder();
sb.append("Well done. We now have your data!");
storerLogger.info("Going to store key " + key + " and value " +
value + ".");
sb.append(" Key: ").append(key).append(" Value: ").append(value)
.append(". ");
sb.append(" Stored at date: ").append(buildDate()).append(".");
returnText = sb.toString();
} else {
// Failed the validation check. So we are now going to log some
// information to the server and return some information to the
// front-end as well.
storerLogger.info("Seems to be a problem with the input. Cannot find" +
" the appropriate \'key\' and \'value\' string keys.");
returnText = "Problem with the input that you sent in. Are they just" +
" the default values?";
}
// Send to client.
client.getAsyncRemote().sendText(returnText);
}
private String buildDate() {
returnnewSimpleDateFormat("HH:mm dd/MM/yy").format(Calendar
.getInstance().getTime());
}
}
There are some important points to note over here:
- The @ServerEndpoint class level annotation tells the application container, Wildfly in this case, that this class will be dealing with WebSocket messages to the endpoint '/store'. Going back a little bit, in our HTML form, when we submit the key and value that we want to store, that input will be taken by this Java class.
- A method with the @OnMethod annotation will be called when a WebSocket sends a message to this endpoint. Any class which has a @ServerEndpoint annotation can only have one @OnMessage annotation. This method will take in a String parameter (our input) and a javax.websocket.Session object.
- In this case, we are using a simple JSON parser available here. It will parse our input into a Map object and then we can take out the keys and values which we require (as long as they exist!).
- After storing the key/value pair, we can then return back to the front-end.
- We use the Session object to send a message back to the front-end using the same WebSocket connection.
- WE ARE NOT ACTUALLY STORING ANYTHING IN THIS CLASS AS YET.
As we can see, we would need a separate class for our Getter object. This would follow a similar path except it would use a different endpoint on the @ServerEndpoint annotation, and would expect a slightly different format of input. Below is how the Getter class has been set up.
@ServerEndpoint("/getter")
publicclassGetter {
static Logger getterLogger = Logger.getLogger(Getter.class.getName());
/**
* Method that will attempt to get a value for a given key.
*
* @param data - A String that is the key
* @param client - the client session
*/
@OnMessage
publicvoidgetValue(String data, Session client) {
getterLogger.info("Getting key: " + data);
String returnText = "{No value found for key " + data + "}.";
try {
returnText = getValueFromCache(data);
} catch (Exception e) {
returnText = e.getMessage();
getterLogger.log(Level.WARNING, e.getMessage());
e.printStackTrace();
}
client.getAsyncRemote().sendText(returnText);
}
- The getValueFromCache() method is a dud here and doesn't do anything yet.
And that's it!
That's all for the first part of this blog looking at how to set up a clustered WebSocket application on Wildfly running on Amazon EC2. Going forward from here, we will look at how to use Infinispan for this application to allow for the availability of our data on fail-over and also on how we can put a load balancer in front of our two Wildfly instances.
Thanks for reading!
Navin Surtani
C2B2 Support Engineer