STOMPing

Recently I was playing with the STOMP protocol. Spring framework provides a very nice integration with the STOMP protocol. STOMP simply means Simple Text Oriented Messaging Protocol. Whoever wants to look into the documentation of Spring+Stomp integration can follow this Spring+Stomp.

Here, my intention is not to explain the STOMP, but want to show how beautiful it is ! On the contrary of the traditional synchronous (request response)  messaging , it is completely asynchronous. For example the Stock applications. They give you the real time updates of the stock values.Another example can be a chat example.

How I played with STOMP ?

My aim was to make a simple message oriented architecture where various clients (Web browser, Java client) can communicate asynchronously. As I already told that Spring provides a nice integration with STOMP, I chose the Spring framework and set up a very simple (without exaggeration) Spring project. If you want to jump to the project directly then here is the git App.

I did use the spring configuration in Java as it is much more convenient than the traditional xml configuration. Here I would like to discuss only the salient points that I discovered while trying to understand the topic.

    • The Message Broker configuration class
package com.anupam.app.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;

@Configuration
@EnableWebSocketMessageBroker
public class SpringStompConfig extends AbstractWebSocketMessageBrokerConfigurer {
	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		config.enableSimpleBroker("<span style="color: #ff9900;"><strong>/topic</strong></span>");
		config.setApplicationDestinationPrefixes("<span style="color: #4fad0c;"><strong>/app</strong></span>");
	}

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("<strong>/websocket</strong>").withSockJS();
		registry.addEndpoint("/websocket")
			.setHandshakeHandler(new DefaultHandshakeHandler(new TomcatRequestUpgradeStrategy()))
				.setAllowedOrigins("*");
	}

}

Points to be noted:

  1.  In the second method,
registerStompEndpoints(StompEndpointRegistry registry)

I have declared the Endpoint as “/websocket“. It means that all STOMP request must be sent to this endpoint. After declaring the endpoint, I have added a special line of code, otherwise the Tomcat server complains to upgrade it to Websocket support.

      2. In the first method,

configureMessageBroker(MessageBrokerRegistry config)

I have declared a simple in-memory message broker with the the destination prefix “/topic“. It means we can subscribe to messages in the destinations for example “/topic/xyz” etc.

Then in the line below, I have added a prefix named as “/app” for the destinations (@MessageMapping) on which we will publish messages. Do please look at the message controller class StompController  .

        @MessageMapping("/ping")
	@SendTo("/topic/ping")
	public String ping(String message) throws Exception {
		System.out.println(message);
		return message;
	}

Look at the @MessageMapping annotation as shown above. So, to publish/send a message to the “/ping” destination, we must add the “/app” prefix to it. So, from client applications we will be publishing message on this destination “/app/ping“.

And to subscribe to message we have to listen on the topic “/topic/ping“. Please check the @SendTo annotation in the above piece of code. You can put any destination name but it must have a prefix “/topic“. It’s because we have defined

config.enableSimpleBroker("/topic");

in the message broker configuration.

    • The ServletInitializer

This ServletInitializer class is same as configuring servlet-mapping in the web.xml. As I am using Java config, I have to map the urls for our websocket message broker.

package com.anupam.app.config;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class ServletInitializer implements WebApplicationInitializer {

	public void onStartup(ServletContext container) throws ServletException {
		// Context
		AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
		ctx.register(SpringAppConfig.class, SpringStompConfig.class);

		// Manage the life cycle of the root application context
		container.addListener(new ContextLoaderListener(ctx));

		// Register and map the dispatcher servlet.
		ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(ctx));
		dispatcher.setLoadOnStartup(1);
		dispatcher.addMapping("/rest/*");
		dispatcher.addMapping("<strong>/ws/*</strong>");
	}

}

