According to the poll on our group (if you haven’t joined it yet, I invite you to do so -> https://www.facebook.com/groups/devopsi . You will have an impact on what we will write about). Today’s post will be devoted to Websockets. What is WebSockets? When and how to use them?

The post about WebSockets will be split into three smaller entries:

  1. Today’s: some theory + Java;
  2. It will appear at the end of February – it will be about the frontend;
  3. It will appear at the beginning of March – we will combine Java and frontend and run our application.

Websockets – what are they?

Clients (Internet browsers) and servers (backend applications) communicate with each other using HTTP/S protocol, which supports requests (from browsers) and replies (from applications).

So much theory. What does it look like in practice?

  • We type the address of our application in the browser.
  • The application (Client) sends a request to the server.
  • Then a connection is established between the Client and the Server.
  • The response is sent by the server – to the Client.
  • The connection – is completed.

Long Pooling

The HTTP/S connection has served its purpose – it has communicated between the Client and the Server. So it can be terminated. But what if we want the connection to continue? For example, we are writing an application that has to show real-time quotes. Following the HTTP/S communication, we can come across the concept of HTTP Pooling. In practice it will look like the following:

  • Client in 1s: Has Apple’s quote changed?
  • Server in 1s: No.
  • Client in 2s: Has Apple’s stock changed?
  • Server in 2s: No.
  • And so on every second.

As you can guess, this solution is very inefficient. With such communication, we use a lot of resources. And an additional problem is the number of unsuccessful requests. At this point, the Long Pooling technique comes to our rescue. Long Pooling maintains the connection, but it is very resource-intensive. Therefore, we return to our Websockets.

A Websocket does not require a request to be sent to respond. It establishes a connection to the server and listens for data. As soon as the server receives the data it will pass it on to the client as part of the established connection.

What does it look like in practice?

  • The client sends an HTTP request to the server.
  • A client-to-server HTTP connection is established.
  • If the server supports WebSocket protocol, the connection is updated.
  • This changes the HTTP connection to WebSocket.

So much theory.

Websockets in Java

For our implementation, we will use the STOMP protocol.

We start by generating a project in Spring Initializer

In my case it looks like this:

Of course, I don’t impose the choice of Maven or Java 11, but WebSocket and Spring Web as Dependencies are compulsory 🙂 .

Then we need to add additional dependencies:

<dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-data-rest</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Next, we need to create our model. Let’s assume that we will be dealing with stock quotes, so our model will have 4 elements:

  • name
  • purchaseLimit
  • saleLimit
  • rate
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class Stock {
    public String name;
    public Double purchaseLimit;
    public Double saleLimit;
    public Double rate;
}

In the next step, we create our service. We’re going to store the data we’re going to use initially and add a method responsible for randomly pushing the data to the frontend.

package pl.stompexample.websockets.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import pl.stompexample.websockets.model.Stock;

import javax.annotation.PostConstruct;
import java.util.*;

@Service
@Slf4j
public class StockService {
    private final Set<String> listenerClients = new HashSet<>();
    private final List<Stock> stock=new LinkedList<>();

    @PostConstruct
    private void putData(){
        stock.add(new Stock("Alior",38.80,16.6650,16.6300));
        stock.add(new Stock("CCC",368.80,16.6650,17.6300));
        stock.add(new Stock("CDPROJECT",368.80,16.6650,145.6300));
        stock.add(new Stock("CyfrPLSat",368.80,16.6650,1644.6300));
        stock.add(new Stock("DINOPL",68.80,13.6650,16.6300));
        stock.add(new Stock("JSW",332.80,16.6650,10.6300));
        stock.add(new Stock("KGHM",368.80,16.6650,16.6300));
        stock.add(new Stock("LOTOS",38.80,14.6650,16.6300));
        stock.add(new Stock("LPP",368.80,16.6650,16.6300));
        stock.add(new Stock("MBANK",368.80,16.6650,16.6300));
        stock.add(new Stock("ORANGEPL",368.80,16.6650,1.6300));
        stock.add(new Stock("PEKAO",368.80,16.6650,156.6300));
        stock.add(new Stock("PGE",368.80,16.6650,23.6300));
        stock.add(new Stock("PGNIG",368.80,16.6650,36.6300));
        stock.add(new Stock("PKNORLEN",368.80,16.6650,16.6300));
        stock.add(new Stock("Sample",368.80,16.6650,16.6300));
        stock.add(new Stock("Sample 2",368.80,16.6650,16.6300));
        stock.add(new Stock("Sample 3",368.80,16.6650,16.6300));
        stock.add(new Stock("Sample 4",368.80,16.6650,16.6300));
        stock.add(new Stock("Sample 5",368.80,16.6650,16.6300));
    }

    @EventListener
    public void sessionDisconnectionHandler(SessionDisconnectEvent event) {
        String sessionId = event.getSessionId();
        log.info("Disconnecting " + sessionId + "!");
        removeSession(sessionId);
    }

    public List<Stock> getStocks(){
        return stock;
    }

    public void addSession(String sessionId) {
        log.info("Added session "+sessionId);
        this.listenerClients.add(sessionId);
    }

    public void removeSession(String sessionId) {
        log.info("Removed session "+sessionId);
        this.listenerClients.remove(sessionId);
    }

    public Set<String> getListenerClients() {
        return listenerClients;
    }

    public Stock getRandomChangedStock() {
        Random rand= new Random();
        int index = rand.nextInt(stock.size());
        Stock stockObj=stock.get(index);
        int randChoice=rand.nextInt(3)+1;
        double randDouble=Math.round((rand.nextDouble()*400)+1);
        switch(randChoice){
            case 1:
                stockObj.setPurchaseLimit(randDouble);
                break;
            case 2:
                stockObj.setSaleLimit(randDouble);
                break;
            case 3:
                stockObj.setRate(randDouble);
                break;
        }
        return stockObj;
    }
}

Next, we create our controller:

package pl.stompexample.websockets.controller;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import pl.stompexample.websockets.model.Stock;
import pl.stompexample.websockets.service.StockService;

import java.util.List;

@Controller
@Slf4j
@RequestMapping("/stocks")
@AllArgsConstructor
class StockController {
    private final StockService stockService;

    @GetMapping
    @ResponseBody
    List<Stock> getAllStocks(){
        return stockService.getStocks();
    }

    //stomp
    @MessageMapping("/start")
    public void start(StompHeaderAccessor stompHeaderAccessor) {
        log.info(stompHeaderAccessor.getSessionId());
        stockService.addSession(stompHeaderAccessor.getSessionId());
    }
    @MessageMapping("/stop")
    public void stop(StompHeaderAccessor stompHeaderAccessor) {
        stockService.removeSession(stompHeaderAccessor.getSessionId());
    }

}

We need to create a configuration for Websockets and the Scheduler, which are responsible for randomly sending random data to the frontend, so calling getRandomChangedStock.

Websockets configuration:

package pl.stompexample.websockets.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/stocks")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}

and Scheduler configuration:

package pl.stompexample.websockets.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import pl.stompexample.websockets.model.Stock;
import pl.stompexample.websockets.service.StockService;

import javax.annotation.PostConstruct;
import javax.management.Notification;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

@Configuration
@RequiredArgsConstructor
@EnableScheduling
@Slf4j
public class SchedulerConfig {
    private final SimpMessagingTemplate template;
    private final StockService stockService;

    @Scheduled(fixedDelay = 6000)
    public void dispatch(){
        for (String listener : stockService.getListenerClients()) {
            SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
            headerAccessor.setSessionId(listener);
            headerAccessor.setLeaveMutable(true);
            template.convertAndSendToUser(
                    listener,
                    "/topic/stocks",
                    stockService.getRandomChangedStock(),
                    headerAccessor.getMessageHeaders());
        }
    }
}

And that’s it as far as the backend is concerned. CORS configuration will be covered in part 3. If you have any questions – feel free to ask them directly on our -> group <- . There we will also release a repository with the above code in a few days 🙂 .

Image

Launch your rocket!

Leave your email - we'll get back to you as soon as possible!