WebSocket 实时通信
本教程共 48 篇 · 第 25 篇 · 更新于 2026-08-13 · 约 3 分钟阅读
本节目标:理解 WebSocket 相比 HTTP 轮询的优势,会用 STOMP 搭建服务端推送,让浏览器实时收到消息。
为什么需要 WebSocket
HTTP 是「一问一答」的协议。客户端发请求,服务端给响应,连接就结束了。服务端想主动通知客户端,做不到。
早期解决方案是轮询:前端定时发请求问「有消息了吗」。
轮询的问题一眼可见:
- 大部分请求没有新数据,浪费带宽。
- 实时性取决于轮询间隔,间隔短就费资源。
- 服务端连接反复建立销毁,压力大。
长轮询改良了一点:服务端没数据就挂起连接,有数据再响应。但连接悬挂着,每个客户端占一个线程,并发一大就撑不住。
WebSocket 换了个思路:建立一条全双工的 TCP 连接,客户端和服务端随时互发消息,不再一问一答。
| 方式 | 实时性 | 资源消耗 | 适用 |
|---|---|---|---|
| 短轮询 | 差 | 高 | 低频简单场景 |
| 长轮询 | 中 | 中 | 老系统兼容 |
| WebSocket | 好 | 低 | 聊天、通知、看板 |
WebSocket 怎么建立连接
WebSocket 不走新协议,握手还是 HTTP。
浏览器发一个带 Upgrade 头的请求,服务端同意后,连接升级为 WebSocket,此后双方在一条连接上自由收发。
Spring Boot 对 WebSocket 支持两层:
- 底层
WebSocketHandler:处理原始文本/二进制消息。 - 上层 STOMP:在 WebSocket 之上定义消息格式和路由,推荐用这个。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
前端要连 SockJS 和 STOMP 的客户端库,用 WebJars 引:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.4</version>
</dependency>
配置消息代理
STOMP 把消息分成两类目的地:/topic 广播给所有订阅者,/app 发给服务端处理。
配置类:
package com.example.demo.config;
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;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 广播目的地前缀
registry.enableSimpleBroker("/topic");
// 客户端发消息的前缀
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 握手端点,SockJS 兜底
registry.addEndpoint("/ws").withSockJS();
}
}
Note旧教程常写继承 AbstractWebSocketMessageBrokerConfigurer,那个类已废弃。Spring Boot 3.x/4.x 直接实现 WebSocketMessageBrokerConfigurer 接口即可。
消息处理器:接收与广播
@MessageMapping 接收 /app 前缀下的消息,@SendTo 把结果广播到指定主题:
package com.example.demo.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class GreetingController {
// 客户端发到 /app/hello
@MessageMapping("/hello")
// 结果广播到 /topic/greetings
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) {
return new Greeting("你好," + message.getName() + "!");
}
}
两个消息模型:
package com.example.demo.model;
public class HelloMessage {
private String name;
public HelloMessage() {
}
public HelloMessage(String name) {
this.name = name;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
package com.example.demo.model;
public class Greeting {
private String content;
public Greeting() {
}
public Greeting(String content) {
this.content = content;
}
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
服务端主动推送也简单:注入 SimpMessagingTemplate,任何地方都能往主题发消息:
package com.example.demo.service;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@Service
public class NotifyService {
private final SimpMessagingTemplate messagingTemplate;
public NotifyService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// 业务代码里调用,全端广播
public void notifyAll(String content) {
messagingTemplate.convertAndSend("/topic/notices", content);
}
}
这解决了轮询的痛点:有新数据才推送,连接只有一条。
前端订阅示例
页面 src/main/resources/static/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 示例</title>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
<input id="name" placeholder="输入名字" />
<button onclick="send()">发送</button>
<ul id="messages"></ul>
<script>
var stompClient = null;
function connect() {
// 与后端 /ws 端点握手
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, function () {
// 订阅广播主题
stompClient.subscribe('/topic/greetings', function (msg) {
var li = document.createElement('li');
li.textContent = JSON.parse(msg.body).content;
document.getElementById('messages').appendChild(li);
});
});
}
function send() {
var name = document.getElementById('name').value;
// 发到 /app/hello
stompClient.send('/app/hello', {}, JSON.stringify({name: name}));
}
connect();
</script>
</body>
</html>
页面加载即连接,发送后所有打开页面的用户都能收到广播。
WarningSockJS 的
withSockJS()会在浏览器不支持原生 WebSocket 时降级走 HTTP 长轮询,兼容老浏览器。纯内网新项目可以去掉 SockJS,直连原生 WebSocket。
原始 WebSocketHandler
不想用 STOMP,也可以直接处理原始消息。实现 TextWebSocketHandler:
package com.example.demo.websocket;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Component
public class EchoHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
// 收到什么回什么
session.sendMessage(new TextMessage("收到:" + message.getPayload()));
}
}
用 WebSocketConfigurer 注册:
package com.example.demo.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.demo.websocket.EchoHandler;
@Configuration
@EnableWebSocket
public class RawWebSocketConfig implements WebSocketConfigurer {
private final EchoHandler echoHandler;
public RawWebSocketConfig(EchoHandler echoHandler) {
this.echoHandler = echoHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler, "/echo");
}
}
适合简单回声、小流量场景。消息要路由、要群发,还是 STOMP 省心。
本节小结
- 轮询费资源、实时性差,WebSocket 全双工连接是实时场景的正解。
- STOMP 基于 WebSocket 定义消息路由,Spring Boot 推荐用它。
- @MessageMapping 收消息,@SendTo 广播,SimpMessagingTemplate 主动推。
- 配置类实现 WebSocketMessageBrokerConfigurer,别再用废弃的抽象类。