Introduction to WebSockets :: Part - 1


Bhaskar S 02/08/2014


Overview

WebSockets were introduced in HTML5 to provide a real-time, bi-directional, full-duplex (using a single TCP socket connection) communication between a client (HTML5 compliant browser) and a server (HTML5 compliant web server).

Using WebSockets, one can build scalable and interactive real-time web applications with ease without resorting to any type of polling mechanism.

WebSockets consists of two aspects - the WebSocket Application Programming Interface (API) that has been defined and standardized by the World Wide Web Consortium (W3C) in the specification WebSockets and the WebSocket Network Protocol that has been defined and standardized by the Internet Engineering Task Force (IETF) in the specification RFC 6455.

WebSocket API uses an event-driven programming model with well-defined callbacks both on the client as well as the server.

WebSocket Protocol is an application level protocol that rides on top of the streams-based TCP network protocol.

Installation and Setup

The best way to try and learn anything is to roll-up our sleeves and get our hands dirty.

We will demostrate the capabilities of WebSocket in a Ubuntu based Linux environment.

For the server-side, we will need an HTML5 compliant web-server that supports WebSockets.

For the client-side, most of the mordern web-browsers such as Mozilla Firefox or Google Chrome would suffice. We will use Firefox for our demonstration.

Download the Apache Tomcat 8.x Core from the following site:

Unzip the just downloaded Tomcat 8.x Core in a directory of your choice. We will refer to the root of the Tomcat 8.x Core directory using the environment variable CATALINA_HOME.

To start the Tomcat 8.x server, issue the following command:

$CATALINA_HOME/bin/startup.sh

Now launch Firefox and enter the following URL:

We should see something like the following Figure-1 in the browser:

Localhost Web
Figure-1

To shut the Tomcat 8.x server, issue the following command:

$CATALINA_HOME/bin/shutdown.sh

Hands-on with WebSockets

We will demonstrate the use of WebSockets with a simple monitoring example that will display the CPU usage in the User and Kernal space.

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

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

Listing.1
/*
 * 
 * Name:   SimpleMonitor
 * 
 * Author: Bhaskar S
 * 
 * Date:   02/08/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.OnError;
import javax.websocket.OnMessage;

@ServerEndpoint("/SimpleMonitor")
public class SimpleMonitor {
    private final static Logger LOGGER = Logger.getLogger(SimpleMonitor.class.getName());
    
    private static List<Session> sessionList = new ArrayList<Session>();
    
    private static Thread thread;
    
    private Session session;
    
    public SimpleMonitor() {
        LOGGER.setLevel(Level.INFO);
    }
    
    @OnOpen
    public void onOpen(Session session) {
        LOGGER.info("SimpleMonitor: onOpen() invoked");
        
        this.session = session;
        
        synchronized(sessionList) {
            sessionList.add(session);
            
            if (thread == null) {
                thread = new Thread(new DispatcherThread()); 
                thread.setName("DispatcherThread");
                thread.start();
            }
        }
    }
    
    @OnClose
    public void onClose() {
        LOGGER.info("SimpleMonitor: onClose() invoked");
        
        closeSession(this.session);
    }
    
    @OnError
    public void onError(Throwable t) throws Throwable {
        LOGGER.severe("SimpleMonitor: onError() invoked, Exception = " + t.getMessage());
    }
    
    @OnMessage
    public void onMessage(String text) {
        if (text != null) {
            LOGGER.info("SimpleMonitor: 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 String getCurrentCPUMetrics() {
        int COUNT = 4;
        
        String PROC_STAT_FILE = "/proc/stat";
        String CPU = "cpu";
        String DELIMITER = " ";
        String FORMAT = "User Level: %s, Kernel Usage: %s";
        
        String metrics = null;
        
        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 userCPU = tokens.nextToken(); // User CPU usage
                        tokens.nextToken(); // Ignore cpu
                        String kernCPU = tokens.nextToken(); // Kernel CPU usage
                        metrics = String.format(FORMAT, userCPU, kernCPU);
                    }
                    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
                }
            }
        }
        
        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
     * getCurrentCPUMetrics() 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 = getCurrentCPUMetrics();
                    
                    for (Session s : sessions) {
                        if (s.isOpen()) {
                            try {
                                s.getBasicRemote().sendText(text);
                            }
                            catch (Throwable e) {
                                LOGGER.severe("SimpleMonitor: DispatcherThread: run() invoked, Exception = " + e.getMessage());
                                
                                closeSession(s);
                            }
                        }
                        else {
                            closeSession(s);
                        }
                    }
                }
                
                try {
                    Thread.sleep(SLEEP_INTERVAL);
                }
                catch (Throwable e) {
                    // Ignore
                }
            }
        }
    }
}

Some aspects of the source code in Listing.1 above needs a little explaining so we can get our head around the server-side of WebSockets.

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

Listing.2
<html>
    <head>
        <title>Simple Monitor</title>
    </head>
    
    <body>
        <script type="text/javascript">
            var webSocket = null;
            
            function start() {
                webSocket = new WebSocket('ws://localhost:8080/polarsparc/SimpleMonitor');
    
                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('cpu').innerHTML = event.data;
                };
                
                webSocket.send('start');
                
                return false;
            }
            
            function stop() {
                if (webSocket) {
                    webSocket.close();
                }
            }
        </script>
        
        <table style="border: 1px solid red">
            <tr>
                <td colspan="2" style="border: 1px solid blue"><div id="cpu" style="color:blue; font-size: 10px; font-family: verdana, arial, sans-serif">CPU 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>

Some aspects of the HTML code in Listing.2 above needs a little explaining so we can understand the client-side of WebSockets.

Now that we have the code for both the WebSocket server and client, we need to perform some setup as follows:

Now launch Firefox and enter the following URL:

We should see something like the following Figure-2 in the browser:

SimpleMonitor Open
Figure-2

Clicking on the Start Monitor button, we should see something like the following Figure-3 in the browser:

SimpleMonitor Stream
Figure-3

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