WebSocket 实战学习文档
更新: 6/6/2026 字数: 0 字 时长: 0 分钟
面向前端 / 全栈开发者的科普向技术解读。本文把 WebSocket 是什么、原理、前后端怎么用、以及如何用心跳重连让连接更稳定,讲清楚、讲通透。
一、WebSocket 是什么:从"一问一答"到"双向实时"

WebSocket 是一种网络通信协议,它能在单个 TCP 连接上实现全双工(双向同时)通信。 它在 2011 年被 IETF 定为标准 RFC 6455。
要理解它的价值,先看传统 HTTP 的局限:
- 传统 HTTP 是"一问一答":必须客户端先提问,服务器才能回答。服务器无法主动把消息推给客户端。想做实时聊天?只能让前端不停轮询"有新消息吗?有新消息吗?"——既费流量又有延迟。
- WebSocket 是"双向实时通话":连接一旦建立,就像架起了一条始终在线的双向管道,客户端和服务器都能随时主动发消息,无需对方先开口。
它的三个核心特点:
| 特点 | 含义 |
|---|---|
| 双向实时通讯 | 服务器可主动向客户端推送数据,不必等客户端请求 |
| 低延迟 | 省去了 HTTP 每次请求都要重新建立连接的开销,延迟远低于轮询 |
| 长连接 | 连接建立后持续保持,直到任一方主动关闭 |
通俗类比:HTTP 像发短信——你发一条、对方才回一条;WebSocket 像打电话——接通后双方随时都能说话。
典型应用场景:实时聊天、在线游戏、股票/数据实时推送、协同编辑、扫码登录、监控大屏等一切需要"实时"的场景。
二、工作原理:握手、数据交换、关闭三阶段

一次完整的 WebSocket 通信分为三个阶段:
1. 握手(建立连接) 客户端先用一个普通的 HTTP 请求向服务器发起连接,但这个请求带了一个特殊的 Upgrade 头部,意思是"请把这条连接升级成 WebSocket"。服务器确认后回应,握手完成——连接就从 HTTP 升级成了 WebSocket。
为什么要借 HTTP 起步?因为这样能复用现有的端口(80/443)和基础设施,更容易穿过防火墙和代理。
2. 数据交换 握手完成后,双方就能自由收发数据了。数据会被封装成一个个**帧(frame)**进行传输——帧是 WebSocket 里传输数据的基本单位,这种结构便于解析、也便于区分消息边界。
3. 关闭连接 任意一方都可以发送一个特定的关闭帧来结束连接,收到关闭帧的一方随即关闭。连接就此优雅地断开。
①握手 ②数据交换 ③关闭
HTTP Upgrade → ⇄ 帧 ⇄ 帧 ⇄ 帧 ⇄ → 关闭帧 → 断开
(升级为 WS) (双向自由收发) (任一方发起)三、服务端实现:Spring Boot 整合 WebSocket

