一、为什么需要 WebSocket
传统的 HTTP 是「请求---响应」模型:客户端不发请求,服务端就无法主动推送数据。早期实现实时效果通常靠轮询 或长轮询,前者会产生大量无效请求,后者会长期占用连接,两者在消息延迟和服务器资源上都存在明显浪费。
WebSocket 在单个 TCP 连接上提供全双工 通信能力:连接建立后,客户端和服务端都可以随时主动发送消息,且握手阶段复用 HTTP 协议(通过 Upgrade: websocket 升级),因此能很好地穿透代理和防火墙。它的典型特征是:
- 低延迟:消息到达即可推送,无需等待下一次轮询。
- 低开销:握手完成后数据帧头部很小,不像 HTTP 每次都要携带完整请求头。
- 长连接:一次连接可承载大量双向消息,适合高频交互场景。
- 服务端主动推送:这是它相对 HTTP 最核心的优势。
二、WebSocket 的典型使用场景
2.1 即时通讯与聊天室
单聊、群聊、客服系统是最经典的场景。用户 A 发送消息后,服务端需要立刻把消息推送给用户 B,而不是让 B 不断轮询接口。WebSocket 天然适合这种「一方发送、多方接收」的广播模型。
2.2 实时通知与消息中心
站内信、系统公告、订单状态变更、审批流转提醒等,都要求「事件发生即通知」。相比轮询,WebSocket 可以把服务端的业务事件直接推送到浏览器,用户无需刷新页面即可看到红点或弹窗。
2.3 实时数据大屏与行情推送
股票行情、数字货币价格、监控指标、物流轨迹等场景数据变化频繁。用 WebSocket 推送增量数据,既能保证秒级甚至毫秒级更新,又能避免高频轮询给后端带来的压力。
2.4 协同编辑与在线文档
多人同时编辑同一份文档时,需要把光标位置、文本变更等操作实时同步给其他协作者。WebSocket 的低延迟双向通道是协同编辑(如 OT、CRDT 算法)的基础设施。
2.5 在线游戏与实时互动
棋牌、答题、直播弹幕、在线白板等场景需要玩家操作即时同步。WebSocket 可以承载高频的小数据包交互,配合心跳机制维持连接稳定。
2.6 任务进度与长耗时操作反馈
文件导入导出、视频转码、批量数据处理等耗时任务,可以通过 WebSocket 把进度百分比持续推送给前端,让用户看到实时进度条,而不是干等一个最终结果。
三、整体架构设计
下面以一个「实时消息推送」的最小可用系统为例,技术栈为 Spring Boot + Java WebSocket + Vue3。整体链路如下:
核心要点有三个:
- 连接管理:服务端需要保存每个用户的 WebSocket 会话,支持按用户 ID 定向推送。
- 消息协议:约定统一的消息体格式(如 type、from、content、timestamp),便于前后端解析和扩展。
- 连接保活:通过心跳检测及时发现断线,并配合前端自动重连。
四、Java 服务端实现
4.1 引入依赖
使用 Spring Boot 提供的 spring-boot-starter-websocket,它封装了 JSR-356 标准 API,使用起来非常简洁。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
4.2 注册 WebSocket 端点
通过 ServerEndpointExporter 把标注了 @ServerEndpoint 的类注册为 WebSocket 端点。
java
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();
}
}
4.3 编写 WebSocket 端点
下面实现一个支持「连接、断开、接收消息、定向推送」的端点。注意 @ServerEndpoint 的实例不是单例,每个连接都会创建一个新对象,因此会话集合必须用 static 或交给 Spring 单例 Bean 管理。
java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocketServer {
/** 在线会话:userId -> Session */
private static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>();
private static final ObjectMapper MAPPER = new ObjectMapper();
private String userId;
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.userId = userId;
SESSIONS.put(userId, session);
System.out.println("用户上线:" + userId + ",当前在线人数:" + SESSIONS.size());
}
@OnMessage
public void onMessage(String message, Session session) {
try {
ChatMessage msg = MAPPER.readValue(message, ChatMessage.class);
msg.setFrom(userId);
msg.setTimestamp(System.currentTimeMillis());
if (msg.getTo() == null || msg.getTo().isEmpty()) {
// 未指定接收人,广播给所有在线用户
broadcast(msg);
} else {
// 定向推送给指定用户
sendTo(msg.getTo(), msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
SESSIONS.remove(userId);
System.out.println("用户下线:" + userId + ",当前在线人数:" + SESSIONS.size());
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("连接异常:" + userId + "," + error.getMessage());
}
private void broadcast(ChatMessage msg) throws IOException {
String json = MAPPER.writeValueAsString(msg);
for (Session s : SESSIONS.values()) {
if (s.isOpen()) {
s.getBasicRemote().sendText(json);
}
}
}
private void sendTo(String to, ChatMessage msg) throws IOException {
Session target = SESSIONS.get(to);
if (target != null && target.isOpen()) {
target.getBasicRemote().sendText(MAPPER.writeValueAsString(msg));
}
}
}
4.4 消息实体
java
public class ChatMessage {
/** 消息类型:CHAT / NOTICE / HEARTBEAT */
private String type;
/** 发送者 */
private String from;
/** 接收者,为空表示广播 */
private String to;
/** 消息内容 */
private String content;
/** 时间戳 */
private Long timestamp;
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getFrom() { return from; }
public void setFrom(String from) { this.from = from; }
public String getTo() { return to; }
public void setTo(String to) { this.to = to; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
}
4.5 从业务代码主动推送
实际项目中,推送往往由业务事件触发,例如订单支付成功后通知用户。可以把会话管理抽成独立的单例 Bean,供任意业务代码调用。
java
import org.springframework.stereotype.Service;
import javax.websocket.Session;
import java.io.IOException;
@Service
public class PushService {
public void pushToUser(String userId, String json) {
Session session = ChatWebSocketServer.getSession(userId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(json);
} catch (IOException e) {
throw new RuntimeException("推送失败", e);
}
}
}
}
提示:如果服务端是多实例部署,单机的 Session 集合无法跨节点共享。此时需要引入 Redis 发布订阅、MQ 或专门的网关(如 Spring Cloud Gateway + WebSocket)来做消息路由。
五、Vue3 前端实现
5.1 封装 WebSocket 组合式函数
把连接、重连、心跳、消息分发封装成一个可复用的 useWebSocket,避免在组件里散落大量样板代码。
javascript
import { ref, onMounted, onUnmounted } from 'vue'
export function useWebSocket(url, options = {}) {
const { heartbeatInterval = 30000, reconnectDelay = 3000 } = options
const socket = ref(null)
const connected = ref(false)
const messages = ref([])
let heartbeatTimer = null
let reconnectTimer = null
let manualClose = false
const connect = () => {
socket.value = new WebSocket(url)
socket.value.onopen = () => {
connected.value = true
startHeartbeat()
}
socket.value.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (data.type === 'HEARTBEAT') return
messages.value.push(data)
} catch (e) {
console.warn('消息解析失败', event.data)
}
}
socket.value.onclose = () => {
connected.value = false
stopHeartbeat()
if (!manualClose) {
reconnectTimer = setTimeout(connect, reconnectDelay)
}
}
socket.value.onerror = () => {
socket.value && socket.value.close()
}
}
const startHeartbeat = () => {
stopHeartbeat()
heartbeatTimer = setInterval(() => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify({ type: 'HEARTBEAT' }))
}
}, heartbeatInterval)
}
const stopHeartbeat = () => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer)
heartbeatTimer = null
}
}
const send = (data) => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify(data))
return true
}
return false
}
const close = () => {
manualClose = true
stopHeartbeat()
clearTimeout(reconnectTimer)
socket.value && socket.value.close()
}
onMounted(connect)
onUnmounted(close)
return { socket, connected, messages, send, close }
}
5.2 在组件中使用
vue
<template>
<div class="chat">
<p>连接状态:{{ connected ? '已连接' : '未连接' }}</p>
<ul class="msg-list">
<li v-for="(msg, index) in messages" :key="index">
<strong>{{ msg.from }}</strong>:{{ msg.content }}
</li>
</ul>
<div class="input-area">
<input v-model="inputText" placeholder="输入消息" @keyup.enter="handleSend" />
<button @click="handleSend">发送</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'
const userId = '1001'
const inputText = ref('')
const { connected, messages, send } = useWebSocket(
`ws://localhost:8080/ws/chat/${userId}`
)
const handleSend = () => {
if (!inputText.value.trim()) return
const ok = send({
type: 'CHAT',
to: '',
content: inputText.value
})
if (ok) inputText.value = ''
}
</script>
<style scoped>
.chat { max-width: 600px; margin: 20px auto; }
.msg-list { height: 300px; overflow-y: auto; border: 1px solid #eee; padding: 10px; }
.input-area { display: flex; gap: 8px; margin-top: 10px; }
</style>
5.3 生产环境注意事项
- 协议选择 :HTTPS 页面必须使用
wss://,否则浏览器会拦截混合内容。 - 鉴权 :浏览器原生 WebSocket API 不支持自定义请求头,通常把 token 放在 URL 参数或握手后的首条消息中,服务端在
@OnOpen或拦截器中校验。 - 断线重连:网络抖动、服务重启都会导致连接断开,必须实现指数退避重连,避免瞬间大量重连压垮服务端。
- 心跳保活:Nginx 等代理默认会断开长时间无数据的连接,需要客户端定时发送心跳包。
- 消息幂等:网络重连可能导致消息重复,业务侧应通过消息 ID 做去重。
六、HTTP 与 WebSocket 选型对比
| 对比项 | HTTP 轮询 | WebSocket |
|---|---|---|
| 通信方向 | 单向(客户端发起) | 双向全双工 |
| 实时性 | 取决于轮询间隔 | 毫秒级推送 |
| 连接开销 | 每次请求都有完整头部 | 握手后帧头很小 |
| 服务端压力 | 高频轮询压力大 | 长连接,压力可控 |
| 适用场景 | 低频、非实时数据 | 聊天、通知、行情、协同 |
选型建议:如果业务对实时性要求不高(如每分钟同步一次配置),普通 HTTP 接口更简单;一旦涉及「服务端主动推送」和「高频双向交互」,WebSocket 才是更合适的方案。
七、总结
WebSocket 解决的是 HTTP 无法主动推送的问题,在聊天、通知、实时数据、协同编辑等场景中几乎是标配。落地时把握三个关键点:
- 服务端做好会话管理,支持定向推送与广播,并考虑多实例下的消息路由。
- 前端封装连接逻辑,统一处理心跳、重连和消息解析,避免组件内重复造轮子。
- 重视生产环境细节,包括 wss 协议、鉴权方式、心跳保活和消息幂等。
把上面的 Java 端点和 Vue3 组合式函数跑通后,就可以在此基础上扩展群组、离线消息、消息已读等更完整的业务能力。