Wednesday, January 15, 2014

Spring 4 WebSockets with SockJS, STOMP and Spring Security 3.2



What are WebSockets?

You may already know this, but I’m adding an excerpt from Wikipedia.
WebSocket is a protocol providing full-duplex communications channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011.
Modern browsers like Google Chrome and FireFox have been supporting WebSockets for some time now.

Spring 4 and WebSockets

Spring has grown to be a full fledged Enterprise solution. The latest offering of interest in Spring 4 is the implementation of the WebSocket specification as per JSR 356. Oracle has already published a tutorial on how to use the Java (Java EE 7.0) implementation of JSR 356. But I prefer to use the Spring 4 implementation so that it fits snugly in the Spring based ecosystem already in place.

This diagram depicts what I have understood on how WebSockets work. Traditional HTTP is a Request and Response based system. WebSockets is a full duplex communication channel. Its like opening a socket connection from the browser to the web server. Usually you need a thick client like a Java applet or an ActiveX plugin to create and open a socket connection from the browser to the web server. With WebSockets this ‘socket connection’ is now provided by the browser and understood by the server. Please do not confuse WebSockets with TCP connections. Differences between these two are well explained in these stackoverflow answers here and here.

learn-websockets-1-01
Http uses the http:// and https:// for regular and secured connections respectively from the browser to the web server. With WebSockets you use the ws:// and wss:// for regular and secured connections respectively.

Obviously you are not going to replace http:// with ws:// in the browser’s address bar as you explore using WebSockets as an alternative to the prevalent Ajax communication. Read more on this topic here.

Why should we bother about WebSockets? It’s just another transport mechanism …

Do watch this video which shows a quick comparison on the performance of using WebSockets as a transport for real-time web applications, as opposed to the traditional HTTP (long) polling.

YAWT (Yet Another WebSockets Tutorial) ??

There are many resources on this topic already on the web. Then why am I doing this? Well, I like to share what I learnt with others, in my own way.

Also the Spring example demonstrates WebSockets by using Spring Boot.
I wanted to 'retro-fit' my existing traditional Spring MVC application with WebSockets.
Hence this blog post.

Learning WebSockets

I am sharing my WebSocket learning experience with you through the example below. We will build a simple web application which will connect with a WebSocket server and echo back messages that we send to it. Sounds boring, but it’s the most basic example on how to go about WebSockets. In a later post I will share something more meaty.

What you will need

Before we get into WebSockets, you will need the following:
You don't need to create a fancy UI, but I decided to use some ready made UI components provided by Twitter Bootstrap. It creates responsive web pages which are suitable for rendering on mobile devices as well as traditional laptop screens. And all this without having to be a HTML5 and CSS expert!
Spring 4 WebSockets natively supports SockJS on the server side. So you will need the client side SockJS javascript framework. For this you will need to download -


Do all browsers support WebSockets?

Unfortunately, not all browsers support WebSockets. For example, IE10 was the first version of Internet Explorer browser to support it, while Google Chrome and FireFox have been supporting it since quite some time.

Many organizations still use older versions of Internet Explorer. Not everyone uses Google Chrome or FireFox. So in such cases you need a way to fallback on some other mechanisms like Ajax to communicate with the server. Also some networks are configured in such a way that proxies wont support WebSockets. In both cases you need a ‘fallback’ mechanism so that users can use WebSocket based applications.
You can test if your browser supports WebSockets by clicking here.

Project Setup

Using STS, create a simple Spring MVC Web Application. See the following screenshots to setup your project.
STS Project Setup 1: Select a wizard

STS Project Setup 1: Select a wizard

STS Project Setup 2: Select Template

STS Project Setup 2: Select Template

Note: We will change the Spring version 3.2.2 that the example MVC project uses to 4.0.0.RELEASE via the pom.xml file once the project is set up.

STS Project Setup 3: Project Specific Settings

STS Project Setup 3: Project Specific Settings


STS Project Setup 4: Package Explorer

STS Project Setup 4: Package Explorer

After you have setup your project, you should have a project layout similar to the above screenshot. The packages like com.blogspot.sunitkatkar.config , etc. will be added by you as you write code. The downloadable project file has the entire source code as per the screenshot below.

Modifying the pom.xml

We need to modify the example pom.xml file to have the proper Spring version in place. Let's look at the first section of the pom.xml file.
<!-- Properties -->
<properties>
  <!-- pom.xml -->
  <!-- Generic properties -->
  <java-version>1.7</java-version>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

  <!-- Spring framework -->
  <org.springframework-version>4.0.0.RELEASE</org.springframework-version>
  <org.springframework.security-version>3.2.0.RELEASE</org.springframework.security-version>

  <!-- Logging - Using Logback instead of Log4J -->
  <logback.version>1.0.13</logback.version>
  <org.slf4j-version>1.7.5</org.slf4j-version>

  <!-- Web -->
  <jsp.version>2.2</jsp.version>
  <jstl.version>1.2</jstl.version>
  <servlet.version>3.1.0</servlet.version>

  <!-- Test -->
  <junit.version>4.11</junit.version>