下面以一个聊天室为例,演示后端如何用 Spring Boot 搭起 WebSocket 服务。核心思路:用一张「在线连接表」记录所有连接,靠四个生命周期事件(上线、收消息、下线、异常)来管理它们。
① 引入依赖(pom.xml)
<!-- Spring WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>② 配置类:注册 ServerEndpointExporter 以启用 WebSocket 端点
/**
* 开启 WebSocket 支持
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}③ 工具类:用一个线程安全的 Map 维护「用户 ID → 会话」的映射,并封装收发消息的方法
public class WebsocketUtil {
/** 记录当前在线的 Session(key 为 userId) */
private static final Map<String, Session> ONLINE_SESSION = new ConcurrentHashMap<>();
/** 添加 session:同一用户只保留一个连接,多余连接视为无效 */
public static void addSession(String userId, Session session) {
ONLINE_SESSION.putIfAbsent(userId, session);
}
/** 关闭 session */
public static void removeSession(String userId) {
ONLINE_SESSION.remove(userId);
}
/** 给单个用户推送消息(异步发送) */
public static void sendMessage(Session session, String message) {
if (session == null) {
return;
}
RemoteEndpoint.Async async = session.getAsyncRemote();
async.sendText(message);
}
/** 向所有在线用户广播消息 */
public static void sendMessageForAll(String message) {
ONLINE_SESSION.forEach((sessionId, session) -> sendMessage(session, message));
}
}④ 端点处理类:这是 WebSocket 真正的请求地址,userId 相当于"房间名",用户按房间名进入并监听消息
@Component
@ServerEndpoint(value = "/chat/{userId}")
public class WebsocketController {
/** 连接建立:用户上线 */
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
// 把当前连接登记到在线表
WebsocketUtil.addSession(userId, session);
// 如需广播上线通知,可放开下行
// WebsocketUtil.sendMessageForAll("[" + userId + "]加入聊天室!");
}
/** 连接关闭:用户下线 */
@OnClose
public void onClose(@PathParam("userId") String userId, Session session) {
WebsocketUtil.removeSession(userId);
WebsocketUtil.sendMessageForAll("[" + userId + "]退出了聊天室...");
}
/** 收到用户消息:直接广播给所有人 */
@OnMessage
public void onMessage(@PathParam("userId") String userId, Session session, String message) {
String msg = "[" + userId + "]:" + message;
System.out.println("接收到信息:" + msg);
WebsocketUtil.sendMessageForAll(msg);
}
/** 处理连接异常:关闭并打印堆栈 */
@OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
throwable.printStackTrace();
}
}⑤ 用 HTTP 接口给指定用户推消息 有时消息源不在 WebSocket 这一侧。比如扫码登录:手机端通过普通 HTTP 通知后端登录成功,而展示二维码、监听结果的是网页端(两者是分开的)。此时就需要一个 HTTP 接口,按房间 ID 找到对应连接并推送:
@PostMapping("/send")
public void send(@RequestParam("id") String id, @RequestParam("message") String message) {
Session session = ONLINE_SESSION.get(id);
WebsocketUtil.sendMessage(session, message);
System.out.println("发送成功");
}对比一下:聊天室这种场景,前端直接
socket.send()就能把消息发给服务器;而扫码登录因为收/显分离在不同设备,所以触发推送的一端走 HTTP。本节代码思路参考自掘金社区文章[1]。
四、前端使用:几行代码连上服务器

浏览器原生就内置了 WebSocket 对象,前端只需关注四个关键点:建立连接、监听消息、监听关闭、发送消息。
// ws:// 是 WebSocket 协议,路径里的 1 即后端的 userId(房间名)
const socket = new WebSocket('ws://localhost:8888/chat/1');
// 连接成功打开
socket.onopen = (event) => {
console.log('WebSocket 连接已建立:', event);
};
// 接收后端推来的消息
socket.onmessage = (event) => {
const data = event.data;
console.log('收到消息:', data);
};
// 连接关闭
socket.onclose = (event) => {
console.log('WebSocket 连接已关闭:', event);
};
// 主动发送消息到后端
function sendMessage(message) {
socket.send(message);
}| 事件/方法 | 作用 |
|---|---|
new WebSocket(url) | 发起连接,ws://(明文)或 wss://(加密,对应 HTTPS) |
onopen | 连接建立成功时触发 |
onmessage | 收到服务器消息时触发,数据在 event.data 里 |
onclose | 连接关闭时触发 |
send() | 主动向服务器发送数据 |
五、心跳与重连:让连接"始终在线"

