在许多实时性要求较高的业务场景里,比如在线客服、股票行情推送、协同编辑、设备状态监控等,传统的 HTTP 请求轮询方式不仅浪费带宽,还无法做到服务端主动推送。WebSocket 协议在完成一次 HTTP 握手之后,会在客户端和服务端之间建立一条持久化的全双工通道,双方都可以随时发送数据,通信开销远低于轮询。本文将以 Spring Boot 为例,完整演示如何搭建一个可用的 WS 服务,并覆盖配置、编码、前端对接以及部署阶段的常见问题。

一、引入依赖并编写 WebSocket 配置
第一步是在 pom.xml 中加入 spring-boot-starter-websocket 依赖。这个 starter 内置了 Tomcat 对 WebSocket 的支持以及 Spring 封装好的相关类,不需要额外引入其他包。依赖坐标如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>接下来需要向容器注册一个 ServerEndpointExporter。这个 Bean 的作用是扫描项目中所有带 @ServerEndpoint 注解的类,并把它们注册到 WebSocket 容器中。很多人写完服务端代码却发现连接始终报 404,十有八九就是漏掉了这个配置。注意,如果应用最终打成 war 包部署到外置 Tomcat 中,这个 Bean 不需要注册,由容器自己管理,重复注册反而会报错。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}另外还要留意 URL 路径前缀问题。如果 application.yml 中配置了 server.servlet.context-path,WebSocket 的连接地址也需要带上这个前缀,否则前端会一直连不上,这是实际开发中非常高频的一个坑。
二、编写服务端端点与前端对接
Spring Boot 中实现 WS 服务端有两种主流方式:一是直接使用 JSR-356 规范的 @ServerEndpoint 注解,二是使用 Spring 自带的 WebSocketHandler。前者写法简洁,接近原生 Java WebSocket,适合简单场景;后者可以配合拦截器、更好的生命周期控制,适合复杂业务。先看 @ServerEndpoint 方式的完整代码:
@Component
@ServerEndpoint("/ws/{userId}")
public class WebSocketServer {
// 线程安全容器,保存每个客户端对应的会话
private static final ConcurrentHashMap<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
SESSION_MAP.put(userId, session);
System.out.println("用户 " + userId + " 已连接,当前在线数:" + SESSION_MAP.size());
}
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId) {
System.out.println("收到来自 " + userId + " 的消息:" + message);
// 收到消息后回显,也可以转发给其他用户
sendToUser(userId, "服务端已收到:" + message);
}
@OnClose
public void onClose(@PathParam("userId") String userId) {
SESSION_MAP.remove(userId);
System.out.println("用户 " + userId + " 已断开");
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 单发消息
public void sendToUser(String userId, String message) {
Session session = SESSION_MAP.get(userId);
if (session != null && session.isOpen()) {
// 异步发送,避免阻塞
session.getAsyncRemote().sendText(message);
}
}
// 广播消息
public void broadcast(String message) {
SESSION_MAP.values().forEach(s -> {
if (s.isOpen()) {
s.getAsyncRemote().sendText(message);
}
});
}
}这里有几个容易被忽略的细节。第一,@ServerEndpoint 标注的类默认不是单例的,每来一个连接容器都会创建一个新实例,所以成员变量不能跨连接共享状态,必须用 static 的 Map 保存会话,而且要选线程安全的容器。第二,发送消息优先使用 getAsyncRemote() 异步接口,getBasicRemote() 是同步阻塞的,在广播场景下容易拖慢整体吞吐。第三,@OnError 触发后通常会紧跟 @OnClose,在错误处理里记得做幂等清理,避免重复移除。
前端对接非常简单,浏览器原生支持 WebSocket 对象,几行代码就能连上:
const userId = '1001';
const socket = new WebSocket('ws://localhost:8080/ws/' + userId);
socket.onopen = function () {
console.log('连接建立成功');
socket.send('hello server');
};
socket.onmessage = function (event) {
console.log('收到服务端消息:' + event.data);
};
socket.onclose = function () {
console.log('连接已关闭');
};
// 发消息直接调用
function sendMsg(text) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(text);
}
}注意 ws 协议和 wss 的区别:生产环境走 HTTPS 时,前端必须把地址写成 wss:// 开头,否则浏览器会因为混合内容策略直接拦截连接。
三、常见问题排查与进阶方案
实际上线后会遇到不少环境层面的问题。第一个是 Nginx 反向代理。默认情况下 Nginx 不会转发 Upgrade 头,WS 握手会失败,需要单独配置 proxy_set_header Upgrade 和 Connection,并适当调大 read timeout,否则连接会在默认 60 秒后被断开:
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}第二个是心跳保活。浏览器和移动网络中,空闲的 WS 连接很容易被中间设备掐掉,建议客户端每 30 秒发一次 ping 文本,服务端收到后回 pong;如果连续几个周期没收到心跳,就主动清理 Session 释放资源。配合前端的重连机制(断开后指数退避重连),可以做到用户无感知的连接恢复。
第三个是水平扩展问题。应用部署多个实例时,Session 只存在于当前 JVM 中,用户 A 连在节点一,用户 B 连在节点二,A 想发消息给 B 就找不到会话了。常见解法有两种:一是引入 MQ 或 Redis 发布订阅做消息中转,由持有目标连接的节点执行真正下发;二是用网关按用户 ID 做一致性哈希,保证同一用户始终落在同一节点。前者实现成本低、通用性好,推荐优先考虑。
如果业务需要消息路由、订阅发布这类语义更丰富的模型,可以直接上 STOMP 子协议。Spring 对 STOMP 的支持非常完善,通过 @EnableWebSocketMessageBroker 开启后就能用 @MessageMapping 和 @SendTo 编写类似 MVC 的消息处理代码,客户端用 SockJS 兜底还能兼容不支持 WS 的老旧浏览器。对于简单的推送场景,本文介绍的裸 WebSocket 方案已经完全够用;对于复杂的 IM 系统,STOMP 加消息代理才是更合适的架构。
Spring BootWebSocketWS通信修改时间:2026-09-08 23:23:22