Spring Cloud Gateway And WebSockets

Spring Cloud Gateway And WebSockets thumbnail
52K
By Dhiraj 06 February, 2020

WebSocket provides a bi-directional, full-duplex communication channel between a server and client and is a key feature in modern web apps. We have already discussed a lot about WebSockets in my previous articles but in this article, we will specifically discuss creating load-balanced WebSocket connections over an API gateway with STOMP protocol.

The API gateway that we will use is Spring Cloud Gateway and we will have a downstream microservice as notification service that will have all the WebSockets implementation. This example app can also be used to demonstrate a simple client-server chat system.

In my previous post of spring cloud gateway demo, we created 2 microservices as First Service and Second Service with Eureka as a discovery service for service discovery. We will enhance the same example app and add one more service as Notification Service to demonstrate WebSocket connection with Spring cloud gateway. We will have an index.html page to verify the WebSockets connections and bi-directional communication in the browser through an API gateway. Below is the screenshot of it.

spring-cloud-gateway-websocket

Project Setup

As discussed above, we have one gateway-service, discovery-service, and 3 downstream microservices but in this article we will only build notification-service. Hence, let us start by generating these projects on https://start.spring.io

Spring Cloud Gateway Service

For gateway service, the selected dependencies are Gateway, Hystrix, and Actuator.

spring-cloud-gateway-project-strct

Below is the final pom.xml. Additionally, we have added spring-cloud-starter-netflix-eureka-client dependency in our pom. The spring cloud version that we are using is Greenwich.RC2

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
spring-cloud-gateway-websocket-project-structure

Spring Cloud Route Configuration

Route is the basic building block of the gateway. Once a request reaches to the gateway, the first thing gateway does is to match the request with each of the available route based on the predicate defined and the request is routed to the matched route.

You can follow my another article for the complete spring cloud gateway route predicates..

Below is our load-balanced route configuration. For notification-service, we have route configured as any request matching with the path /websocket will be routed to notification-service.

