Building Real-Time Applications with Web Sockets and Reactive Streams

Mahidhar MullapudiMahidhar Mullapudi
Jul 15, 2024·
8 min read

Introduction

Real-time applications are becoming increasingly important in today’s digital world, providing users with instant updates and interactive experiences. Technologies like Web Sockets and Reactive Streams play a crucial role in enabling these real-time capabilities. In this article, we’ll explore how to build a real-time chat application using Web Sockets and Reactive Streams with Java and Spring Boot. We’ll cover all the necessary concepts and provide detailed examples to help you get started.

Understanding WebSockets

What are WebSockets?

WebSockets are a communication protocol that provides full-duplex communication channels over a single TCP connection. Unlike traditional HTTP, which follows a request-response model, WebSockets allow for continuous two-way interaction between the client and server. This makes WebSockets ideal for applications that require real-time data updates, such as chat applications, live notifications, and online gaming.

How WebSockets Work

  1. Handshake: The WebSocket connection starts with a handshake request from the client to the server. This request is an HTTP request upgraded to a WebSocket connection.

  2. Connection Establishment: If the server accepts the handshake request, the connection is established, and both parties can start sending and receiving messages.

  3. Data Transfer: Once the connection is established, messages can be sent in both directions at any time. Messages are transmitted in frames, which can be either text or binary data.

  4. Connection Closure: Either the client or server can close the connection by sending a close frame. The other party responds with a close frame to complete the closure.

A sequence diagram showing interactions between a client and a server. The steps are as follows: Connect, Connection Accepted, Send Message, Process Message and Respond, and Disconnect.

Benefits of WebSockets

  • Low Latency: WebSockets provide low-latency communication, making them suitable for real-time applications.

  • Persistent Connection: Once established, the WebSocket connection remains open, reducing the overhead of establishing multiple HTTP connections.

  • Bidirectional Communication: Both the client and server can send messages independently, enabling real-time updates.

WebSocket Lifecycle

  1. Connection Establishment: The client sends a handshake request to the server to establish a WebSocket connection.

  2. Data Transfer: Once the connection is established, the client and server can exchange messages in both directions.

  3. Connection Closure: Either the client or the server can close the connection when it’s no longer needed.

WebSocket lifecycle state Diagram

Introduction to Reactive Streams

What are Reactive Streams?

Reactive Streams is a specification for asynchronous stream processing with non-blocking backpressure. It provides a standard for handling asynchronous data streams, allowing you to build resilient and responsive applications. Reactive Streams are particularly useful in environments where you need to handle a large number of concurrent data streams efficiently.

Advantages of Reactive Programming

  • Asynchronous Processing: Reactive Streams allow you to handle data asynchronously, improving the scalability and performance of your application.

  • Non-blocking Backpressure: Reactive Streams provide mechanisms to handle backpressure, ensuring that your application can handle varying data rates without overwhelming the system.

  • Composability: Reactive Streams offer a composable approach to building complex data pipelines, making it easier to manage and reason about your code.

Key Concepts in Reactive Streams

  • Publisher: Produces data and sends it to Subscribers.

  • Subscriber: Consumes data produced by Publishers.

  • Subscription: Represents the relationship between a Publisher and a Subscriber.

  • Processor: Acts as both a Publisher and a Subscriber, enabling data transformation and processing.

Combining WebSockets with Reactive Streams

The Synergy Between WebSockets and Reactive Streams

Combining WebSockets with Reactive Streams allows you to build powerful real-time applications. WebSockets provide the communication channel, while Reactive Streams handle the data flow and processing. This combination enables you to build applications that are both responsive and scalable.

Architecture Overview

  1. WebSocket Connection: Establish a WebSocket connection between the client and server.

  2. Reactive Stream Processing: Use Reactive Streams to handle incoming and outgoing data.

  3. Data Broadcasting: Broadcast data to multiple clients using WebSockets.

Use Case: Live Chat Application

Why Choose a Chat Application?

A live chat application is an excellent example to demonstrate the capabilities of WebSockets and Reactive Streams. It requires real-time communication, scalability to handle multiple users, and efficient data processing. This makes it an ideal use case to showcase how these technologies work together.

Features of the Chat Application

  • Real-time messaging between users.

  • Broadcast messages to all connected clients.

  • Handle multiple users simultaneously.

  • Efficiently manage data streams and backpressure.

Real-time chat application sequence diagram

Implementation with Java and Spring Boot

Setting Up the Project

Create a Spring Boot Project: Use Spring Initializr to create a new Spring Boot project with WebSocket and Reactive Web dependencies.

spring init --dependencies=websocket,reactive-web chat-application

Project Structure:

├── src
   │   ├── main
   │   │   ├── java
   │   │   │   └── com.example.chat
   │   │   │       ├── ChatApplication.java
   │   │   │       ├── config
   │   │   │       │   └── WebSocketConfig.java
   │   │   │       ├── controller
   │   │   │       │   └── ChatController.java
   │   │   │       ├── handler
   │   │   │       │   └── ChatWebSocketHandler.java
   │   │   │       ├── model
   │   │   │       │   └── ChatMessage.java
   │   │   │       └── service
   │   │   │           └── ChatService.java
   │   │   ├── resources
   │   │   │   └── application.properties

WebSocket Configuration

Configure WebSockets in Spring Boot by creating a WebSocket configuration class.

package com.example.chat.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import com.example.chat.handler.ChatWebSocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final ChatWebSocketHandler chatWebSocketHandler;

    public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
        this.chatWebSocketHandler = chatWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatWebSocketHandler, "/chat").setAllowedOrigins("*");
    }
}

