在 Spring Boot 项目中使用 WebSocket 实现实时通信

一、前言

在传统的 HTTP 通信中,客户端发起请求,服务器给出响应,一次通信就此结束。这种模式对于静态页面展示完全够用,但对于需要实时推送的场景------如在线聊天、实时通知、股票行情、设备状态监控------就力不从心了。

早期的解决方案是轮询:客户端每隔几秒就发一次请求问服务器"有新消息吗",效率低下且浪费资源。WebSocket 的出现彻底改变了这一局面。


二、WebSocket 是什么

WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议,由 HTML5 规范引入,RFC 6455 正式定义。

2.1 与 HTTP 的核心区别

对比项 HTTP WebSocket
通信方向 单向(客户端请求,服务器响应) 双向(任意一方均可主动发送)
连接状态 无状态,一问一答后断开 有状态,连接建立后持续保持
协议头开销 每次请求都携带完整 Header 握手一次后,后续帧头极小(2~10字节)
适用场景 普通页面请求 实时推送、聊天、游戏

2.2 握手过程

WebSocket 复用了 HTTP 的握手机制,通过一次 HTTP 请求完成协议升级:

复制代码
客户端发送升级请求:
GET /ws/chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手成功后,协议从 HTTP 升级为 WebSocket,后续通信不再经过 HTTP 层,直接在 TCP 连接上传输 WebSocket 帧。


三、Spring Boot 集成 WebSocket

Spring Boot 提供了两种方式使用 WebSocket:

  • 方式一 :基于 Java EE 标准的 @ServerEndpoint(本文重点介绍)
  • 方式二 :基于 Spring 的 WebSocketHandler

3.1 添加依赖

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

3.2 注册配置类

使用内置 Tomcat 时,需要手动注册 ServerEndpointExporter,让 Spring 能扫描到 @ServerEndpoint 注解:

复制代码
@Configuration
public class WebSocketConfig {

    /**
     * 使用内置 Tomcat 时必须注入此 Bean
     * 若使用外部 Tomcat 部署,则不需要,容器会自行管理
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3.3 编写 WebSocket 端点

复制代码
@Component
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocket {

    private static final Logger log = LoggerFactory.getLogger(ChatWebSocket.class);

    // 存储所有在线连接,key=userId,value=WebSocket会话
    private static final ConcurrentHashMap<String, Session> SESSIONS = new ConcurrentHashMap<>();

    /**
     * 连接建立时触发
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        SESSIONS.put(userId, session);
        log.info("用户 {} 上线,当前在线人数:{}", userId, SESSIONS.size());
    }

    /**
     * 收到客户端消息时触发
     */
    @OnMessage
    public void onMessage(String message, @PathParam("userId") String userId) {
        log.info("收到用户 {} 的消息:{}", userId, message);
        // 解析消息,转发给目标用户
        handleMessage(userId, message);
    }

    /**
     * 连接关闭时触发
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId) {
        SESSIONS.remove(userId);
        log.info("用户 {} 下线,当前在线人数:{}", userId, SESSIONS.size());
    }

    /**
     * 发生异常时触发
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket 发生异常:{}", error.getMessage());
    }

    /**
     * 向指定用户发送消息
     */
    public static void sendToUser(String userId, String message) {
        Session session = SESSIONS.get(userId);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("向用户 {} 发送消息失败:{}", userId, e.getMessage());
            }
        }
    }

    /**
     * 广播消息给所有在线用户
     */
    public static void broadcast(String message) {
        SESSIONS.forEach((userId, session) -> {
            if (session.isOpen()) {
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    log.error("广播消息失败,userId={}:{}", userId, e.getMessage());
                }
            }
        });
    }

    /**
     * 处理消息逻辑(示例:解析JSON转发)
     */
    private void handleMessage(String fromUserId, String message) {
        try {
            // 假设消息格式为 JSON:{"toUserId":"xxx","content":"hello"}
            JSONObject json = JSONObject.parseObject(message);
            String toUserId = json.getString("toUserId");
            String content = json.getString("content");

            JSONObject response = new JSONObject();
            response.put("fromUserId", fromUserId);
            response.put("content", content);
            response.put("time", System.currentTimeMillis());

            sendToUser(toUserId, response.toJSONString());
        } catch (Exception e) {
            log.error("消息处理失败:{}", e.getMessage());
        }
    }
}

3.4 在业务代码中主动推送

WebSocket 不只能被动接收消息,也可以在任意业务逻辑中主动向客户端推送:

复制代码
@Service
public class OrderService {

    public void completeOrder(Long orderId, Long userId) {
        // 处理订单完成逻辑...

        // 订单完成后,主动推送通知给用户
        JSONObject notify = new JSONObject();
        notify.put("type", "ORDER_COMPLETE");
        notify.put("orderId", orderId);
        notify.put("message", "您的订单已完成,请及时查看");

        ChatWebSocket.sendToUser(String.valueOf(userId), notify.toJSONString());
    }
}

四、前端对接

复制代码
<!DOCTYPE html>
<html>
<body>
<script>
    const userId = "user_001";
    const ws = new WebSocket(`ws://localhost:8080/ws/chat/${userId}`);

    // 连接建立
    ws.onopen = function () {
        console.log("WebSocket 连接成功");
        ws.send(JSON.stringify({
            toUserId: "user_002",
            content: "你好!"
        }));
    };

    // 收到消息
    ws.onmessage = function (event) {
        const data = JSON.parse(event.data);
        console.log(`收到来自 ${data.fromUserId} 的消息:${data.content}`);
    };

    // 连接关闭
    ws.onclose = function () {
        console.log("WebSocket 连接已关闭");
    };

    // 发生错误
    ws.onerror = function (error) {
        console.error("WebSocket 错误:", error);
    };
