Introduction to WebSockets :: Part - 3


Bhaskar S 02/15/2014


Introduction

In Part-1 we got our hands dirty with a simple WebSockets example.

For each new client connection, a separate new instance of the server endpoint class com.polarsparc.websockets.SimpleMonitor is created.

Is it possible to create a single instance of the server endpoint class and manage the client sessions within ???

In this part, we will do just that by creating a custom factory by extending the class javax.websocket.server.ServerEndpointConfig.Configurator.

Hands-on with WebSockets - 2

We will demonstrate the use of ServerEndpointConfig.Configurator to create a single instance of the server endpoint class for all of the client connections with another simple monitoring example that will display the Memory Total and Free space.

Note that this example will work only on the Linux platforms.

The following is the custom ServerEndpointConfig.Configurator Java code that will create a single instance of the server endpoint class com.polarsparc.websockets.SimpleMonitor2 and reuse it for each of the client connections:

Listing.1
/*
 * 
 * Name:   SimpleMonitor2Configurator
 * 
 * Author: Bhaskar S
 * 
 * Date:   02/15/2014
 * 
 */

package com.polarsparc.websockets;

import java.util.logging.Logger;
import java.util.logging.Level;

import javax.websocket.server.ServerEndpointConfig.Configurator;

public class SimpleMonitor2Configurator extends Configurator {
    private final static Logger LOGGER = Logger.getLogger(SimpleMonitor2Configurator.class.getName());
    
    private final static SimpleMonitor2 monitor2 = new SimpleMonitor2();
    
    public SimpleMonitor2Configurator() {
        LOGGER.setLevel(Level.INFO);
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass)
        throws InstantiationException {
        LOGGER.info("SimpleMonitor2Configurator: getEndpointInstance() invoked, endpointClass = " + endpointClass.getName());
        
        return (T) monitor2;
    }
}

The java class javax.websocket.server.ServerEndpointConfig.Configurator can be extended to create a custom configurator which can then be applied to a server endpoint to modify the default configuration behavior. In our example, we use it to create a single instance of the server endpoint class and associate it with each client that connects.

The method getEndpointInstance() is invoked by the container each time a new client connects to the server endpoint. This method returns an instance of the server endpoint that will handle all the interactions from the client.

The following is the server-side Java code that will push the Memory metrics at a regular interval of 5 seconds:

Listing.2
/*
 * 
 * Name:   SimpleMonitor2
 * 
 * Author: Bhaskar S
 * 
 * Date:   02/15/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 attribute
 */
@ServerEndpoint(value="/SimpleMonitor2", configurator=com.polarsparc.websockets.SimpleMonitor2Configurator.class)
public class SimpleMonitor2 {
    private final static Logger LOGGER = Logger.getLogger(SimpleMonitor2.class.getName());
    
    private List<Session> sessionList = new ArrayList<Session>();
    
    private Thread thread;
    
    public SimpleMonitor2() {
        LOGGER.setLevel(Level.INFO);
        
        thread = new Thread(new DispatcherThread()); 
        thread.setName("DispatcherThread");
        thread.start();
    }
    
    @OnOpen
    public void onOpen(Session session) {
        LOGGER.info("SimpleMonitor2: onOpen() invoked, session-id: " + session.getId());
        
        synchronized(sessionList) {
            sessionList.add(session);
        }
    }
    
    @OnClose
    public void onClose(Session session, CloseReason reason) {
        LOGGER.info("SimpleMonitor2: onClose() invoked, session-id: " + session.getId() +
            ", reason = " + reason.toString());
        
        closeSession(session);
    }
    
    @OnError
    public void onError(Session session, Throwable t) throws Throwable {
        LOGGER.severe("SimpleMonitor2: onError() invoked, Exception = " + t.getMessage());
    }
    
    @OnMessage
    public void onMessage(String text, Session session) {
        if (text != null) {
            LOGGER.info("SimpleMonitor2: onMessage() invoked, text = " + text);
        }
    }
    
    // ----- Private Method(s) -----
    
    /*
     * 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 String getCurrentMemoryMetrics() { int COUNT = 2; String PROC_MEMINFO_FILE = "/proc/meminfo"; String MEMTOTAL = "MemTotal:"; String MEMFREE = "MemFree:"; String DELIMITER = " "; String FORMAT = "Memory Total: %s (kB), Memory Free: %s (kB)"; String total = null; String free = null; String metrics = 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) { metrics = String.format(FORMAT, total, free); break; } } br.close(); } catch (Throwable ex) { LOGGER.severe("SimpleMonitor2: getCurrentMemoryMetrics() invoked, Exception = " + ex.getMessage()); } finally { if (br != null) { try { br.close(); } catch (Throwable e) { // Ignore } } } return metrics; } 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 for (;;) { Session[] sessions = null; synchronized(sessionList) { sessions = sessionList.toArray(new Session[0]); } if (sessions != null && sessions.length > 0) { String text = getCurrentMemoryMetrics(); for (Session s : sessions) { if (s.isOpen()) { try { s.getBasicRemote().sendText(text); } catch (Throwable e) { LOGGER.severe("SimpleMonitor2: 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 some of the aspects from the source code in Listing.2 above as follows:

The following is the client-side HTML/JavaScript code that initializes and uses a WebSocket connection:

Listing.3
<html>
    <head>
        <title>Simple Monitor 2</title>
    </head>
    
    <body>
        <script type="text/javascript">
            var webSocket = null;
            
            function start() {
                if (! webSocket) {
                    webSocket = new WebSocket('ws://localhost:8080/polarsparc/SimpleMonitor2');
    
                    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) {
                        document.getElementById('memory').innerHTML = event.data;
                    };
                
                    webSocket.send('start');
                }
                else {
                    alert("WebSocket already open .....");
                }
                
                return false;
            }
            
            function stop() {
                if (webSocket) {
                    webSocket.close();
                    webSocket = null;
                }
            }
        </script>
        
        <table style="border: 1px solid red">
            <tr>
                <td colspan="2" style="border: 1px solid blue"><div id="memory" style="color:blue; font-size: 10px; font-family: verdana, arial, sans-serif">Memory Metrics</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 blue"><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 two separate instances of the Firefox browser and enter the following URL in both the browser instances:

Clicking on the Start Monitor button in both the browser instances and we should see something like the following Figure-1 below:

SimpleMonitor2 Browsers
Figure-1

Clicking on the Stop Monitor button closes the WebSocket connection and stops the message stream.

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