Explanation of Keywords

  • @Configuration: Indicates that the class declares one or more @Bean methods and may be processed by the Spring container to generate bean definitions and service requests at runtime.

  • @EnableWebSocket: Enables WebSocket support in a Spring application.

  • WebSocketConfigurer: An interface to configure WebSocket handlers.

  • WebSocketHandlerRegistry: A registry for WebSocket handlers.

  • setAllowedOrigins: Configures allowed origins for cross-origin requests.

Creating the WebSocket Handler

Implement the WebSocket handler to manage chat messages.

package com.example.chat.handler;
import com.example.chat.model.ChatMessage;
import com.example.chat.service.ChatService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.stereotype.Component; 

@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
    private final ChatService chatService;
    private final ObjectMapper objectMapper;

    public ChatWebSocketHandler(ChatService chatService, ObjectMapper objectMapper) {
        this.chatService = chatService;
        this.objectMapper = objectMapper;
    }

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
        chatService.sendMessage(chatMessage);
    }
}

Explanation of Keywords

  • @Component: Indicates that an annotated class is a “component”. Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.

  • TextWebSocketHandler: A convenience base class for handling WebSocket text messages.

  • WebSocketSession: Represents a WebSocket session between a client and server.

  • TextMessage: Represents a WebSocket text message.

Reactive Streams for Chat Messages

Implement a service to handle chat messages using Reactive Streams.

package com.example.chat.service;
import com.example.chat.model.ChatMessage;
import org.springframework.stereotype.Service;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;

@Service
public class ChatService {
    private final EmitterProcessor<ChatMessage> chatProcessor = EmitterProcessor.create();
    private final FluxSink<ChatMessage> chatSink = chatProcessor.sink();

    public Flux<ChatMessage> getChatMessages() {
        return chatProcessor.publish().autoConnect();
    }

    public void sendMessage(ChatMessage message) {
        chatSink.next(message);
    }
}

Explanation of Keywords

  • @Service: Indicates that an annotated class is a “Service”. Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.

  • @EmitterProcessor: A processor that allows dynamic push-pull flow control.

  • Flux: A Reactive Streams Publisher with RxJava 2.x API and backpressure support.

  • FluxSink: An interface through which subscribers receive items.

WebSocket Controller

Create a WebSocket controller to manage chat message broadcasting.

package com.example.chat.controller;
import com.example.chat.model.ChatMessage;
import com.example.chat.service.ChatService;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.SubscribeMapping;
import reactor.core.publisher.Flux;

@Controller
public class ChatController {
    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(ChatMessage chatMessage) {
        chatService.sendMessage(chatMessage);
        return chatMessage;
    }

    @SubscribeMapping("/chat.getMessages")
    public Flux<ChatMessage> getMessages() {
        return chatService.getChatMessages();
    }
}

Explanation of Keywords

  • @Controller: Indicates that an annotated class is a “Controller” (e.g., a web controller).

  • @MessageMapping: Maps a message to a specific handler method.

  • @SendTo: Specifies the destination to send the return value of a message-handling method.

  • @SubscribeMapping: Maps a subscription to a specific handler method.

Client-Side Implementation

Implement the client-side using JavaScript with WebSocket.

<!DOCTYPE html>
<html>
<head>
    <title>Chat Application</title>
    <script>
        let socket = new WebSocket("ws://localhost:8080/chat");
        socket.onmessage = function(event) {
            let message = JSON.parse(event.data);
            displayMessage(message);
        };

        function sendMessage() {
            let messageContent = document.getElementById("message").value;
            let message = {
                sender: "User",
                content: messageContent
            };
            socket.send(JSON.stringify(message));
            document.getElementById("message").value = '';
        }

        function displayMessage(message) {
            let messageElement = document.createElement('div');
            messageElement.textContent = `${message.sender}: ${message.content}`;
            document.getElementById('messages').appendChild(messageElement);
        }
    </script>
</head>
<body>
    <h1>Chat Application</h1>
    <div id="messages"></div>
    <input type="text" id="message" placeholder="Enter your message">
    <button onclick="sendMessage()">Send</button>
</body>
</html>

Explanation of Keywords

  • WebSocket: Creates a new WebSocket connection to the specified URL.

  • onmessage: An event handler that is called when a message is received from the server.

  • send: Sends data to the server over the WebSocket connection.

Testing and Debugging

Testing WebSocket Endpoints

  • Use tools like Postman or WebSocket clients to test the WebSocket endpoints.

  • Write unit tests to validate the WebSocket configuration and handlers.

Debugging Tips

  • Enable detailed logging for WebSocket events.

  • Use browser developer tools to monitor WebSocket connections and messages.

Conclusion

In this article, we explored how to build a real-time chat application using Web Sockets and Reactive Streams with Java and Spring Boot. We covered the essential concepts of Web Sockets and Reactive Streams, set up a Spring Boot project, and implemented the chat application step-by-step. By combining these technologies, you can build responsive and scalable real-time applications. We encourage you to experiment with these technologies and expand the chat application with additional features like user authentication, private messaging, and message persistence.

46
Subscribe to my newsletter

Read articles from Mahidhar Mullapudi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mahidhar Mullapudi
Mahidhar Mullapudi

Currently working as Senior Staff Engineer @Microsoft, I'm an expert in software architecture, system design, architectural patterns of a large-scale distributed products/services, cloud infrastructure and security. Proficient in different programming languages including Java, C#, Python with over a decade of experience working on applications at scale. Independent researcher with more than 20+ research articles across different fields in Computer Science with focus on Distributed systems, designing and building large-scale resilient applications, building real-time data platforms for analytics and Machine Learning. Founder and author of tutorialQ (https://tutorialq.com/) which provides quality technical content for learning programming, web development and other software related tech stack.