Web-Socket with Spring: Real-Time Connections Made Easy

Say goodbye to back-and-forth! Web-Socket lets websites and servers have real-time conversations instead of sending endless requests

Sivaram Rasathurai
Javarevisited
9 min readDec 23, 2023

--

In this blog, we’ll dive into:

  • How Web-Sockets transform communication
  • Mastering configuration with @EnableWebSocketMessageBroker ⚙️
  • Building a real-time location-sharing app
  • Code breakdowns for both frontend and backend

What’s the buzz about WebSockets?

Photo by National Cancer Institute on Unsplash

Tired of the back-and-forth dance between browsers and servers? Enter WebSockets — a game-changer that lets them chat seamlessly in real-time, on a single connection! Imagine the possibilities: instant messaging, live games, dynamic maps, and more!

Let’s ditch the letter writing

Photo by Kate Macate on Unsplash

Think of regular web communication as snail mail — slow, one-way exchanges. You send a request (a letter), the server replies (a response), and the cycle repeats. WebSockets are like phone calls — instant, two-way conversations! Both servers and clients can talk and listen simultaneously, making everything faster and more interactive.

But how does it work?

Photo by Tachina Lee on Unsplash

Unlike HTTP, which needs a new connection for each request, Web-Sockets establish a single, persistent connection. This opens up a world of possibilities:

  • Real-time updates: Watch live game scores, track chat messages, or see friends’ locations on a map, all with no lag!
  • Push notifications: Servers can send updates directly to clients without waiting for requests, so you’ll never miss a beat.
  • Two-way communication: Send and receive data simultaneously, enabling features like online games or collaborative editing.

WebSockets vs. Alternatives:

Photo by Chelsea shapouri on Unsplash
  • Long Polling: Picture yourself waiting for your friend’s reply (the server) while holding onto your letter (the request). Long Polling is like that — the client keeps asking for updates until the server has something new. It’s inefficient and wastes resources.
  • Server-Sent Events: Think of your friend (the server) only talking to you (the client), without you being able to respond — that’s Server-Sent Events. It’s good for one-way updates, but lacks the interactive power of WebSockets.
  • MQTT: Imagine a radio station broadcasting news — anyone can tune in (clients) and receive updates. MQTT is similar, but focuses on lightweight messaging for resource-constrained environments.

Building a Real-Time App with Spring → Geo+:

Photo by Maxim Hopman on Unsplash

Now that you’re hyped about WebSockets, let’s build a real-time location sharing app using Spring! Imagine friends sharing their whereabouts instantly, keeping track of each other during adventures. We’ll use:

  • Web-Socket configuration: Set up the communication bridge between clients and the server usring spring web-sockets.
  • Client-side connection: Connect to the server and send/receive location updates with HTML, CSS and Javascript.

Ready to dive in? We’ll cover the code and explain each step, making your real-time app a masterpiece!

Spring’s secret weapon for Web-Socket mastery → @EnableWebSocketMessageBroker!

Photo by Phil Mosley on Unsplash

But first, let’s visualize Web-Sockets:

Imagine a busy airport where messages are passengers:

@EnableWebSocketMessageBroker flips the switch, opening the airport for seamless travel.

WebSocketMessageBrokerConfigurer acts as air traffic control, directing passengers to the right gates (destinations).

Message brokers serve as efficient baggage handlers, routing messages smoothly.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/app");
registry.setApplicationDestinationPrefixes("/geoplus");
registry.setUserDestinationPrefix("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}

@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(new ObjectMapper());
converter.setContentTypeResolver(resolver);
messageConverters.add(converter);
return false;
}

}

@EnableWebSocketMessageBroker Annotation is used to enable the spring boot application for web-socket message handling, including the configuration of a message broker. WebSocketMessageBrokerConfigurer is a spring framework interface whic defines methods for configuring message handling with simple messaging protocols (e.g. STOMP) from WebSocket clients.

To enable web-socket in spring boot application, you need to override three methods in this interface.

  1. ConfigureMessageBroker: Mapping the Message Highways ️
  2. registerStompEndpoints: Opening the Airport Doors ✈️
  3. configureMessageConverters: Hiring the Language Experts

Let’s explore each configuration method in detail

ConfigureMessageBroker: Mapping the Message Highways ️

Picture this as mapping out the message highways. It sets up the message broker, defining routes for different message types. Think of it as organizing a buzzing airport, directing passengers (messages) to the right gates (destinations).

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/app");
registry.setApplicationDestinationPrefixes("/geoplus");
registry.setUserDestinationPrefix("/app");
}

