Introduction to WebSockets :: Part - 4
In Part-1
as well as in Part-3
we got our hands dirty with simple WebSockets
examples that passed text WebSocket messages
between the client and the server.
In a real world web application, typically we pass JSON
objects between the client and the server.
In this part, we will do just that - create and send a JSON
object from the client as a request to the WebSocket
server endpoint and in response the server endpoint will send both the CPU and MEMORY metrics
as JSON object.
Since we will be using JSON for message
passing between the client and the server, we need a Java framework that
can encode and decode JSON . For this we will
leverage the Java API for JSON Processing
(JSR 353) Reference Implemention.
Download the Java API for JSON Processing
(JSR 353) Reference Implementation JARs from the following site:
Copy the just downloaded JARs to the directory $CATALINA_HOME/lib .
Hands-on with WebSockets - 3
The following is the Java POJO class that encapsulates the client
request to the server endpoint:
Listing.1
/*
*
* Name: MetricsRequest
*
* Author: Bhaskar S
*
* Date: 02/16/2014
*
*/
package com.polarsparc.websockets;
/*
* Represents the client metrics request POJO
*/
public class MetricsRequest {
private boolean cpu = false;
private boolean mem = false;
public boolean isCPU() {
return cpu;
}
public void setCPU(boolean flag) {
cpu = flag;
}
public boolean isMEM() {
return mem;
}
public void setMEM(boolean flag) {
mem = flag;
}
}
When the client web application initiates a WebSocket
connection, it sends a JSON string. How do we
convert the JSON request into an object of
type com.polarsparc.websockets.MetricsRequest
???
Enter javax.websocket.Decoder . The Decoder class is an interface that allows one to
extend and implement a concrete class which the WebSocket
server can invoke on incoming client WebSocket
messages to convert to the corresponding application specific POJO s.
The following is our custom Decoder class
that will convert the incoming client JSON
request to an object of type MetricsRequest :
Listing.2
/*
*
* Name: MetricsRequestDecoder
*
* Author: Bhaskar S
*
* Date: 02/16/2014
*
*/
package com.polarsparc.websockets;
import java.io.StringReader;
import javax.json.Json;
import javax.json.JsonObject;
import javax.websocket.Decoder;
import javax.websocket.DecodeException;
import javax.websocket.EndpointConfig;
/*
* A custom Decoder class that will take the incoming client websocket text
* message (which is in JSON format) and convert it into a Java POJO of type
* MetricsRequest
*/
public class MetricsRequestDecoder implements Decoder.Text<MetricsRequest> {
@Override
public void init(EndpointConfig config) {
// Ignore - not used
}
@Override
public void destroy() {
// Ignore - not used
}
/*
* This method will decode the given JSON message into an object of type
* MetricsRequest.
*/
@Override
public MetricsRequest decode(String msg)
throws DecodeException {
JsonObject json = Json.createReader(new StringReader(msg)).readObject();
MetricsRequest request = new MetricsRequest();
if (json.getString("cpu").equalsIgnoreCase("yes")) {
request.setCPU(true);
}
if (json.getString("memory").equalsIgnoreCase("yes")) {
request.setMEM(true);
}
return request;
}
/*
* This method will answer whether the given JSON message can be decoded into
* an object of type MetricsRequest. We will assume the client will always
* send a valid JSON text
*/
@Override
public boolean willDecode(String msg) {
return true;
}
}
The following is the Java POJO class that encapsulates the server CPU
and MEMORY metrics alert to the client:
Listing.3
/*
*
* Name: MetricsAlert
*
* Author: Bhaskar S
*
* Date: 02/16/2014
*
*/
package com.polarsparc.websockets;
/*
* Represents the server metrics alert POJO
*/
public class MetricsAlert {
private String user = null;
private String kernel = null;
private String total = null;
private String free = null;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getKernel() {
return kernel;
}
public void setKernel(String kernel) {
this.kernel = kernel;
}
public String getTotal() {
return total;
}
public void setTotal(String total) {
this.total = total;
}
public String getFree() {
return free;
}
public void setFree(String free) {
this.free = free;
}
}
The client web application will be expecting the metrics information
from the server as WebSocket messages in a JSON format. The server creates and populates an
object of type com.polarsparc.websockets.MetricsAlert .
How do we convert an object of type MetricsAlert
into a JSON format ???
Enter javax.websocket.Encoder . The Encoder class is an interface that allows one to
extend and implement a concrete class which the WebSocket
server can invoke on an outgoing application specific POJO
to convert it into the corresponding WebSocket
message which in our example will be a JSON
message.
The following is our custom Encoder class
that will convert an outgoing object of type MetricsAlert
to a JSON message:
Listing.4
/*
*
* Name: MetricsAlertEncoder
*
* Author: Bhaskar S
*
* Date: 02/16/2014
*
*/
package com.polarsparc.websockets;
import java.util.logging.Logger;
import java.util.logging.Level;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.websocket.Encoder;
import javax.websocket.EncodeException;
import javax.websocket.EndpointConfig;
/*
* A custom Encoder class that will take the a Java POJO of type MetricsAlert
* and convert it into a server websocket text message (which is in JSON format)
*/
public class MetricsAlertEncoder implements Encoder.Text<MetricsAlert> {
private final static Logger LOGGER = Logger.getLogger(MetricsAlertEncoder.class.getName());
public MetricsAlertEncoder() {
LOGGER.setLevel(Level.INFO);
}
@Override
public void init(EndpointConfig arg0) {
// Ignore - not used
}
@Override
public void destroy() {
// Ignore - not used
}
/*
* This method will encode the given object of type MetricsAlert into a text
* message (in JSON format).
*/
@Override
public String encode(MetricsAlert alert)
throws EncodeException {
JsonObjectBuilder builder = Json.createObjectBuilder();
// CPU metrics
if (alert.getUser() != null) {
builder.add("user", alert.getUser()).add("kernel", alert.getKernel());
}
// MEM metrics
if (alert.getTotal() != null) {
builder.add("total", alert.getTotal()).add("free", alert.getFree());
}
JsonObject json = builder.build();
LOGGER.info("MetricsAlertEncoder: encode() invoked, json = " + json.toString());
return json.toString();
}
}
The following is the custom ServerEndpointConfig.Configurator
Java code that will create a single instance of the server endpoint
class com.polarsparc.websockets.SimpleMonitor3
and reuse it for each of the client connections:
Listing.5
/*
*
* Name: SimpleMonitor3Configurator
*
* Author: Bhaskar S
*
* Date: 02/16/2014
*
*/
package com.polarsparc.websockets;
import java.util.logging.Logger;
import java.util.logging.Level;
import javax.websocket.server.ServerEndpointConfig.Configurator;
public class SimpleMonitor3Configurator extends Configurator {
private final static Logger LOGGER = Logger.getLogger(SimpleMonitor3Configurator.class.getName());
private final static SimpleMonitor3 monitor3 = new SimpleMonitor3();
public SimpleMonitor3Configurator() {
LOGGER.setLevel(Level.INFO);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getEndpointInstance(Class<T> endpointClass)
throws InstantiationException {
LOGGER.info("SimpleMonitor3Configurator: getEndpointInstance() invoked, endpointClass = " + endpointClass.getName());
return (T) monitor3;
}
}
The following is the server-side Java code that will push both the CPU and MEMORY metrics
at a regular interval of 5 seconds:
Listing.6
/*
*
* Name: SimpleMonitor3
*
* Author: Bhaskar S
*
* Date: 02/16/2014
*
*/
package com.polarsparc.websockets;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.List;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.io.FileReader;
import java.io.BufferedReader;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.Session;
import javax.websocket.OnOpen;
import javax.websocket.OnClose;
import javax.websocket.CloseReason;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
/*
* There will be only one instance of this class. Notice the use of the configurator, the encoder, and the
* decoder attributes
*/
@ServerEndpoint(value="/SimpleMonitor3",
configurator=com.polarsparc.websockets.SimpleMonitor3Configurator.class,
encoders=com.polarsparc.websockets.MetricsAlertEncoder.class,
decoders=com.polarsparc.websockets.MetricsRequestDecoder.class
)
public class SimpleMonitor3 {
private final static Logger LOGGER = Logger.getLogger(SimpleMonitor2.class.getName());
private List<Session> sessionList = new ArrayList<Session>();
private Thread thread;
public SimpleMonitor3() {
LOGGER.setLevel(Level.INFO);
thread = new Thread(new DispatcherThread());
thread.setName("DispatcherThread");
thread.start();
}
@OnOpen
public void onOpen(Session session) {
LOGGER.info("SimpleMonitor3: onOpen() invoked, session-id: " + session.getId());
synchronized(sessionList) {
sessionList.add(session);
}
}
@OnClose
public void onClose(Session session, CloseReason reason) {
LOGGER.info("SimpleMonitor3: onClose() invoked, session-id: " + session.getId() +
", reason = " + reason.toString());
closeSession(session);
}
@OnError
public void onError(Session session, Throwable t) throws Throwable {
LOGGER.severe("SimpleMonitor3: onError() invoked, Exception = " + t.getMessage());
}
@OnMessage
public void onMessage(String text, Session session) {
if (text != null) {
LOGGER.info("SimpleMonitor3: onMessage() invoked, text = " + text);
}
}
// ----- Private Method(s) -----
/*
* NOTE: This will only work on Linux platform.
*
* We will open the /proc/stat system file to read the line beginning with 'cpu'
* to get the first and third values which represent the user and kernel usage
*
*/
private void getCurrentCPUMetrics(MetricsAlert alert) {
int COUNT = 4;
String PROC_STAT_FILE = "/proc/stat";
String CPU = "cpu";
String DELIMITER = " ";
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(PROC_STAT_FILE));
for (String line; (line = br.readLine()) != null;) {
if (line.trim().startsWith(CPU)) {
StringTokenizer tokens = new StringTokenizer(line, DELIMITER);
if (tokens.countTokens() > COUNT) {
tokens.nextToken(); // Ignore cpu
String user = tokens.nextToken(); // User CPU usage
tokens.nextToken(); // Ignore cpu
String kernel = tokens.nextToken(); // Kernel CPU usage
alert.setUser(user);
alert.setKernel(kernel);
}
break;
}
}
br.close();
}
catch (Throwable ex) {
LOGGER.severe("SimpleMonitor: getCurrentCPUMetrics() invoked, Exception = " + ex.getMessage());
}
finally {
if (br != null) {
try {
br.close();
}
catch (Throwable e) {
// Ignore
}
}
}
}
/*
* NOTE: This will only work on Linux platform.
*
* We will open the /proc/meminfo system file to read the line beginning with 'MemTotal:'
* to get the second numeric value which the total memory. Similarly, read the line
* beginning with 'MemFree:' to get the second numeric value which the free memory.
*
*/
private void getCurrentMemoryMetrics(MetricsAlert alert) {
int COUNT = 2;
String PROC_MEMINFO_FILE = "/proc/meminfo";
String MEMTOTAL = "MemTotal:";
String MEMFREE = "MemFree:";
String DELIMITER = " ";
String total = null;
String free = null;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(PROC_MEMINFO_FILE));
for (String line; (line = br.readLine()) != null;) {
if (line.trim().startsWith(MEMTOTAL)) {
StringTokenizer tokens = new StringTokenizer(line, DELIMITER);
if (tokens.countTokens() > COUNT) {
tokens.nextToken(); // Ignore MemTotal:
total = tokens.nextToken(); // Memory total
}
}
else if (line.trim().startsWith(MEMFREE)) {
StringTokenizer tokens = new StringTokenizer(line, DELIMITER);
if (tokens.countTokens() > COUNT) {
tokens.nextToken(); // Ignore MemFree:
free = tokens.nextToken(); // Memory free
}
}
if (total != null && free != null) {
alert.setTotal(total);
alert.setFree(free);
break;
}
}
br.close();
}
catch (Throwable ex) {
LOGGER.severe("SimpleMonitor3: getCurrentMemoryMetrics() invoked, Exception = " + ex.getMessage());
}
finally {
if (br != null) {
try {
br.close();
}
catch (Throwable e) {
// Ignore
}
}
}
}
private void closeSession(Session s) {
synchronized(sessionList) {
try {
s.close();
}
catch (Throwable e) {
// Ignore
}
sessionList.remove(s);
}
}
// ----- Inner Class(es) -----
/*
* This background thread wakes-up every 5 secs, invokes the method
* getCurrentMemoryMetrics() and distributes the formatted metrics
* to all the WebSocket client session(s)
*/
private class DispatcherThread implements Runnable {
@Override
public void run() {
int SLEEP_INTERVAL = 5000; // 5 secs
MetricsAlert alert = new MetricsAlert();
for (;;) {
Session[] sessions = null;
synchronized(sessionList) {
sessions = sessionList.toArray(new Session[0]);
}
if (sessions != null && sessions.length > 0) {
getCurrentCPUMetrics(alert);
getCurrentMemoryMetrics(alert);
for (Session s : sessions) {
if (s.isOpen()) {
try {
s.getBasicRemote().sendObject(alert);
}
catch (Throwable e) {
LOGGER.severe("SimpleMonitor3: DispatcherThread: run() invoked, session-id: " +
s.getId() + ", Exception = " + e.getMessage());
closeSession(s);
}
}
else {
closeSession(s);
}
}
}
try {
Thread.sleep(SLEEP_INTERVAL);
}
catch (Throwable e) {
// Ignore
}
}
}
}
}
We will highlight the most important aspect from the source code in
Listing.6 above as follows:
Notice how the annotation @ServerEndpoint
defines the server endpoint using the value
attribute, the custom server endpoint configurator java class using
the configurator attribute, the
custom server object encoder java class using the encoder
attribute, and the custom server object decoder java class using the
decoder attribute.
The following is the client-side HTML/JavaScript
code that initializes and uses a WebSocket
connection:
Listing.7
<html>
<head>
<title>Simple Monitor 3</title>
</head>
<body>
<script type="text/javascript">
var webSocket = null;
function start() {
if (! webSocket) {
webSocket = new WebSocket('ws://localhost:8080/polarsparc/SimpleMonitor3');
webSocket.onopen = function(event) {
document.getElementById('status').innerHTML = "Connection open";
};
webSocket.onclose = function(event) {
document.getElementById('status').innerHTML = "Connection closed";
};
webSocket.onerror = function(event) {
document.getElementById('status').innerHTML = "Error: " + event.data;
};
webSocket.onmessage = function(event) {
var json = JSON.parse(event.data);
document.getElementById('user').innerHTML = json.user;
document.getElementById('kernel').innerHTML = json.kernel;
document.getElementById('total').innerHTML = json.total;
document.getElementById('free').innerHTML = json.free;
};
var json = {
'cpu': 'yes',
'memory': 'yes'
};
webSocket.send(JSON.stringify(json));
}
return false;
}
function stop() {
if (webSocket) {
webSocket.close();
webSocket = null;
}
}
</script>
<table style="border: 5px solid brown">
<tr>
<td style="border: 1px solid darkgreen"><div style="color:blue; font-size: 10px; font-family: verdana, arial, sans-serif">CPU (User)</div></td>
<td style="border: 1px solid darkgreen"><div id="user" style="color:purple; font-size: 10px; font-family: verdana, arial, sans-serif">-</div></td>
</tr>
<tr>
<td style="border: 1px solid darkgreen"><div style="color:blue; font-size: 10px; font-family: verdana, arial, sans-serif">CPU (Kernel)</div></td>
<td style="border: 1px solid darkgreen"><div id="kernel" style="color:purple; font-size: 10px; font-family: verdana, arial, sans-serif">-</div></td>
</tr>
<tr>
<td style="border: 1px solid darkgreen"><div style="color:red; font-size: 10px; font-family: verdana, arial, sans-serif">MEM (Total)</div></td>
<td style="border: 1px solid darkgreen"><div id="total" style="color:darkgreen; font-size: 10px; font-family: verdana, arial, sans-serif">-</div></td>
</tr>
<tr>
<td style="border: 1px solid darkgreen"><div style="color:red; font-size: 10px; font-family: verdana, arial, sans-serif">MEM (Free)</div></td>
<td style="border: 1px solid darkgreen"><div id="free" style="color:darkgreen; font-size: 10px; font-family: verdana, arial, sans-serif">-</div></td>
</tr>
<tr>
<td><input type="submit" value="Start Monitor" onclick="start()" /></td>
<td><input type="submit" value="Stop Monitor" onclick="stop()" /></td>
</tr>
<tr>
<td colspan="2" style="border: 1px solid brown"></td>
</tr>
<tr>
<td colspan="2" style="border: 1px solid brown"><div id="status" style="font-size: 10px; font-family: verdana, arial, sans-serif">Status</div></td>
</tr>
</table>
</body>
</html>
Now that we have the code for both the WebSocket
server and client, we need to perform some setup as follows:
Copy all the java classes related to the server SimpleMonitor3
under the directory $CATALINA_HOME/webapps/polarsparc/WEB-INF/classes/com/polarsparc/websockets
Copy the html file of the client SimpleMonitor3.html
under the directory $CATALINA_HOME/webapps/polarsparc
Start the Tomcat 8.x server
Now launch the Firefox browser and enter the following URL:
By clicking on the Start Monitor button and after few
seconds on the Stop Monitor button in
the browser window, we should see something like the following
Figure-1 below:
Figure-1
The following Figure-2 below shows the output from the Tomcat
logs:
Figure-2