1. WebSocket 的"不稳定性"
想象你和朋友打电话,说着说着信号断了,但双方都没收到提示。你继续说,对方却听不见——沟通失效了。WebSocket 也有这个问题:网络波动时连接可能悄悄断开,而客户端和服务器都浑然不觉。
2. 什么是心跳包?
心跳包就是定时发送的"小问候",用来确认双方还活着、连接还在。 比如客户端每隔 5 秒发一句"我还在哦~"(即 ping),服务器回一句"我也在~"(即 pong);如果连续几次收不到回复,就判定连接已断。
心跳包的三大作用:① 检测连接是否存活;② 防止长时间不通信被防火墙/代理切断;③ 像定期体检一样监控连接状态。
3. 关键参数怎么定
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 心跳间隔 | 5–30 秒 | 太短浪费流量,太长不能及时发现断开 |
| 超时时间 | 心跳间隔的 2–3 倍 | 如间隔 5 秒,超时设 15 秒 |
| 重连策略 | 指数退避 | 等待时间逐次翻倍(3→6→12 秒),避免雪崩式重连 |
4. 常见问题与对策
- 心跳被防火墙拦截 → 改由服务器主动发心跳(反向心跳),或调整间隔避开防火墙规则。
- 频繁重连拖垮性能 → 采用指数退避 + 设置最大重连次数。
- 重连后状态丢失 → 重连成功后补发未完成的请求,并用
localStorage等缓存关键状态。
5. 完整封装示例
下面这个 WebSocketClient 类把"连接 + 心跳 + 重连"封装到一起,可直接复用:
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.heartBeatTimer = null;
this.reconnectTimer = null;
this.heartBeatInterval = 5000; // 心跳间隔 5 秒
this.reconnectInterval = 3000; // 重连间隔 3 秒
this.maxReconnectTimes = 10; // 最多重连 10 次
this.currentReconnectTimes = 0;
this.isConnecting = false;
this.connect();
}
connect() {
if (this.isConnecting) return;
this.isConnecting = true;
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 连接已建立');
this.isConnecting = false;
this.currentReconnectTimes = 0; // 重置重连计数
this.startHeartBeat(); // 启动心跳
};
this.ws.onmessage = (event) => {
if (event.data === 'pong') {
console.log('收到心跳响应'); // 连接正常
} else {
this.onMessage(event.data); // 处理业务消息
}
};
this.ws.onclose = () => {
console.log('WebSocket 连接已关闭');
this.stopHeartBeat();
if (!this.isConnecting) {
this.scheduleReconnect(); // 安排重连
}
};
this.ws.onerror = (error) => {
console.error('WebSocket 发生错误:', error);
this.ws.close();
};
}
startHeartBeat() {
this.stopHeartBeat();
this.heartBeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send('ping'); // 定时发心跳
}
}, this.heartBeatInterval);
}
stopHeartBeat() {
if (this.heartBeatTimer) {
clearInterval(this.heartBeatTimer);
this.heartBeatTimer = null;
}
}
scheduleReconnect() {
if (this.currentReconnectTimes >= this.maxReconnectTimes) {
console.log('已达到最大重连次数,停止重连');
return;
}
this.currentReconnectTimes++;
console.log(`准备第 ${this.currentReconnectTimes} 次重连,${this.reconnectInterval / 1000} 秒后...`);
this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectInterval);
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
console.log('WebSocket 未连接,消息发送失败');
// 可选:缓存消息,重连后补发
}
}
onMessage(message) {
// 子类可重写此方法处理业务消息
console.log('收到消息:', message);
}
close() {
this.stopHeartBeat();
clearTimeout(this.reconnectTimer);
this.isConnecting = false;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
// 使用示例
const client = new WebSocketClient('ws://example.com');
client.onMessage = (message) => console.log('处理业务消息:', message);
client.send('Hello, server!');6. 一句话记住心跳重连
就像两个人约定每隔几分钟说一句"我还在";连续几次没回应就当对方失联,于是尝试重新联系;一次打不通就等一会儿再打,等待时间逐次拉长;打满 10 次还不通就暂时放弃。这套机制大幅提升了实时聊天、监控系统、在线游戏等长连接场景的稳定性。
一页速记
- WebSocket:单 TCP 连接上的全双工协议,双向、低延迟、长连接,弥补 HTTP "一问一答"的局限。
- 三阶段:HTTP 握手升级 → 帧形式双向交换 → 任一方发关闭帧断开。
- 后端(Spring Boot):
@ServerEndpoint+ 四个事件(@OnOpen/@OnMessage/@OnClose/@OnError),用在线表管理连接、广播消息。 - 前端:
new WebSocket()+onopen/onmessage/onclose/send()四件套。 - 心跳重连:定时 ping/pong 探活 + 指数退避自动重连 + 最大次数限制,是长连接稳定的关键。