This method is used to configure the message broker. In WebSocket, a message broker is responsible for routing messages between clients.

The MessageBrokerRegistry class is a part of the configuration mechanism provided by Spring for WebSocket support. It plays a crucial role in setting up the messaging infrastructure, allowing developers to define how messages are routed, where they are sent, and how clients interact with the message broker. Messaging infrastructure refers to the underlying system that handles the sending and receiving of messages between clients and servers in a WebSocket-based application. The MessageBrokerRegistry helps configure this infrastructure, making it possible to define how messages flow within the application, which is essential for the proper functioning of WebSocket-based features.

With this WebSocket configuration, we establish an in-memory message broker infrastructure for WebSocket communication, utilizing a publish-subscribe (pub-sub) model. The broker acts as a memory queue, mediating messages between clients.

  • Publishing Messages: Clients or producers use the application destination prefix (/app) to publish messages to the server. For example, sending a message to /app/chat.sendMessage indicates a chat message publication.
  • Consuming Messages: Clients subscribe to topics using the destination prefix /topic. For instance, subscribing to /topic/chat/general allows a client to receive general chat messages. The registry.enableSimpleBroker("/topic") line enables the broker to handle these topics and broadcast messages to subscribed clients.
  • Separation of Concerns: The distinct prefixes (/app for publishing, /topic for subscribing) neatly separate concerns and organize messages by their intended purpose.

The use of different prefixes for client-to-server messages ("/app") and server-to-client messages ("/topic") helps in cleanly separating the concerns of sending messages from clients to the server and broadcasting messages from the server to multiple clients.

In essence, this configuration creates a structured WebSocket communication system, where clients publish messages with one prefix and subscribe to topics with another, allowing for organized and efficient message handling.

The line registry.setUserDestinationPrefix(“/user”); in a Spring WebSocket configuration sets the prefix for messages targeted at specific users. In a WebSocket application, there might be scenarios where you want to send messages directly to a particular user. For example, sending a private message or user-specific notifications. This configuration is often used in conjunction with user authentication and authorization mechanisms. It ensures that messages are delivered only to the intended user. It allows for secure and targeted communication with specific users. This configuration separates user-specific destinations from general destinations. While /topic might be used for broadcasting messages to all interested clients, /user is specifically used for messages directed at individual users.

registerStompEndpoints: Opening the Airport Doors ✈️

This is like opening the airport doors for passengers! It creates the endpoints where clients can connect using WebSockets, ensuring a smooth entry point for communication.

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

Clients Need to use an endpoint to establish a WebSocket connection with the server. For that we need to create an endpoint for the connection creation usage. SockJS is a JavaScript library that provides a fallback mechanism in case WebSocket is not supported by the client or the network. By calling .withSockJS(), we’re enabling SockJS support for the specified WebSocket endpoint. This ensures that clients with browsers that do not support WebSocket or are behind restrictive networks can still establish a connection using SockJS.

For example, Front end client can connect like below

var socket = new SockJS('/ws');
var stompClient = Stomp.over(socket);

stompClient.connect({}, function (frame) {
console.log('Websocket connection connected: ' + frame);
});

configureMessageConverters: Hiring the Language Experts

Ever needed a translator in a foreign country? This method does just that for messages, ensuring they’re understood by both clients and servers. It’s like having a language expert on hand, avoiding any miscommunications.

@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(new ObjectMapper());
converter.setContentTypeResolver(resolver);
messageConverters.add(converter);
return false;
}

We configure a message converter for WebSocket communication, specifically for handling JSON messages. It sets up a MappingJackson2MessageConverter with a default MIME type of JSON and adds it to the list of message converters. This configuration ensures that WebSocket messages in the application are converted to and from JSON using Jackson’s ObjectMapper. By returnig false, to indicate that the default message converters should not be modified. This means that the method is providing additional configuration without completely replacing the default converters.

Once we create above configuration class, we are ready to use the websocket for messaging.

We are going to build an application where the users will send their location to server and subscribe a specific user to get their locations.

Backend Code

