Introduction to WebSockets :: Part - 4


Bhaskar S 02/16/2014


Introduction

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.

Setup

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 POJOs.

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:

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:

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:

SimpleMonitor3 Browser
Figure-1

The following Figure-2 below shows the output from the Tomcat logs:

Tomcat Logs
Figure-2

References

Introduction to WebSockets :: Part - 1

Introduction to WebSockets :: Part - 2

Introduction to WebSockets :: Part - 3