Backend Development 14 min read

How to Build a Real‑Time Chat with Spring Boot WebSocket: Step‑by‑Step Guide

This article explains how to integrate WebSocket into a Spring Boot project to create a lightweight instant‑messaging system, covering dependency setup, configuration classes, core server implementation, required modules, common deployment issues, and practical solutions with complete code examples.

macrozheng
macrozheng
macrozheng
How to Build a Real‑Time Chat with Spring Boot WebSocket: Step‑by‑Step Guide

1. Implementation Steps

Integrating instant messaging into a project can be solved instantly by adding Spring Boot WebSocket support. The integration consists of three steps: adding the Maven dependency, creating a configuration class, and implementing the core WebSocket server class.

1.1 Add Dependency

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-websocket&lt;/artifactId&gt;
  &lt;version&gt;2.1.13.RELEASE&lt;/version&gt;
&lt;/dependency&gt;</code>

1.2 Add WebSocket Configuration

<code>import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}</code>

1.3 Core Server Class

<code>@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
    private static MessageStore messageStore;
    private static MessageSender messageSender;

    public static void setMessageStore(MessageStore store) { messageStore = store; }
    public static void setMessageSender(MessageSender sender) { messageSender = sender; }

    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        messageStore.saveSession(session);
    }

    @OnClose
    public void onClose(Session session, @PathParam("userId") String userId) {
        messageStore.deleteSession(session);
    }

    @OnMessage
    public void onMessage(String message, Session session) throws Exception {
        log.warn("Received from {}: {}", session.getId(), message);
        handleTextMessage(session, new TextMessage(message));
    }

    @OnError
    public void onError(Session session, @PathParam("userId") String userId, Throwable error) {
        log.error("Error occurred", error);
    }

    public void handleTextMessage(Session session, TextMessage message) throws Exception {
        log.warn("Processing message: {}", message.getPayload());
    }
}</code>

1.4 Front‑end Integration

<code>&lt;!DOCTYPE html&gt;
&lt;html xmlns="http://www.w3.org/1999/html"&gt;
&lt;head&gt;&lt;title&gt;WebSocket Example&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
    登录用户ID:&lt;input type="text" id="sendUserId"/&gt;&lt;br/&gt;
    接受用户ID:&lt;input type="text" id="receivedUserId"/&gt;&lt;br/&gt;
    发送消息内容:&lt;input type="text" id="messageInput"/&gt;&lt;br/&gt;
    接受消息内容:&lt;input type="text" id="messageReceive"/&gt;&lt;br/&gt;
    &lt;button onclick="sendMessage()"&gt;Send&lt;/button&gt;
    &lt;script&gt;
        var socket = new WebSocket("ws://localhost:8080/websocket/aaa");
        var roomId = "123456";
        var sendUserId = Math.floor(Math.random()*1000000);
        document.getElementById("sendUserId").value = sendUserId;
        var messageReceive = document.getElementById("messageReceive");
        socket.onopen = function(event) {
            console.log("WebSocket is open now.");
            let loginInfo = {msgType:2, sendUserId:sendUserId, bizType:1, bizOptModule:1, roomId:roomId, msgBody:{}};
            socket.send(JSON.stringify(loginInfo));
        };
        socket.onmessage = function(event) {
            var message = event.data;
            console.log("Received message: "+message);
            messageReceive.value = message;
        };
        socket.onclose = function(event) { console.log("WebSocket is closed now."); };
        function sendMessage() {
            var message = document.getElementById("messageInput").value;
            var receivedUserId = document.getElementById("receivedUserId").value;
            let operateInfo = {msgType:100, sendUserId:sendUserId, bizType:1, bizOptModule:1, roomId:roomId, receivedUserId:receivedUserId, msgBody:{operateType:1, operateContent:"1"}};
            socket.send(JSON.stringify(operateInfo));
        }
        setInterval(()=>{ socket.send("ping"); }, 30000);
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code>

2. Modules Required for a Small IM System

The basic WebSocket integration provides full‑duplex communication, but a functional instant‑messaging system also needs several core modules.

2.1 Architecture Diagram

2.2 Message Object Model

<code>public class SocketMsg<T> {
    /** Message type: 1‑heartbeat, 2‑login, 3‑business operation */
    private Integer msgType;
    /** Sender user ID */
    private String sendUserId;
    /** Receiver user ID */
    private String receivedUserId;
    /** Business type */
    private Integer bizType;
    /** Business operation module */
    private Integer bizOptModule;
    /** Message body */
    private T msgBody;
}</code>

2.3 Message Storage Module

Stores the mapping between user IDs and WebSocket sessions to prevent data loss on server restarts.

2.4 Message Sending Module

In a distributed environment, sessions may reside on different machines. The sending module forwards messages to a message broker (e.g., Kafka) so that each instance can deliver to its local sessions.

2.5 Message Push Module

Consumes messages from the broker and pushes them to the appropriate client sessions on the current node.

3. Problems Encountered and Solutions

3.1 Connection Auto‑Disconnect

WebSocket connections close after a period of inactivity. The solution is to send periodic heartbeat (ping/pong) messages, e.g., every 30 seconds.

3.2 Session Serialization Issue

WebSocket sessions cannot be serialized, so storing them in Redis is not feasible. The approach is to keep sessions in memory on each node and use a broker to route messages.

3.3 Bean Injection Failure in @ServerEndpoint

WebSocket endpoint instances are created per connection and are not Spring singletons, causing @Autowired/@Resource to fail. The fix is to expose static setter methods and inject beans during application startup.

<code>@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
    private static MessageStore messageStore;
    private static MessageSender messageSender;
    public static void setMessageStore(MessageStore store) { messageStore = store; }
    public static void setMessageSender(MessageSender sender) { messageSender = sender; }
}

@Service
public class MessageStore {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @PostConstruct
    public void init() { WebSocketServer.setMessageStore(this); }
}</code>

3.4 Distributed Message Delivery

When a user's session is on a different node, publish the message to a message‑queue broker. Each instance consumes the queue, checks if the target user’s session exists locally, and pushes the message if found.

3.5 Nginx Proxy Configuration

For a single‑layer proxy, a simple location block with upgrade headers is sufficient. With a double‑layer proxy, additional headers (X‑Forwarded‑For, X‑Forwarded‑Proto) must be forwarded to preserve the original protocol and client IP.

<code>location ~* ^/api/websocket/* {
    proxy_pass http://backend.service;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
}</code>

4. Complete Code and Demo

4.1 Demo Effect

Open two browser windows, enter the peer’s user ID, type a message, and click Send. The message appears in the other window’s receive field.

4.2 Code Structure Diagram

4.3 Repository

Source code is available at https://github.com/yclxiao/spring-websocket.git .

5. Conclusion

The article demonstrated how to integrate WebSocket into Spring Boot, described the essential modules for a lightweight instant‑messaging system, discussed common deployment challenges, and provided complete code examples to help developers build real‑time communication features quickly.

distributed systemsBackend DevelopmentSpring BootWebSocketReal‑Time CommunicationInstant Messaging
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.