@Controller
@RequiredArgsConstructor
@Slf4j
class LocationController {
private final SimpMessagingTemplate wsTemplate;

@MessageMapping("/location")
public void sendLocation(@Payload Location location) {
log.info("Received location : {}", location);
wsTemplate.convertAndSendToUser(location.getRecipient(), "/user-locations", location);
}
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
class Location implements Serializable {
private String sender;
private String recipient;
private double latitude;
private double longitude;
}

With above backend setup, We created websocket connection to send the location to /geoplus/location endpoint and receive the locatio updates to relevent userid subscription /app/{userId}/user-locations

Lets create front end code for this backend to simulate the functionality

HTML code

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoPlus</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="map"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>

CSS code

body, html {
margin: 0;
padding: 0;
height: 100%;
}

#map {
height: 100vh;
}

Javascript

document.addEventListener("DOMContentLoaded", function () {
// Connect to WebSocket
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

let map;
let senderMarker;
let receiverMarker;

// Prompt user for sender and receiver IDs
const senderId = prompt("Enter your ID:");
const receiverId = prompt("Enter the ID of the person you want to share your location with:");

stompClient.connect({}, onConnect, onError);

function onConnect() {
// Subscribe to the user's location updates
const destination = `/app/${receiverId}/user-locations`;
console.log("Subscribe to the user's location updates " + destination);
stompClient.subscribe(destination, onMessageReceived);

// Get and send the current location every 60 seconds (for testing purposes)
setInterval(() => {
getCurrentLocation().then((location) => {
// Attach sender and receiver IDs to the location object
location.sender = senderId;
location.recipient = receiverId;
stompClient.send(`/geoplus/location`, {}, JSON.stringify(location));
});
}, 60000); // 60-second interval (adjust for your needs)

// Leaflet Map Initialization
map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);

// Initialize receiver marker
receiverMarker = L.marker([0, 0]).addTo(map);
}

function onMessageReceived(message) {
const location = JSON.parse(message.body);
updateMap(location);
}

function onError() {
console.log('Could not connect to WebSocket server. Please refresh this page to try again!');
}

function updateMap(location) {
// Handle sender's marker
if (location.sender === senderId) {
if (!senderMarker) {
senderMarker = createMarker(location);
} else {
updateMarker(senderMarker, location);
}
}

// Handle receiver's marker
if (location.sender === receiverId) {
updateMarker(receiverMarker, location);
}

// Adjust map bounds only if necessary
if (shouldAdjustBounds(senderMarker, receiverMarker)) {
const bounds = L.latLngBounds([senderMarker.getLatLng(), receiverMarker.getLatLng()]);
map.fitBounds(bounds, { padding: [10, 10] });
}
}

function createMarker(location) {
const marker = L.marker([location.latitude, location.longitude])
.addTo(map)
.bindPopup(`${location.sender}'s Location`);
return marker;
}

function updateMarker(marker, location) {
marker.setLatLng([location.latitude, location.longitude]);
marker.bindPopup(`${location.sender}'s Location`).update();
}

function shouldAdjustBounds(marker1, marker2) {
if (marker1 && marker2) {
const distance = marker1.getLatLng().distanceTo(marker2.getLatLng()); // Calculate distance in meters
const thresholdDistance = 100; // Adjust this threshold as needed
return distance > thresholdDistance; // Return true if distance exceeds threshold
}
return false;
}

async function getCurrentLocation() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
const location = {
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
resolve(location);
},
(error) => {
reject(error);
}
);
});
}
});
  • Socket Connection and STOMP: WebSockets provide the foundation for real-time communication, and STOMP (Simple Text Oriented Messaging Protocol) streamlines messaging over WebSockets.
  • User Prompts: The script initiates a conversation, asking for sender and receiver IDs to establish connections.
  • Subscription and Location Sending: Upon connection, the script subscribes to the receiver’s location updates and sends the sender’s location every 60 seconds for continuous synchronization.
  • Leaflet Map Initialization: The Leaflet library paints the map on which the real-time journey unfolds.
  • Message Handling and Map Updates: Incoming location messages trigger marker updates and map adjustments, ensuring a visually engaging and informative experience.

Key Takeaways:

  • WebSockets and STOMP: This dynamic duo empowers real-time communication in web applications.
  • Spring’s Ease of Handling: Spring’s WebSocket support simplifies configuration and message management.
  • Front-End and Back-End Coordination: Seamless integration of front-end and back-end code is crucial for a smooth real-time experience.

You can find the source code here: https://github.com/rcvaram/geoplus/tree/main

So, ditch the snail mail and say hello to instant connections! WebSockets open a world of possibilities — unleash your real-time app dreams today!

--

--