</properties>
<!-- /Properties -->

The rest of the pom.xml is a standard pom for Spring MVC projects. I have been using Log4J for logging, but for this project I decided to try the newer logging framework Logback by the creator of Log4J. More details here

Spring XML configuration

We are using XML based Spring configuration for this example. Here is the standard configuration via servlet-context.xml file.
    <?xml version="1.0" encoding="UTF-8"?>
<!-- servlet-context.xml -->
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/mvc 
                      http://www.springframework.org/schema/mvc/spring-mvc.xsd
                      http://www.springframework.org/schema/beans 
                      http://www.springframework.org/schema/beans/spring-beans.xsd
                      http://www.springframework.org/schema/context 
                      http://www.springframework.org/schema/context/spring-context.xsd">

  <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
  <context:component-scan base-package="com.blogspot.sunitkatkar" />

  <!-- Enables the Spring MVC @Controller programming model -->
  <annotation-driven />

  <!-- Handles HTTP GET requests for /resources/** by efficiently serving 
    up static resources in the ${webappRoot}/resources directory -->
  <resources mapping="/resources/**" location="/resources/" />

  <!-- Resolves views selected for rendering by @Controllers to .jsp resources 
    in the /WEB-INF/views directory -->
  <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
  </beans:bean>
</beans:beans>
    


Setting up Spring Security

Today the trend is to use Java based application and application security configuration for Spring based apps. However, I want to retro-fit my existing Spring MVC secured web application. So I continue to use XML based security configuration.

In our example, we want a simple login form. To achieve this, I have used the following security configuration via the application-security.xml file.

By using Spring Security XML configuration element <form-login> we can restrict access to secured web pages. Any attempt to access these pages will result in a login form being shown to the user. Once the user enters credentials and is authenticated then access is granted to the secured pages.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
  xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans  
                      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
                      http://www.springframework.org/schema/security 
                      http://www.springframework.org/schema/security/spring-security.xsd">


  <!-- Get a basic Spring Security provided form based login infra -->
  <http auto-config="true" use-expressions="true">
    <intercept-url pattern="/index" access="permitAll" />
    <intercept-url pattern="/index.jsp" access="permitAll" />
    <intercept-url pattern="/app/**" access="permitAll" />
    <intercept-url pattern="/simplemessages/**" access="permitAll" />
    <intercept-url pattern="/topic/**" access="permitAll" />
    <intercept-url pattern="/topic/simplemessages" access="permitAll" />
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/login" access="permitAll" />
    <intercept-url pattern="/loginPage" access="permitAll" />
    <!-- Requests to secured pages need to be authenticated and authorized -->
    <intercept-url pattern="/secured/*"
      access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
    <!-- Define the security form login and logout pages/urls -->
                
    <form-login login-processing-url="/login" login-page="/loginPage"
      username-parameter="username" password-parameter="password"
      default-target-url="/secured/basicWebsockets"
      authentication-failure-url="/loginPage?auth=fail" />
<logout invalidate-session="true" logout-url="/logout" logout-success-url="/logoutPage" /> </http> <!-- Define some example users and admins who have role based access to the application. In a real world scenario this would be linked with a user credentials database or a custom authentication provider. Some examples: 1) http://krams915.blogspot.in/2012/01/spring-security-31-implement_1244.html 2) http://krams915.blogspot.in/2010/12/spring-security-mvc-integration-using_26.html --> <authentication-manager> <authentication-provider> <user-service> <user name="john" password="doe" authorities="ROLE_USER" /> <user name="sunit" password="katkar" authorities="ROLE_USER" /> <user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN" /> </user-service> </authentication-provider> </authentication-manager> </beans:beans>
  • login-page: Mapping URL of the custom login page. If not defined, then Spring Security will create a default URL at '/spring_security_login' and render a default login form.
  • login-processing-url: Login form needs to be posted to this URL. If not defined then, it needs to be posted to default URL '/j_spring_security_check'.
  • username-parameter: Request parameter name which contains the username Default is 'j_username'.
  • password-parameter: Request parameter name which contains the password. Default is 'j_password'.
  • default-target-url: User will be redirected to this URL after successful login.
  • authentication-failure-url: If authentication failed, then user will be forwarded to this URL. Default is '/spring_security_login?login_error'. In this example we have set it to '/loginPage?auth=fail'. That means user will be redirected to the same login page and there we'll use request parameter 'auth=fail' as indicator to show the authentication failure message.


Adding Spring Security To The Spring MVC Web App

Now that the Spring security configuration is done via the application-security.xml file, it is time to combine it with Spring configuration file servlet-context.xml. This is done via the web.xmlb> file as shown
    <?xml version="1.0" encoding="UTF-8"?>
<!-- web.xml -->
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

  <!-- The definition of the Root Spring Container shared by all Servlets 
    and Filters -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
         /WEB-INF/spring/appServlet/servlet-context.xml
        
 /WEB-INF/application-security.xml
</param-value> </context-param> <!-- Creates the Spring Container shared by all Servlets and Filters --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Processes application requests --> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/spring/appServlet/servlet-context.xml </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- Spring standard form based login security -->
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  
<filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>


Spring MVC Controller

In this example, there is a LearnWebSocketsMVCController which receives all requests. We need to add methods to handle the requests for authentication as shown below.
/**
 * Copyright &copy; Sunit Katkar (sunitkatkar@gmail.com) http://sunitkatkar.blogspot.com
 */