@Configuration
public class BeanConfig {

    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/api/v1/first/**")
                        .filters(f -> f.rewritePath("/api/v1/first/(?.*)", "/${remains}")
                                .addRequestHeader("X-first-Header", "first-service-header")
                                .hystrix(c -> c.setName("hystrix")
                                        .setFallbackUri("forward:/fallback/first")))
                        .uri("lb://FIRST-SERVICE/")
                        .id("first-service"))

                .route(r -> r.path("/websocket/**")
                        .filters(f -> f.rewritePath("/websocket/(?.*)", "/${remains}"))
                        .uri("lb://NOTIFICATION-SERVICE/")
                        .id("notification-service"))
                
                .route(r -> r.path("/api/v1/second/**")
                        .filters(f -> f.rewritePath("/api/v1/second/(?.*)", "/${remains}")
                                .hystrix(c -> c.setName("hystrix")
                                        .setFallbackUri("forward:/fallback/second")))
                        .uri("lb://SECOND-SERVICE/")
                        .id("second-service"))
                .build();
    }

}

Below is the application.yml file of gateway-service

hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 2000
spring:
  application:
    name: api-gateway

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka
    register-with-eureka: false
  instance:
    preferIpAddress: true

management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream


Notification Service Setup with WebSockets

The configuration of this service will be a normal websocket configuration in Spring Boot. You can follow this article on spring boot WebSocket to get some inside concepts of the configuration and implementation. Here, we are using STOMP as a message broker but you can also use raw WebSocket by defining your own messaging protocol to connect to spring WebSocket using TextWebSocketHandler. The complete article can be found here.

Below is the pom.xml of noification-service.

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-websocket</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	</dependency>
	<dependency>
		<groupId>com.google.code.gson</groupId>
		<artifactId>gson</artifactId>
	</dependency>

</dependencies>

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>${spring-cloud.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>
<repositories>
	<repository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
	</repository>
</repositories>

@EnableWebSocketMessageBroker : It enables WebSocket message handling, backed by a message broker. Here we are using STOMP as a mesage broker.

The method configureMessageBroker() enables a simple memory-based message broker to carry the messages back to the client on destinations prefixed with "/topic" and "/queue".

WebSocket Config

WebSocketConfig.java

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/greeting");
    }

}

It designates the "/app" prefix for messages that are bound for @MessageMapping-annotated methods in controller class. This prefix will be used to define all the message mappings; for example, "/app/message" is the endpoint that the WebSocketController.processMessageFromClient() method is mapped to handle.

Similarly,registerStompEndpoints() enables STOMP support and registers stomp endoints at "/greeting".Doing so, all the websocket messages will be channelised through STOMP and this also adds an extra layer of security to the websocket endpoint.

Remember that, while creating websocket connection from javascipt, we will be using this particular stomp endpoint only.

Discovery Server Setup

Below is the pom.xml and application.yml file required for this integration. You can visit my previous article for the complete integration of Netflix Eureka with Spring Cloud

<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>${spring-cloud.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>
<repositories>
	<repository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
	</repository>
</repositories>
<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>

Spring WebSocket Controller

@MessageMapping: This isto map the message headed for the url /message

@SendTo: Indicates a method's return value should be converted to message if necessary and sent to the specified destination.

@MessageExceptionHandler: throw any exceptions caused by STOMP to the end user on the /queue/errors destination.

@Payload: to extract the payload of a message and optionally convert it using spring MessageConverter.

@Controller
public class WebSocketController {

	@MessageMapping("/message")
	@SendTo("/topic/reply")
	public String processMessageFromClient(@Payload String message){
		String name = new Gson().fromJson(message, Map.class).get("name").toString();
		return "Hello " + name;
	}
	
	@MessageExceptionHandler
    @SendToUser("/topic/errors")
    public String handleException(Throwable exception) {
        return exception.getMessage();
    }

}

Below is the application.yml file for notification-service.

spring:
  application:
    name: notification-service
server:
  port: 8085

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

WebSocket Client Implmentation

Here, we are using plain Javascript and HTML to create our client app. You can also use Angular for the same. I have integrated it with Angular in this article.

These static files are available inside gateway-service inside src/main/resources/static. You can visit this article to know more about serving static content from API Gateway.

app.js

The connect() will create a web socket connection. Remember the mapping we did in WebSocketConfig.java class for /greeting.

The function onmessage() will execute itself once any message is broadcasted by server

function showGreeting() will be simply showing the message received by the server broadcast.

send() function will construct a JSON message and sends message to the server.

function connect() {
	var socket = new WebSocket('ws://localhost:8080/websocket/greeting');
	ws = Stomp.over(socket);

	ws.connect({}, function(frame) {
		ws.subscribe("/topic/errors", function(message) {
			alert("Error " + message.body);
		});

		ws.subscribe("/topic/reply", function(message) {
			showGreeting(message.body);
		});
	}, function(error) {
		alert("STOMP error " + error);
	});
}

function disconnect() {
    if (ws != null) {
        ws.close();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    ws.send($("#name").val());
}

function showGreeting(message) {
    $("#greetings").append(" " + message + "");
}

we have a .html file to trigger the WebSocket connection over a button click. The text entered in the text box will be sent to server over websocket protocol and the response from the server will be diaplayed in the Greetings div element.

Index.html

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link href="/bootstrap.min.css" rel="stylesheet">
    <link href="/main.css" rel="stylesheet">
    <script src="/jquery-1.10.2.min.js"></script>
    <script src="/app.js"></script>
    <script src="/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>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">What is your name?</label>
                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
 
</div>
</body>
</html>

Conclusion

In this article, we discussed about WebSocket protocol, and created an example app to communicate over WebSocket via Spring cloud gateway. As usual, the source code can be found on my github page here.

Share

If You Appreciate This, You Can Consider:

We are thankful for your never ending support.

About The Author

author-image
A technology savvy professional with an exceptional capacity to analyze, solve problems and multi-task. Technical expertise in highly scalable distributed systems, self-healing systems, and service-oriented architecture. Technical Skills: Java/J2EE, Spring, Hibernate, Reactive Programming, Microservices, Hystrix, Rest APIs, Java 8, Kafka, Kibana, Elasticsearch, etc.

Further Reading on Spring Cloud