Here I have mapped the url pattern as “/ws/*” for all the STOMP requests. Please do not forget to do this.

      • Writing a simple web client-AngularJS

My intention was to write a browser client containing a simple textbox and a send button so that one can send message to backend. The back end replies with the same message sent and shows up in a small text area in the browser.

<html lang="en">

<head>
<!-- Bootstrap-->
	<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- Angular JS -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
 <!-- Web Socket -->
 <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.1/sockjs.js"></script>
 <!-- App -->
 <script src="app/scripts/app.js"></script>
</head>

<body ng-app="stompapp" ng-controller="MyController">
<div class="panel panel-default" style="width: 400px">
<div class="panel-heading">STOMP Testing</div>
<div class="panel-body">
<textarea class="form-control" rows="10" ng-model="info.msgoutput"></textarea>


<input type="text" class="form-control" ng-model="info.msginput">


<button ng-click="onStomp()" class="btn btn-primary">Stomp</button>
</div>
</div>
</body>

</html>

Do please import the stomp.js and sockjs.js properly. And of course do not forget to include your own app.js. Here goes my app.js.

var app = angular.module('stompapp', []);
app.controller('MyController', function($scope) {
	var client = null;

	$scope.info = {
		msginput : &amp;amp;amp;amp;quot;&amp;amp;amp;amp;quot;,
		msgoutput : &amp;amp;amp;amp;quot;&amp;amp;amp;amp;quot;
	};

	// Connect to Websocket.
	connectStomp();

	function connectStomp() {
		var socket = new SockJS('/springjavaconfig/ws/websocket');
		client = Stomp.over(socket);
		client.connect({}, function(frame) {
			console.log('Connected: ' + frame);

			// Subscribe to messages published on this destination.
			client.subscribe('<span style="color: #ff9900;"><strong>/topic/ping</strong></span>', function(result) {
				var data = result.body;

				$scope.info.msgoutput = data;
				$scope.$apply();
			});
		});
	}
	;

	// Send a message.
	$scope.onStomp = function() {
		client.send('<span style="color: #339966;"><strong>/app/ping</strong></span>', {}, $scope.info.msginput);
	};

});

This is a very simple angularjs controller script. Please note the following things in the script.

  1. The websocket url.
var socket = new SockJS('/springjavaconfig/ws/websocket');

Do not forget the url mapping we added “/ws/*” in the ServletInitializer class and the websocket endpoint we have configured in SpringStompConfig  as “/websocket“.

      2. Subscription to a message destination

client.subscribe('/topic/ping', function(result) 
                        {
                             var data = result.body;
                             $scope.info.msgoutput = data;
			     $scope.$apply();
			}
                );

I have subscribed to the destination “/topic/ping“. If you look carefully in the message controller StompController there is an annotation @SendTo with destination “/topic/ping“.

        @MessageMapping("/ping")
	@SendTo("/topic/ping")
	public String ping(String message) throws Exception {
		System.out.println(message);
		return message;
	}

And also in the Message broker configuration SpringStompConfig there is a  piece of code as shown below

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
		config.enableSimpleBroker("/topic");
		config.setApplicationDestinationPrefixes("/app");
}

The line

config.enableSimpleBroker("/topic");

means that all subscription destination must be prefixed with “/topic“. So in the javascript code I am setting my subscription destination as “/topic/ping“.

           3. Sending a message.

I am sending a simple message as shown below:

// Send a message.
$scope.onStomp = function() {
	client.send('/app/ping', {}, $scope.info.msginput);
};

Here, if you look carefully, I am sending a message to the destination “/app/ping“. If you look carefully in the message controller StompController ,

        @MessageMapping("/ping")
	@SendTo("/topic/ping")
	public String ping(String message) throws Exception {
		System.out.println(message);
		return message;
	}

there is a mapping for the destination “/ping“. And also in the Message broker configuration SpringStompConfig,

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
		config.enableSimpleBroker("/topic");
		config.setApplicationDestinationPrefixes("/app");
}

I have configured that all messages sent to the websocket must be prefixed with “/app“. So the final destination becomes “/app/ping“.

    • Java client

Now in the java client  I wanted to send a message to the web client on a particular destination. The client code is simple.

package com.anupam.app.client;

import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;

public class Client {

	static String url = &amp;amp;amp;amp;quot;ws://localhost:8080/springjavaconfig/ws/websocket&amp;amp;amp;amp;quot;;

	public static void main(String[] a) throws InterruptedException {
		WebSocketClient webSocketClient = new StandardWebSocketClient();
		WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
		stompClient.setMessageConverter(new StringMessageConverter());
		StompSessionHandler sessionHandler = new MyStompSessionHandler();
		stompClient.connect(url, sessionHandler);

		// Wait sometime.
		Thread.sleep(5000);
	}

	public static class MyStompSessionHandler extends StompSessionHandlerAdapter {

		@Override
		public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
			System.out.println(&amp;amp;amp;amp;quot;Connection successful.&amp;amp;amp;amp;quot;);
			session.send("<strong><span style="color: #339966;">/app/ping</span></strong>", "Hello");
		}
	}
}

Please note the url of our websocket.

static String url = "ws://localhost:8080/springjavaconfig/ws/websocket";

Please note that it starts with “ws” protocol and nothttp“. And I am sending a simple message to the destination “/app/ping“. Then the Message controller StompController receives the message at

        @MessageMapping("/ping")
	@SendTo("/topic/ping")
	public String ping(String message) throws Exception {
		System.out.println(message);
		return message;
	}

and routes to the destination “/topic/ping“. As the browser client is already subscribing to the destination it receives the message. And please do not forget to put a delay 

Thread.sleep(5000);

Please check out this video for a demo.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s