package com.blogspot.sunitkatkar.controllers;

import java.security.Principal;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.blogspot.sunitkatkar.util.Util;

/**
 * {@link LearnWebSocketsMVCController} is for handling the user login and logout. Simple form based security is used.
 * Refer to <code>application-security.xml</code> and <code>web.xml</code> for details.
 * 
 * @author <a href="mailto:sunitkatkar@gmail.com">Sunit Katkar</a>
 * @since 1.0
 * @version 1.0.0.1
 */
@Controller
public class LearnWebSocketsMVCController {

    private static final Logger LOG = LoggerFactory.getLogger(LearnWebSocketsMVCController.class);

    /**
     * To handle the regular request to the application context. e.g. http://localhost:8080/learn-websockets-1
     * 
     * @param model
     * @param locale
     * @return
     */
    @RequestMapping("/")
    public String handleIndexPage(Model model, Locale locale) {
        LOG.info("Request for default / url processed at {}", Util.getSimpleDate());
        return "loginPage";
    }

    /**
     * Method is executed when there is a call to the <code>/logoutPage</code> url.
     * 
     * @return
     */
    
@RequestMapping(value = "/logoutPage", method = RequestMethod.GET)
public String logoutPage() { LOG.info("Request for /logoutPage url processed at {}", Util.getSimpleDate()); return "logoutPage"; } /** * Method is executed when there is a call to the <code>/loginPage</code> url. * On successful login, the user is re-directed to the <code>/secured/myPage</code> url. * * @return */
@RequestMapping(value = "/loginPage", method = RequestMethod.GET)
public String loginPage() { LOG.info("Request for /loginPage url processed at {}", Util.getSimpleDate()); return "loginPage"; } /** * Method gets executed when there are requests to the <code>/secured/basicWebsockets</code> url. * This url is called after a successful login. * * @param model * @param principal * @param locale * @return */
@RequestMapping("/secured/basicWebsockets")
public String basicWebsocketsPage(Model model, Principal principal, Locale locale) { // Get a simple human readable date and time String formattedDate = Util.getSimpleDate(locale); // Get the logged in user's name String userName = principal.getName(); // Set some sample messages to show on the landing 'basicWebsockets.jsp' page. model.addAttribute("username", userName); model.addAttribute("time", formattedDate); LOG.info("Request from user:{} for /secured/basicWebsockets url processed at time:{}", userName, formattedDate); return "secured/basicWebsockets"; } }


Creating Views Using JSP

I have used Twitter Bootstrap to get a quick yet responsive and visually elegant user interface. This example has four jsp files.
  • index.jsp - Has a simple redirect to the loginPage.jsp
  • loginPage.jsp - To show the login form
  • logoutPage.jsp - To show the 'session ended' message
  • basicWebsockets.jsp - The landing page after authentication. This also has the WebSockets user interface and client side code

index.jsp

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<c:redirect url="/loginPage"/>
    

loginPage.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c'%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<!--  Bootstrap related -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="<c:url value="/resources/scripts/bootstrap/css/bootstrap.min.css"/>">
<%-- <link href="<c:url value="/resources/css/login.css"/>" rel="stylesheet"> --%>
<title>Login :: Learn WebSockets</title>
</head>
<body>
  <div class="container">
    <div class="span12">
      <div id="heading" class="masthead">
        <h3 class="muted">Login</h3>
      </div>
      <div class="row">
        <div class="col-sm-6">
          <div class="panel panel-default">
            <div class="panel-body">
              <form action="${pageContext.request.contextPath}/login" method="post" 
                  class="form-signin" role="form">
                <div class="form-group">
                  <label for="username">User name:</label> 
                  <input type="text" name="username" id="username" class="form-control" 
                    placeholder="User name" required autofocus />
                </div>
                <div class="form-group">
                  <label for="password">Password:</label> <input type="password" name="password" id="password"
                    class="form-control" placeholder="Password" required>
                </div>
                <input name="submit" type="submit" value="Submit" class="btn btn-success">
              </form>
              <c:if test="${'fail' eq param.auth}">
                <p>
                <div class="alert alert-danger">Login Failed!!! Reason :
                  ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}</div>
              </c:if>
            </div>
          </div>
        </div>
        <!-- .panel-body -->
      </div>
      <!-- .panel -->
    </div>
    <!-- .span12 -->
  </div>
  <!-- .container -->
  <!-- .container -->
  <!-- Compiled and minified JQuery JavaScript (necessary for Bootstrap's JavaScript plugins). 
       Placed at the end of the document so the pages load faster -->
  <script src="<c:url value="/resources/scripts/jquery-1.10.2.min.js"/>"></script>
  <!-- Include all compiled plugins (below), or include individual files as needed -->
  <script src="<c:url value="/resources/scripts/bootstrap/js/bootstrap.min.js"/>"></script>
</body>
</html>
  

logoutPage.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c'%>
<!DOCTYPE html>
<!-- <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<!--  Bootstrap related -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="<c:url value="/resources/scripts/bootstrap/css/bootstrap.min.css"/>">
<title>Logout :: Learn WebSockets</title>
</head>
<body>
  <div class="container">
    <div class="span12">
      <div id="heading" class="masthead">
        <h3 class="muted">Logged Out</h3>
      </div>
      <div class="row">
        <div class="col-sm-12">
          <div class="panel panel-default">
            <div class="panel-body">
              Your session has ended. Click here 
              to <a href="${pageContext.request.contextPath}/secured/basicWebsockets">login</a>
              again.
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- Compiled and minified JQuery JavaScript (necessary for Bootstrap's JavaScript plugins). 
    Placed at the end of the document so the pages load faster -->
  <script src="<c:url value="/resources/scripts/jquery-1.10.2.min.js"/>"></script>
  <!-- Include all compiled plugins (below), or include individual files as needed -->
  <script src="<c:url value="/resources/scripts/bootstrap/js/bootstrap.min.js"/>"></script>
</body>
</html>
   

basicWebsockets.jsp

We will look at this page after we delve into server and client side code for WebSockets

Server side code for WebSockets

Before we look at the client side javascript required for WebSockets, let's look at the server side code. We need to make our 'legacy' MVC application aware of the new 'WebSockets' feature.

Configuring WebSockets


We have used XML for configuring the MVC app. Now this is the 'legacy' app which we will retro-fit with WebSockets. Note that you can use XML based WebSocket configuration, but we will use Java based configuration. You can mix and match XML and Java based configuration in a Spring MVC app. When starting new projects you can keep it clean by going either complete XML or complete Java based configuration.

Here is Java based configuration for 'retro-fitting' WebSockets in our MVC example via the WebSocketConfig class. The source code has self explanatory comments.
    
/**
 * Copyright &copy; 2013-2014 <a href="http://sunitkatkar.blogspot.com">Sunit's blog</a>
 */
package com.blogspot.sunitkatkar.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * Defines methods for configuring message handling with simple messaging
 * protocols (e.g. STOMP) from WebSocket clients. Typically used to customize
 * the configuration provided via the {@link EnableWebSocketMessageBroker}
 * annotation. <br/>
 * <br/>
 * WebSocketConfig is annotated with <code>@Configuration</code> to indicate
 * that it is a Spring configuration class. It is also annotated with
 
* <code>@EnableWebSocketMessageBroker</code>. As its name suggests,
 * <code>@EnableWebSocketMessageBroker</code> enables WebSocket message
 * handling, backed by a message broker. <br/>
* <br/> * Furthermore, this is also annotated with the <code>@EnableScheduling</code> * annotation. This enables Spring's scheduled task execution ability. * * @author <a href="mailto:sunitkatkar@gmail.com">Sunit Katkar</a> * @since 1.0 * @version 1.0.0.1 */ @Configuration
@EnableWebSocketMessageBroker
@EnableScheduling public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * Configure STOMP over WebSocket end-points. * * @see com.blogspot.sunitkatkar.WebSocketBroadcastController.controllers.WebsocketBroadcastController */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // The registerStompEndpoints() method registers the "/simplemessages" // endpoint, enabling SockJS fallback options so that alternative // messaging options may be used if WebSocket is not available. This // endpoint when prefixed with "/app", is the endpoint that the // WebSocketBroadcastController.processMessageFromClient() method is // mapped to handle. registry.addEndpoint("/simplemessages").withSockJS(); } /** * Configure message broker options. */ @Override public void configureMessageBroker(MessageBrokerRegistry config) { // The configureMessageBroker() method overrides the default method in // WebSocketMessageBrokerConfigurer to configure the message broker. It // starts by calling enableSimpleBroker() to enable a simple // memory-based message broker to carry the greeting messages back to // the client on destinations prefixed with "/topic/". It also // designates the "/app" prefix for messages that are bound for // @MessageMapping-annotated methods. config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } /** * * Configure the {@link org.springframework.messaging.MessageChannel} used * for incoming messages from WebSocket clients. By default the channel is * backed by a thread pool of size 1. It is recommended to customize thread * pool settings for production use. */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { } /** * Configure the {@link org.springframework.messaging.MessageChannel} used * for outgoing messages to WebSocket clients. By default the channel is * backed by a thread pool of size 1. It is recommended to customize thread * pool settings for production use. */ @Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.taskExecutor().corePoolSize(4).maxPoolSize(10); } }


Handling WebSocket [ws(s)://] Requests

Handling WebSocket requests is similar to handling regular http(s) requests in Spring based code. We create a regular Spring Controller WebSocketBroadcastController but use the messaging annotations (e.g. @MessageMapping) to handle requests and serve content. Take a look at the Spring docs for more details.

Here is the WebSocketBroadcastController class with self explanatory source code comments.
    
/**
 * Copyright &copy; Sunit Katkar (sunitkatkar@gmail.com) http://sunitkatkar.blogspot.com
 */
package com.blogspot.sunitkatkar.controllers;

import java.security.Principal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;

import com.blogspot.sunitkatkar.model.MessageBroadcast;
import com.blogspot.sunitkatkar.model.SimpleMessage;
import com.blogspot.sunitkatkar.util.Util;

/**
 * {@link WebSocketBroadcastController} is a regular Spring Controller as seen
 * in most Spring MVC applications. Its job is to receive {@link SimpleMessage}
 * message objects from the client, extract the <code>payload</code> (or
 * contents) of the message, prepend it with some simple text and finally
 * broadcast (or publish) the message to all clients who have subscribed to the
 * <code>/topic/simplemessages</code> message queue.
 * 
 * 
 * @author <a href="mailto:sunitkatkar@gmail.com">Sunit Katkar</a>
 * @since 1.0
 * @version 1.0.0.1
 */

@Controller
public class WebSocketBroadcastController {

    private static final Logger LOG = LoggerFactory
            .getLogger(WebSocketBroadcastController.class);

    /**
     * Method to handle the requests sent to this controller at
     * <code>/simplemessages</code> <br/>
     * <br/>
     * <b>Explanation:</b> The <code>@MessageMapping</code> annotation ensures
     * that if a message is sent to destination <code>/simplemessages</code>,
     * then the
     * {@link WebSocketBroadcastController#processMessageFromClient(SimpleMessage)}
     * method is called. <br/>
     * <br/>
     * The message payload is bound to the {@link SimpleMessage} object. For
     * simplicity, this method simulates a 3 second delay before sending back
     * the message as a {@link MessageBroadcast} object. The return value is
     * broadcast to all subscribers to
     * <code>/topic/simplemessagesresponse</code> as specified in the
     * <code>@SendTo</code> annotation. <br/>
     * <br/>
     * <b>Note:</b> The 3 second delay demonstrates that after the server
     * receives a message from the client, the client is free to continue any
     * other processing while the server takes its own time to act on the
     * received message.
     * 
     * @param message
     * @param principal
     * @param locale
     * @return
     * @throws Exception
     */
   
@MessageMapping("/simplemessages")
@SendTo("/topic/simplemessagesresponse")
public MessageBroadcast processMessageFromClient(SimpleMessage message, Principal principal) throws Exception { // Simulate a delay of 3 seconds Thread.sleep(3000); LOG.info("Sending server side response '{}' for user: {}", message, principal.getName()); return new MessageBroadcast("Server response: Did you send &lt;b&gt;'" + message.getMessage() + "'&lt;/b&gt;? (Server Response at: " + Util.getSimpleDate() + ")"); } /** * If there are any exceptions thrown by any of the messaging infrastructure * then they can be sent to the end user on the <code>/queue/errors</code> * destination. * * @param exception * @return */ @MessageExceptionHandler @SendToUser("/queue/errors") public String handleException(Throwable exception) { return exception.getMessage(); } }


Client Side WebSockets Code - basickWebsockets.jsp

On the client side we will use the SockJS javascript library to create a WebSocket connection and then communicate with the server. The following jsp page basicWebsockets.jsp has the HTML and Javascript together for easier reading. Note that I have used Twitter Bootstrap and hence there is some javascript code to handle the UI interactions. You can create a bare bones HTML page too.

The code has self explanatory comments.
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c'%>
<!-- basicWebsockets.jsp -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
<!--  Bootstrap related -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Latest compiled and minified Bootstrap CSS -->
<link rel="stylesheet" href="<c:url value="/resources/scripts/bootstrap/css/bootstrap.min.css"/>">
<title>Learn WebSockets 1</title>
<!-- WebSocket related javascript includes -->
<script src="<c:url value="/resources/scripts/sockjs-0.3.4.min.js"/>"></script>
<script src="<c:url value="/resources/scripts/stomp.js"/>"></script>
</head>
<body>
  <noscript>
    <h2 style="color: #ff0000">Seems your browser doesn't support Javascript! WebSocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2>
  </noscript>
  <c:url var="imageUrl" value="/resources/images/user01.png" />
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <!-- WebSocket related Twitter Bootstrap 3.0 based UI elements -->
        <div id="heading" class="masthead">
          <div class="pull-right">
            Logged In: <strong>${username}</strong> | ${time } | <a href="${pageContext.request.contextPath}/logout">Logout&nbsp;<span class="glyphicon glyphicon-remove"></span></a>
          </div>
          <h3 class="muted">
            <img src="${imageUrl}" />Welcome to Learning WebSockets 1
          </h3>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-6">
        <p>&nbsp;</p>
        <!-- Connect and Disconnect buttons to establish/terminate a connection to the websocket service -->
        <div class="panel">
          <button id="connect" class="btn btn-success btn-sm">Connect</button>
          <button id="disconnect" class="btn btn-danger btn-sm">Disconnect</button>
        </div>
        <p />
        <div class="panel panel-default">
          <div class="panel-heading">Send Messages To WebSocket Server</div>
          <div class="panel-body" id="conversationDiv">
            <div class="input-group">
              <input type="text" class="form-control" id="txtSendMessage" placeholder="Enter message"> <span class="input-group-btn">
                <button id="sendMessage" class="btn btn-primary">
                  <span class="glyphicon glyphicon-share-alt"></span>&nbsp;Send
                </button>
              </span>
            </div>
            <!-- Error alert -->
            <div class="alert alert-danger alert-dismissable" id="formAlert">
              <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
              <strong>Error!</strong> Message cannot be blank.
            </div>
            <!-- /Error alert -->
            <!-- Info alert -->
            <div class="alert alert-info alert-dismissable" id="formInfoAlert">
              <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
              <strong>Message Sent!</strong> <br />Your message has been sent to the server. You can continue to do other actions. Server response will be shown when it arrives.
            </div>
            <!-- /Info alert -->
            <!-- .input-group -->
          </div>
          <!-- .panel-body -->
          <div class="panel-body" id="response"></div>
          <!-- Div to show the server responses -->
        </div>
        <!-- .panel -->
      </div>
    </div>
  </div>
  <!-- .container -->
  <!-- External Javascripts -->
  <!-- Latest compiled and minified JQuery JavaScript (necessary for Bootstrap's JavaScript plugins). Placed at the end of the document so the pages load faster -->
  <script src="<c:url value="/resources/scripts/jquery-1.10.2.min.js"/>"></script>
  <!-- Include all compiled plugins (below), or include individual files as needed -->
  <script src="<c:url value="/resources/scripts/bootstrap/js/bootstrap.min.js"/>"></script>
  <!-- Knockout data-binding MVVM library. More at http://knockoutjs.com/  -->
  <script src="<c:url value="/resources/scripts/knockout-3.0.0.js"/>"></script>
  <%-- URL for the STOMP end point for registering a stomp client--%>
  <c:url value="/simplemessages" var="socketDest" />
  <script type="text/javascript">
            /***********************************************/
            /* PLEASE READ UP ON STOMP AND SOCKJS          */
            /* 1) http://jmesnil.net/stomp-websocket/doc/  */
            /* 2) https://github.com/sockjs/sockjs-client  */
            /***********************************************/

            //Declare a stompclient which will connect to the server
            var stompClient = null;

            /************************************************************************** 
            /*  JQUERY WAY OF BEING UNOBTRUSIVE AND ADDING EVENT HANDLERS TO WIDGETS, 
            /*  THUS KEEPING HTML AND JAVASCRIPT SEPARATE 
            /*************************************************************************/
            // Runs this code only when the DOM (all elements) are ready
            $(document).ready(function() {
                // On page load the text input field 'MESSAGE', 'DISCONNECT' and 'SEND' buttons 
                // should all be disabled as user has not clickedd 'CONNECT' button yet.
                $("#disconnect").prop('disabled', true);
                $("#txtSendMessage").prop('disabled', true);
                $("#sendMessage").prop('disabled', true);

                //Also all text in server message should be empty
                $("#txtSendMessage").val("");
                //Remove any server responses from previous interactions
                $("#response").empty();
                //Hide the validation and info alerts on page load
                $(".alert").hide();
                // Event handler: Connect button
                $("#connect").on("click", function(e) {
                    // If alert is visible, hide it
                    $("#formAlert").slideUp(400);
                    connect();
                });

                // Event handler: Disconnect button 
                $("#disconnect").on("click", function(e) {
                    // If alert is visible, hide it
                    $("#formAlert").slideUp(400);
                    disconnect();
                });
                // Event handler: X button on top right of info alert.
                // Clicking the X button on top right will dismiss it from the screen and hide it
                $(".alert").find(".close").on("click", function(e) {
                    // Find all elements with the "alert" class, get all descendant elements 
                    // with the class "close", and bind a "click" event handler

                    // Don't allow the click to bubble up the DOM
                    e.stopPropagation();

                    // Don't let any default functionality occur (in case it's a link)
                    e.preventDefault();

                    // Hide this specific Alert
                    $(this).closest(".alert").slideUp(400);

                    // Focus on the Send Message textfield
                    $("#txtSendMessage").select();
                    $("#txtSendMessage").focus();
                });

                // Event handler: Send button
                $("#sendMessage").on("click", function(e) {

                    // Find the input text element for the server message
                    var messageForServer = $("#txtSendMessage").val();

                    if (messageForServer === "") {

                        // If message is empty prevent submission and show the alert
                        e.preventDefault();
                        $("#formAlert").slideDown(400);

                    } else {

                        // Message is not empty so send to server
                        $("#formAlert").slideUp(400);

                        // Show a please wait alert
                        $("#formInfoAlert").slideDown(400);

                        // Send message to server. The message for the server must 
                        // be in JSON format. 
                        // Also refer SimpleMessage.java POJO.
                        sendMessageToServer(messageForServer);
                    }
                });
            });

            //Function sets the state of the Connect and Disconnect buttons
            function setConnected(connected) {
                //Since we are using bootstrap, this is how you disable buttons 
                // and input widgets
                $("#connect").prop('disabled', connected);
                $("#disconnect").prop('disabled', !connected);
                $("#sendMessage").prop('disabled', !connected);
                $("#txtSendMessage").prop('disabled', !connected)
            }

            // Function to connect the web client to the websocket server
            function connect() {
                //Remove any server responses from previous interactions
                $("#response").empty();
                //Also all text in server message input field should be empty
                $("#txtSendMessage").val("");
                $("#txtSendMessage").focus();
                $("#txtSendMessage").select();
                // Register a websocket endpoint using SockJS and stomp.js
                // Refer to Java class Refer to Java class 
                // WebSocketConfig.java#registerStompEndpoints(StompEndpointRegistry registry)
                var socket = new SockJS('${socketDest}');
                stompClient = Stomp.over(socket);
                // Now that a stomp client is defined, its time to open a connection
                // 1) First we connect to the websocket server
                // Notice that we dont pass in username and password as Spring Security
                // has already provided the server with the Principal object containing user credentials
                // 2) The last argument is a callback function which is called when connection succeeds
                stompClient.connect('', '', function(frame) {
                    //set the connect and disconnect button state. (disable connect button)
                    setConnected(true);
                    // In production code remove the next line
                    console.log("Connected: " + frame);
                    //Lets show a connection success message
                    showServerBroadcast("Connection established: " + frame, false);
                    // Now subscribe to a topic of interest.
                    // Refer to Java class WebsocketBroadcastController.java#processMessageFromClient(SimpleMessage message)
                    // WebsocketBroadcastController is waiting for connections. Upon successful connection, it subscribes to
                    // the "/topic/simplemessagesresponse" destination where the server will echo the messages.
                    // When a broadcast message is received by the client on that destination, it will be shown by appending
                    // a paragraph to the DOM in the client browser.
                    stompClient.subscribe("/topic/simplemessagesresponse", function(servermessage) {//Callback when server responds
                        showServerBroadcast(JSON.parse(servermessage.body).messageContent, false);
                        //Server responded so hide the info alert
                        $("#formInfoAlert").slideUp(400);
                        //Also all text in server message input field should be empty
                        $("#txtSendMessage").val("");
                        $("#txtSendMessage").focus();
                        $("#txtSendMessage").select();
                    });
                });
            }

            // Function to disconnect the web client to the websocket server
            function disconnect() {
                //First hide any alerts
                $("#formAlert").slideUp(400);
                $("#formInfoAlert").slideUp(400);
                // Disconnect the stompClient
                stompClient.disconnect();
                // Set the connect and disconnect button states
                setConnected(false);
                // In production remove the next line
                console.log("Disconnected");
                showServerBroadcast("WebSocket connection is now terminated!", true);
            }

            // Function to send the message typed in by the user to the "/app/simplemessages" destination on the server.
            // WebsocketBroadcastController will receive this message and broadcast the results after 
            // an artificially introduced delay.
            function sendMessageToServer(messageForServer) {
                //Show on the browser page that a message is being sent
                showServerBroadcast("Your message '" + messageForServer + "' is being sent to the server.", true);
                // The message for the server must be in JSON format. Also refer SimpleMessage.java POJO.
                stompClient.send("/app/simplemessages", {}, JSON.stringify({
                    'message' : messageForServer
                }));
            }

            /**
             * Function to show the server response on the web page
             * @param servermessage - text to be shown on webpage
             * @param localMessage - boolean, if true then it means its a 
             *                       client side javascript generated message.
             */
            function showServerBroadcast(servermessage, localMessage) {
                // Server surrounds the user sent message with <b> and </b> 
                // as &ltb&gt;message%lt;/b&gt;
                // Use Jquery to decode the HTML and show it as <b>message</b>
                var decoded = $("<div/>").html(servermessage).text();

                var tmp = "";
                var serverResponse = document.getElementById("response");
                var p = document.createElement('p');
                p.style.wordWrap = 'break-word';

                if (localMessage) {
                    p.style.color = '#006600';
                    tmp = "<span class='glyphicon glyphicon-dashboard'></span> " + decoded + " (Browser time:" + getCurrentDateTime() + ")";
                } else {
                    p.style.color = '#8A0808';
                    tmp = "<span class='glyphicon glyphicon-arrow-right'></span> " + decoded;
                }
                //Assigning the decoded HTML to the <p> element
                p.innerHTML = tmp;
                serverResponse.appendChild(p);
            }

            /**
             * Utility function to return the date time in simple format
             * like Tue Jan 07 2014 @ 11:47:24 AM
             */
            function getCurrentDateTime() {
                var date = new Date();
                var n = date.toDateString();
                var time = date.toLocaleTimeString();
                return n + " @ " + time;
            }
        </script>
</body>
</html>    
    


The Final Result

Download the project (link at the end of this post) and run it.

You can access the WebSockets app in the browser at the url http://localhost:8080/learn-websockets-1. Happy WebSocket-ing :)








Download the app from this location

10 comments:

NIKHIL SAGAR said...

great post...really helped.
waiting for the release to send message to a particular user.

echarcha said...

Thanks Nikhil. Refer to the Spring Stock Portfolio example. It has what you are are looking for - 'sending message to a specific user'.

Kencrisjohn de Guzman said...

What a great post. Very helpful. I just have one question. Do this technology(Spring 4 websockets with Stomp and Sockjs) are efficient for creating a multiplayer games?
Thanks for this. :D

Kencrisjohn de Guzman said...
This comment has been removed by the author.
echarcha said...

Hello Kencrisjohn de Guzman,

Thank you for appreciating my post.

Yes you can use websockets for creating multi-player games. With long polling you dont really have a full duplex communication channel and this can be problematic for games which require high level of two way communications. Websockets are full duplex, i.e. its similar to a dedicated native socket connection between your browser and server. This will surely help in rapid communication between server and browser for a communication intensive application like games.

Erdinç Çiftçi said...

Thanks for the post. As far as I understand, you permit WebSocket URLs, so they are not secured. Why is this so? For example, with Ajax requests, we can add csrf header and we can communicate securely. Can we do something similar with SockJS or Stomp?

Harshal Ladhe said...

I want help in doing this example without security module. Can anyone help me or give me a link where it is located???

Harshal Ladhe said...

I want help in doing this example without security module. Can anyone help me or give me a link where it is located???

echarcha said...

To: Harshad Ladhe

Just dont use the security related configuration in the XML. Use Spring Source Tool Suite (STS) to create a plain Spring MVC project. It will not have any security configurations in the XML. Then its easy to follow the techniques as in the blog post to create what you want.

DilbertWannaBe said...

This was extremely helpful and very well done. So often the examples you find in blog posts -- when learning something new -- are too simplistic and you cannot easily adapt them to "real word" use. This post hits the sweet spot. Thanks so much for posting it!