</script>
</body>
</html>

五、生产环境注意事项

5.1 @ServerEndpoint 无法注入 Spring Bean 的问题

@ServerEndpoint 的实例由 Tomcat 管理,不是 Spring Bean,所以直接用 @Autowired 注入会失败:

复制代码
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocket {

    @Autowired
    private UserService userService;  // ❌ 注入失败,值为 null
}

解决方案:通过静态变量 + ApplicationContext 获取

复制代码
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocket implements ApplicationContextAware {

    private static UserService userService;

    @Override
    public void setApplicationContext(ApplicationContext context) {
        userService = context.getBean(UserService.class);
    }
}

或者更简洁地,在配置类里提前拿到:

复制代码
@Component
public class WebSocketBeanFactory implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }
}

// 在 WebSocket 端点里使用
UserService userService = WebSocketBeanFactory.getBean(UserService.class);

5.2 集群部署下的消息广播问题

单机部署时,所有连接都在同一个 JVM 里,ConcurrentHashMap 存储会话没问题。但集群部署时,用户 A 连接在节点1,用户 B 连接在节点2,节点1无法直接找到用户 B 的 Session。

解决方案:引入消息中间件(如 RabbitMQ / Redis Pub-Sub)

复制代码
节点1收到消息
    ↓
发布到 MQ / Redis Channel
    ↓
所有节点订阅并消费消息
    ↓
各节点检查自己管理的 Session 里有没有目标用户
    ↓
有则发送,无则忽略

5.3 连接心跳保活

长时间无数据交换时,防火墙或负载均衡器可能会断开连接,需要定期发送心跳:

复制代码
// 前端每 30 秒发送一次心跳
setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: "PING" }));
    }
}, 30000);

// 后端收到心跳回复
@OnMessage
public void onMessage(String message, Session session) {
    JSONObject json = JSONObject.parseObject(message);
    if ("PING".equals(json.getString("type"))) {
        try {
            session.getBasicRemote().sendText("{\"type\":\"PONG\"}");
        } catch (IOException e) {
            log.error("心跳回复失败");
        }
    }
}

5.4 连接断开重连

网络抖动时前端需要自动重连:

复制代码
function createWebSocket(userId) {
    const ws = new WebSocket(`ws://localhost:8080/ws/chat/${userId}`);

    ws.onclose = function () {
        console.log("连接断开,3秒后重连...");
        setTimeout(() => createWebSocket(userId), 3000);
    };

    return ws;
}

六、完整流程总结

复制代码
1. 引入 spring-boot-starter-websocket 依赖
         ↓
2. 注册 ServerEndpointExporter Bean(内置Tomcat必须)
         ↓
3. 编写 @ServerEndpoint 端点类
   实现 @OnOpen / @OnMessage / @OnClose / @OnError
         ↓
4. 用 ConcurrentHashMap 管理所有在线 Session
         ↓
5. 业务代码调用静态方法主动推送消息
         ↓
6. 前端用 new WebSocket(url) 建立连接
   通过 onmessage 接收,ws.send() 发送

七、适用场景总结

场景 说明
在线聊天 用户之间实时发送消息
实时通知 订单完成、充值到账、审批结果
设备监控 硬件设备实时上报状态数据
协同编辑 多人同时编辑同一文档
行情推送 股票、加密货币实时价格
在线游戏 多人游戏实时同步状态

WebSocket 适合高频、双向、实时 的通信场景。如果只是服务器单向推送(如消息通知),也可以考虑更轻量的 SSE(Server-Sent Events);如果实时性要求不高,简单的短轮询也能满足需求。选择合适的技术,比盲目使用 WebSocket 更重要。

相关推荐
Sean‘2 小时前
Rancher 日志无法显示?WebSocket 代理配置是罪魁祸首
websocket·网络协议·rancher
曲幽21 小时前
FastAPI实战:WebSocket vs Socket.IO,这回真给我整明白了!
python·websocket·nginx·socket·fastapi·web·async·socketio
Densen20141 天前
发布blazor应用到Linux, 使用nginx作为WebSocket代理
linux·websocket·nginx
花月C1 天前
基于WebSocket的 “聊天” 业务设计与实战指南
java·网络·后端·websocket·网络协议
tyung1 天前
用 zhenyi-base 做一个带网页的群聊 Demo
websocket·go
带娃的IT创业者2 天前
工具状态失踪之谜:EventBus事件漏接与asyncio.Lock并发陷阱双线诊断
qt·websocket·并发控制·eventbus·事件驱动架构·pwa·asyncio.lock
特立独行的猫a2 天前
ESP32小智AI的WebSocket 调试工具实现,小智AI后台交互过程揭秘(一、开篇介绍 )
人工智能·websocket·网络协议·esp32·小智ai
特立独行的猫a2 天前
ESP32小智AI的WebSocket 调试工具的实现,小智AI后台交互过程揭秘(二、技术原理与实现过程详解 )
人工智能·websocket·网络协议·esp32·调试工具·小智ai
带娃的IT创业者3 天前
WeClaw 日志分析实战:如何从海量日志中快速定位根因?
运维·python·websocket·jenkins·fastapi·架构设计·实时通信