WebSocket 与实时通信
聊天、在线协作、实时通知、行情推送……这些场景的共同点是服务器需要主动向客户端推送数据。传统 HTTP 请求-响应模型难以高效满足这种需求,WebSocket 应运而生。本文讲解 WebSocket 的协议特点、浏览器端 API 用法、心跳与重连机制,并对比轮询、SSE 等替代方案。
一、为什么需要 WebSocket
HTTP 是单向、半双工的:客户端发请求,服务器返回响应,一次请求对应一次响应,连接随之结束。服务器无法主动推送,想"实时"只能靠客户端反复询问:
- 短轮询:客户端每隔几秒发起一次请求,大量无效请求浪费带宽。
- 长轮询:请求挂起直到服务器有新数据才返回,随后客户端再发起新请求,实现接近实时的推送,但连接频繁重建、服务器压力大。
WebSocket 则在一次 HTTP 升级握手后建立长连接,双方可随时互相发送数据,延迟从"秒级"降到"毫秒级"。
二、WebSocket 协议特点
| 特点 | 说明 |
|---|---|
| 全双工 | 客户端与服务器可同时双向收发,互不等待 |
| 持久连接 | 一次握手建立连接,持续复用,无需重复建连 |
| 与 HTTP 的关系 | 借道 HTTP 的 Upgrade 握手升级而来,之后走独立的 WS 协议(80 端口)/ WSS(443 端口) |
| 数据帧 | 二进制帧格式,开销小,支持文本与二进制消息 |
| URL 格式 | ws:// 与 wss://(TLS 加密版),如 wss://example.com/socket |
握手过程
客户端先发一个普通的 HTTP 升级请求:
GET /socket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13服务器返回 101 Switching Protocols 表示升级成功:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=此后双方就通过这条 TCP 连接收发帧数据,不再走 HTTP 语义。
三、创建连接与事件
基本使用
const ws = new WebSocket("wss://example.com/socket");
ws.onopen = () => {
console.log("连接已建立");
ws.send("hello server");
};
ws.onmessage = (event) => {
console.log("收到消息:", event.data);
};
ws.onclose = (event) => {
console.log("连接关闭,code =", event.code, "reason =", event.reason);
};
ws.onerror = (err) => {
console.error("连接出错", err);
};readyState 状态
ws.readyState 表示当前连接状态:
| 常量 | 值 | 含义 |
|---|---|---|
CONNECTING | 0 | 正在建立连接 |
OPEN | 1 | 已连接,可收发数据 |
CLOSING | 2 | 正在关闭 |
CLOSED | 3 | 已关闭或连接失败 |
if (ws.readyState === WebSocket.OPEN) {
ws.send("可以发送");
}连接失败的表现
服务器不存在、网络中断、握手被拒绝(如 CORS 限制、路径错误)都会触发 onerror,随后一般会进入 CLOSED 状态并触发 onclose。浏览器对 ws 与 wss 混合也有要求:HTTPS 页面只能使用 wss://。
四、发送与接收消息
文本与二进制
// 发送文本
ws.send("普通字符串");
// 发送 JSON(最常用的消息格式)
ws.send(JSON.stringify({ type: "chat", content: "你好" }));
// 发送二进制数据(ArrayBuffer / Blob / TypedArray)
const buffer = new Uint8Array([1, 2, 3, 4]);
ws.send(buffer.buffer);接收端可通过 event.data 的类型判断消息形式:
| 消息类型 | event.data 的形态 |
|---|---|
| 文本消息 | string |
| 二进制消息(默认) | Blob |
| 二进制消息(设置后) | ArrayBuffer |
ws.binaryType = "arraybuffer"; // 指定二进制以 ArrayBuffer 接收
ws.onmessage = (event) => {
if (typeof event.data === "string") {
const msg = JSON.parse(event.data); // 解析 JSON 文本
handleText(msg);
} else if (event.data instanceof ArrayBuffer) {
const bytes = new Uint8Array(event.data); // 处理二进制
handleBinary(bytes);
}
};消息协议设计
实际项目通常约定统一的消息信封,通过 type 字段区分业务类型:
// 发送
ws.send(JSON.stringify({
type: "ping",
ts: Date.now(),
}));
// 接收时按 type 分发
function handleMessage(raw) {
const { type, data } = JSON.parse(raw);
switch (type) {
case "chat": renderChat(data); break;
case "notice": showNotice(data); break;
case "typing": showTyping(data); break;
}
}五、心跳保活机制
为什么需要心跳
许多代理服务器(Nginx 等)会回收长时间空闲的 TCP 连接,网络设备也可能静默切断空闲链路,导致连接"假死":客户端以为连接还在,实际早已断开。心跳就是在空闲期主动发送小数据包,让链路保持活跃,同时探测连接是否真的健康。
实现思路
WebSocket 协议本身有 ping / pong 控制帧,但浏览器端 API 不暴露发送 ping 的能力,通常的做法是应用层自己发约定消息:
const HEARTBEAT_INTERVAL = 30000; // 每 30 秒心跳一次
let heartbeatTimer = null;
let alive = true;
// 收到任何消息都视为连接存活
ws.onmessage = (event) => {
alive = true;
// ...业务处理
};
// 启动心跳
function startHeartbeat() {
stopHeartbeat();
heartbeatTimer = setInterval(() => {
if (ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: "ping", ts: Date.now() }));
// 若超过超时时间仍未收到 pong,判定连接失效
setTimeout(() => {
if (!alive) {
ws.close();
// 触发重连逻辑
}
alive = false;
}, 5000);
}, HEARTBEAT_INTERVAL);
}
function stopHeartbeat() {
clearInterval(heartbeatTimer);
}服务端收到 ping 后回 pong,双向确认链路通畅;连续多次心跳无响应即主动重连。
六、断线重连策略
指数退避(Exponential Backoff)
网络抖动导致的瞬时断线很常见,立即重连可能加剧服务器压力。指数退避让重连间隔随时间指数增长:1s、2s、4s、8s……直到上限,并加入随机抖动避免大量客户端同时重连(惊群效应):
class ReconnectSocket {
constructor(url, { maxAttempts = 10, maxDelay = 30000 } = {}) {
this.url = url;
this.maxAttempts = maxAttempts;
this.maxDelay = maxDelay;
this.attempts = 0;
this.ws = null;
this.reconnectTimer = null;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.attempts = 0; // 连接成功后重置计数
console.log("连接成功");
};
this.ws.onclose = () => this.scheduleReconnect();
this.ws.onerror = () => this.ws.close();
}
scheduleReconnect() {
if (this.attempts >= this.maxAttempts) {
console.log("重连次数已达上限,放弃重连");
return;
}
const base = Math.min(Math.pow(2, this.attempts) * 1000, this.maxDelay);
const jitter = Math.random() * 1000; // 随机抖动
const delay = base + jitter;
this.attempts++;
console.log(`第 ${this.attempts} 次重连,${Math.round(delay)}ms 后开始`);
this.reconnectTimer = setTimeout(() => this.connect(), delay);
}
close() {
clearTimeout(this.reconnectTimer);
this.ws && this.ws.close();
}
}
const client = new ReconnectSocket("wss://example.com/socket");
client.connect();重连要点
| 要点 | 说明 |
|---|---|
| 重连上限 | 限制最大尝试次数,避免无限循环消耗资源 |
| 指数退避 | 间隔递增,保护服务器 |
| 随机抖动 | 分散重连时间点,防止集体重连 |
| 消息补发 | 重连后需要重新同步状态(重新订阅房间、拉取离线消息) |
| 可见性控制 | 页面不可见时可暂停重连,回到前台再恢复 |
七、与轮询 / 长轮询 / SSE 对比
| 对比项 | 短轮询 | 长轮询 | SSE | WebSocket |
|---|---|---|---|---|
| 通信方向 | 单向(客户端请求) | 单向 | 服务器→客户端单向 | 全双工 |
| 实时性 | 取决于轮询间隔 | 接近实时 | 实时 | 实时 |
| 连接数 | 反复建连 | 反复建连 | 一条长连接 | 一条长连接 |
| 传输格式 | 任意 | 任意 | 仅文本(可拼二进制) | 文本 + 二进制 |
| 自动重连 | 天然支持 | 需自己实现 | 内置 | 需自己实现 |
| 服务端复杂度 | 低 | 中 | 中 | 高 |
| 浏览器支持 | 全 | 全 | 现代浏览器 | 现代浏览器 |
| 适用场景 | 低频、数据量小的更新 | 历史方案 | 新闻推送、行情播报 | 聊天、游戏、协作编辑 |
选型建议:只需服务器单向推送文本且想省事,优先 SSE;需要双向实时交互(聊天、游戏、协同)则选 WebSocket;数据更新频率很低时,简单的轮询也够用。
八、应用场景
| 场景 | 为什么需要 WebSocket |
|---|---|
| 在线聊天 | 消息需实时双向送达,Typing 状态也要即时广播 |
| 实时通知 | 订单状态、系统告警即时推送给客户端 |
| 协同编辑 | 多人的光标位置、编辑操作需要低延迟同步 |
| 在线游戏 | 高频双向状态同步(位置、操作) |
| 实时行情 | 股票、加密货币价格秒级推送 |
| 物联网 | 设备状态上报与远程控制指令下发 |
九、服务端实现简介(Node.js ws 库)
浏览器端使用原生 API,服务端则需要第三方实现。Node.js 生态最常用的是 ws 库(不是 WebSocket 内置对象,后者 Node 21+ 才提供实验性支持):
npm install ws// server.js
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws, req) => {
console.log("客户端已连接:", req.socket.remoteAddress);
// 收到消息:回显并广播给所有客户端
ws.on("message", (data) => {
const text = data.toString();
console.log("收到:", text);
// 广播给所有连接的客户端
wss.clients.forEach((client) => {
if (client.readyState === ws.OPEN) {
client.send(`广播: ${text}`);
}
});
});
// 发送欢迎消息
ws.send(JSON.stringify({ type: "welcome", content: "连接成功" }));
ws.on("close", () => console.log("客户端断开"));
});
console.log("WebSocket 服务运行在 ws://localhost:8080");// client.js(浏览器端或 Node 端均可)
const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => ws.send("大家好");
ws.onmessage = (event) => console.log(event.data);生产环境中还要考虑:Nginx 反代时配置 Upgrade 与 Connection 头转发、跨域校验(Origin)、鉴权(握手时校验 token)、消息压缩(permessage-deflate)与集群下的消息路由等。
掌握 WebSocket 的握手、事件、心跳与重连机制,配合合理的消息协议,就能构建出稳定、实时的在线应用。