Spring Boot中WebSocket的实现详解

在现代Web应用中,实时通信需求日益增长,如在线聊天、实时数据监控、消息推送等场景。传统的HTTP协议基于“请求-响应”模式,无法满足服务器主动向客户端推送数据的需求。而WebSocket协议作为HTML5的重要特性,实现了浏览器与服务器之间的全双工通信,为实时通信提供了高效解决方案。本文将详细介绍如何在Spring Boot项目中集成并实现WebSocket功能。

一、WebSocket核心概念

WebSocket是一种在单个TCP连接上进行全双工通信的协议,它允许服务器主动向客户端发送数据,而无需客户端先发起请求。其核心特点包括:

  • 全双工通信:客户端与服务器可同时发送数据,通信效率高;
  • 持久连接:连接建立后持续存在,避免了HTTP多次握手的开销;
  • 轻量协议:数据帧格式简单,头部开销小,传输效率优于HTTP;
  • 基于TCP:依托TCP协议保证数据传输的可靠性。

WebSocket的连接建立过程:客户端通过HTTP请求发起握手,请求头中携带Upgrade: websocketConnection: Upgrade等字段,表明希望将HTTP连接升级为WebSocket连接;服务器同意升级后,双方后续通信即使用WebSocket协议。

二、Spring Boot对WebSocket的支持

Spring Boot通过spring-boot-starter-websocket starter模块提供了对WebSocket的快速集成支持,底层基于Spring WebSocket模块实现。Spring WebSocket不仅实现了WebSocket协议规范,还提供了以下核心特性:

  • 基于注解的编程模型,简化WebSocket处理器开发;
  • 内置消息代理(如SockJS),解决浏览器不支持WebSocket的兼容问题;
  • 支持STOMP协议(Simple Text Oriented Messaging Protocol),提供更强大的消息路由和订阅/发布功能;
  • 与Spring Security集成,可实现WebSocket连接的权限控制。

三、Spring Boot实现WebSocket的两种方式

Spring Boot中实现WebSocket主要有两种场景:一是基于原生WebSocket API实现简单的点对点通信;二是基于STOMP协议实现复杂的订阅/发布模式(如群聊、广播通知)。下面分别详细介绍实现步骤。

方式一:基于原生WebSocket API实现

适用于简单的实时通信场景,如点对点聊天。核心是实现WebSocketHandler接口(或继承抽象类TextWebSocketHandler)处理消息收发,通过WebSocketConfigurer配置WebSocket连接路径。

步骤1:引入依赖

在Spring Boot项目的pom.xml(Maven)或build.gradle(Gradle)中引入WebSocket starter依赖:

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

步骤2:实现WebSocket处理器

创建自定义处理器类,继承TextWebSocketHandler,重写消息接收、连接建立/关闭等方法:

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

/**
 * 自定义WebSocket处理器
 */
public class CustomWebSocketHandler extends TextWebSocketHandler {

    /**
     * 连接建立时触发
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("WebSocket连接建立:" + session.getId());
        // 向客户端发送连接成功消息
        session.sendMessage(new TextMessage("连接成功!"));
    }

    /**
     * 接收客户端消息时触发
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("收到客户端消息:" + payload);
        // 向客户端回复消息
        session.sendMessage(new TextMessage("服务器已收到消息:" + payload));
    }

    /**
     * 连接关闭时触发
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("WebSocket连接关闭:" + session.getId() + ",状态:" + status);
    }

    /**
     * 连接异常时触发
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        System.out.println("WebSocket连接异常:" + session.getId() + ",异常信息:" + exception.getMessage());
    }
}

步骤3:配置WebSocket

创建配置类,实现WebSocketConfigurer接口,通过@EnableWebSocket注解开启WebSocket支持,注册处理器并配置连接路径:

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;

/**
 * WebSocket配置类
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册处理器,配置连接路径为/ws,允许跨域(开发环境可开启)
        registry.addHandler(new CustomWebSocketHandler(), "/ws")
                .setAllowedOrigins("*"); // 生产环境需指定具体域名,避免安全风险
    }
}

步骤4:编写前端测试页面

创建HTML页面,使用原生WebSocket API与后端建立连接,实现消息收发:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>WebSocket测试</title>
</head>
<body>
    <h3>WebSocket测试页面</h3>
    <input type="text" id="messageInput" placeholder="请输入消息">
    <button onclick="sendMessage()">发送消息</button>
    <div id="messageContainer" style="margin-top: 20px;"></div>

    <script>
        // 建立WebSocket连接(注意:HTTP对应ws,HTTPS对应wss)
        const ws = new WebSocket("ws://localhost:8080/ws");
        
        // 连接建立成功
        ws.onopen = function() {
            appendMessage("连接成功!");
        };
        
        // 接收服务器消息
        ws.onmessage = function(event) {
            appendMessage("服务器:" + event.data);
        };
        
        // 连接关闭
        ws.onclose = function(event) {
            appendMessage("连接关闭,状态码:" + event.code);
        };
        
        // 连接异常
        ws.onerror = function(error) {
            appendMessage("连接异常:" + error.message);
        };
        
        // 发送消息
        function sendMessage() {
            const input = document.getElementById("messageInput");
            const message = input.value.trim();
            if (message) {
                ws.send(message);
                appendMessage("客户端:" + message);
                input.value = "";
            }
        }
        
        // 追加消息到页面
        function appendMessage(content) {
            const container = document.getElementById("messageContainer");
            const div = document.createElement("div");
            div.textContent = new Date().toLocaleString() + ":" + content;
            container.appendChild(div);
        }
    </script>
</body>
</html>

步骤5:测试验证

  1. 启动Spring Boot项目;
  2. 将测试页面放入项目的resources/static目录下;
  3. 打开浏览器访问http://localhost:8080,输入消息并发送,可看到客户端与服务器的实时通信。

方式二:基于STOMP协议实现订阅/发布模式

原生WebSocket API适用于简单场景,但对于复杂的消息路由、订阅分组等需求,手动实现成本较高。STOMP协议(基于WebSocket的文本导向消息协议)提供了标准化的消息格式和订阅/发布机制,Spring Boot可通过集成STOMP快速实现群聊、广播通知等功能。

步骤1:引入依赖

与方式一相同,引入spring-boot-starter-websocket依赖即可(已包含STOMP相关组件)。

步骤2:配置STOMP

创建配置类,实现WebSocketMessageBrokerConfigurer接口,通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理:

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * STOMP协议配置类
 */
@Configuration
@EnableWebSocketMessageBroker // 开启WebSocket消息代理
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注册STOMP端点(客户端连接的入口)
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册端点/ws-stomp,允许跨域,启用SockJS fallback(兼容不支持WebSocket的浏览器)
        registry.addEndpoint("/ws-stomp")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    /**
     * 配置消息代理
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 配置应用前缀:客户端发送到服务器的消息需以/app开头
        config.setApplicationDestinationPrefixes("/app");
        // 配置内置消息代理:服务器向客户端推送的消息以/topic(广播)或/queue(点对点)开头
        config.enableSimpleBroker("/topic", "/queue");
        // 配置用户前缀(点对点消息时使用)
        config.setUserDestinationPrefix("/user");
    }
}

步骤3:编写消息控制器

创建控制器,使用@MessageMapping注解接收客户端消息,通过@SendTo注解将消息广播到指定主题:

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import java.time.LocalDateTime;

/**
 * STOMP消息控制器
 */
@Controller
public class StompMessageController {

    /**
     * 接收客户端发送到/app/chat的消息
     * @param message 客户端消息(格式:用户名:消息内容)
     * @return 广播的消息内容
     */
    @MessageMapping("/chat") // 对应客户端发送的目的地:/app/chat
    @SendTo("/topic/public") // 将返回结果广播到/topic/public主题
    public ChatMessage handleChatMessage(String message) {
        // 解析用户名和消息内容(实际项目中可通过Spring Security获取登录用户)
        String[] parts = message.split(":", 2);
        String username = parts.length > 0 ? parts[0].trim() : "匿名用户";
        String content = parts.length > 1 ? parts[1].trim() : "";
        
        // 构建消息对象
        return new ChatMessage(
                username,
                content,
                LocalDateTime.now().toString()
        );
    }

    /**
     * 消息实体类
     */
    public static class ChatMessage {
        private String username;
        private String content;
        private String time;

        // 构造方法、getter、setter
        public ChatMessage(String username, String content, String time) {
            this.username = username;
            this.content = content;
            this.time = time;
        }

        // getter和setter省略
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getContent() { return content; }
        public void setContent(String content) { this.content = content; }
        public String getTime() { return time; }
        public void setTime(String time) { this.time = time; }
    }
}

步骤4:编写前端测试页面(基于STOMP.js)

STOMP客户端需要使用STOMP.js库与后端通信,需引入STOMP.js和SockJS.js(兼容不支持WebSocket的浏览器):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>STOMP群聊测试</title>
    <!-- 引入SockJS和STOMP.js -->
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@6/dist/stomp.min.js"></script>
</head>
<body>
    <h3>STOMP群聊测试</h3>
    <input type="text" id="usernameInput" placeholder="请输入用户名">
    <button onclick="connect()">连接</button>
    <br><br>
    <input type="text" id="messageInput" placeholder="请输入消息" disabled>
    <button onclick="sendMessage()" disabled>发送消息</button>
    <div id="messageContainer" style="margin-top: 20px; height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px;"></div>

    <script>
        let stompClient = null;

        // 连接STOMP服务器
        function connect() {
            const username = document.getElementById("usernameInput").value.trim();
            if (!username) {
                alert("请输入用户名!");
                return;
            }

            // 创建SockJS连接(兼容模式)
            const socket = new SockJS("/ws-stomp");
            // 创建STOMP客户端
            stompClient = Stomp.over(socket);

            // 连接成功回调
            stompClient.connect({}, function(frame) {
                console.log("STOMP连接成功:" + frame);
                // 禁用用户名输入和连接按钮
                document.getElementById("usernameInput").disabled = true;
                document.getElementById("connect").disabled = true;
                // 启用消息输入和发送按钮
                document.getElementById("messageInput").disabled = false;
                document.getElementById("sendMessage").disabled = false;

                // 订阅/topic/public主题,接收广播消息
                stompClient.subscribe("/topic/public", function(message) {
                    const chatMessage = JSON.parse(message.body);
                    appendMessage(`${chatMessage.time} - ${chatMessage.username}:${chatMessage.content}`);
                });
            });

            // 连接失败回调
            stompClient.onerror = function(error) {
                console.error("STOMP连接失败:" + error);
                appendMessage("连接失败,请重试!");
            };
        }

        // 发送消息
        function sendMessage() {
            const input = document.getElementById("messageInput");
            const content = input.value.trim();
            const username = document.getElementById("usernameInput").value.trim();
            if (content && stompClient) {
                // 发送消息到/app/chat目的地(对应控制器的@MessageMapping("/chat"))
                stompClient.send("/app/chat", {}, `${username}:${content}`);
                input.value = "";
            }
        }

        // 追加消息到页面
        function appendMessage(content) {
            const container = document.getElementById("messageContainer");
            const div = document.createElement("div");
            div.textContent = content;
            container.appendChild(div);
            // 滚动到底部
            container.scrollTop = container.scrollHeight;
        }
    </script>
</body>
</html>

步骤5:测试验证

  1. 启动Spring Boot项目;
  2. 将测试页面放入resources/static目录;
  3. 打开多个浏览器窗口,分别输入不同用户名并连接;
  4. 在任意窗口发送消息,所有连接的窗口均可收到广播消息,实现群聊功能。

四、进阶优化:权限控制与连接管理

1. 权限控制(集成Spring Security)

实际项目中,需对WebSocket连接进行权限验证(如仅登录用户可连接)。可通过Spring Security拦截WebSocket连接,结合Principal获取当前登录用户信息:

// 在STOMP控制器中获取当前登录用户
@MessageMapping("/chat")
@SendTo("/topic/public")
public ChatMessage handleChatMessage(String message, Principal principal) {
    String username = principal.getName(); // 获取登录用户名
    // 后续逻辑...
}

2. 连接管理

可通过WebSocketSession(原生API)或SimpMessageHeaderAccessor(STOMP)管理连接状态,如统计在线人数、强制断开连接等。

3. 异常处理

添加全局异常处理器,处理消息发送失败、连接异常等场景,提升系统稳定性。

五、总结

本文介绍了Spring Boot中实现WebSocket的两种核心方式:基于原生API适用于简单点对点通信,配置简单、轻量;基于STOMP协议适用于复杂订阅/发布场景,提供标准化的消息机制,开发效率更高。实际项目中,可根据业务需求选择合适的实现方式。此外,还需关注权限控制、连接管理、兼容性等问题,确保实时通信功能的稳定可靠。

通过Spring Boot的封装,WebSocket开发变得简单高效,开发者无需关注底层协议细节,可专注于业务逻辑实现,为Web应用的实时化升级提供了便捷方案。


作 者:南烛
链 接:https://www.itnotes.top/archives/1291
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