服务端6月1日 09:19
WebSocket 连接管理有哪些关键实践?断线重连怎么做?WebSocket 连接管理围绕生命周期展开:建立、保活、重连、清理。建立时客户端发起连接,服务端做鉴权和元数据登记(用户 ID、设备信息),然后双方进入通信状态。保活用心跳机制——客户端定时发 ping,服务端回 pong,超时未收到 pong 则认为连接已死,这是检测"半开连接"(对端已断开但本端不知道)的唯一可靠手段。重连是客户端的职责,核心策略是指数退避:首次 1 秒后重试,之后 2s、4s、8s,上限 30s,避免服务端刚恢复就被雪崩式重连打垮。加随机抖动(jitter)防止大量客户端同时重连。清理是关闭连接后释放定时器、事件监听器、内存中的消息队列等资源,避免泄漏。
## 追问
### 指数退避为什么要加随机抖动?
如果 1000 个客户端同时断线,都在 1s、2s、4s 后重连,会出现"惊群效应"——服务端刚恢复就被第一波重连打垮,然后再次宕机,下一波重连又打垮,形成恶性循环。加 jitter(如 delay = baseDelay * 2^retry + Math.random() * 1000)让各客户端的重连时间错开,将瞬间冲击分散到时间窗口内。AWS 和 Google 的重连库都默认启用 jitter。
### 心跳间隔设多少合适?太短和太长各有什么问题?
通常 30 秒。太短(如 5 秒)会浪费带宽和服务端资源,10 万连接每 5 秒 ping 一次就是 2 万次/秒;太长(如 120 秒)无法及时发现半开连接,用户可能等 2 分钟才知道消息收不到了。移动端建议 30-45 秒,服务端 idle timeout 设为心跳间隔的 2-3 倍(如 90 秒),给网络波动留余量。Nginx 代理 WebSocket 时也要配置 `proxy_read_timeout` 与之一致。
### 连接池在什么场景下需要?单连接不够用吗?
浏览器对同一域名的 WebSocket 连接数没有硬限制(不像 HTTP/1.1 的 6 个),所以大多数场景单连接就够了,在应用层做消息多路复用(通过 type 字段区分业务)。需要连接池的场景:不同业务域隔离(聊天和行情走不同连接,避免互相阻塞);大规模数据传输(单连接带宽不够时分流);高可用(主连接断开时备用连接无缝切换)。不要为了"看起来专业"而引入连接池,多连接意味着多倍的心跳和内存开销。
### 页面切换或组件卸载时 WebSocket 连接怎么处理?
SPA 路由切换时不要断开重连,把 WebSocket 实例提升到全局(Redux store 或 Context),路由组件只订阅/取消订阅消息,连接本身持续存在。组件卸载时必须移除事件监听器,否则闭包引用的旧 state 会导致内存泄漏和僵尸回调。页面真正关闭(`beforeunload`)时发关闭帧优雅断开。React 项目推荐用自定义 Hook 封装,`useEffect` 的 cleanup 中移除 listener。
```javascript
// 指数退避 + 随机抖动重连
function createReconnectingWebSocket(url) {
let ws;
let retryCount = 0;
const maxRetries = 10;
const baseDelay = 1000;
const maxDelay = 30000;
function connect() {
ws = new WebSocket(url);
ws.onopen = () => {
retryCount = 0; // 连接成功,重置计数
};
ws.onclose = (event) => {
if (event.code === 1000 || retryCount >= maxRetries) return;
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay)
+ Math.random() * 1000; // 随机抖动
retryCount++;
setTimeout(connect, delay);
};
}
connect();
return ws;
}
// 心跳机制
function startHeartbeat(ws, interval = 30000) {
const timer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping(); // 或 ws.send(JSON.stringify({ type: 'ping' }))
}
}, interval);
return timer; // 组件卸载时 clearInterval(timer)
}
```标签
WebSocket
WebSocket 是一种网络通信协议,提供了在单个TCP连接上进行全双工通讯的能力。它是HTML5一部分的先进技术,允许服务器和客户端之间进行实时、双向的交互通信。WebSocket设计用来取代传统的轮询连接,如长轮询,使得数据可以快速地在客户端和服务器之间传输,从而减少延迟。

服务端6月1日 09:19
WebSocket 连接有哪些安全风险?如何防护?WebSocket 的安全威胁主要来自四方面:窃听、劫持、伪造和洪泛。防护的核心是强制 WSS(TLS 加密)防止窃听,验证 Origin 头防止跨站 WebSocket 劫持(CSWSH),在握手阶段或首条消息中完成身份认证防止伪造,速率限制和消息校验防止洪泛和注入攻击。WSS 是底线——ws:// 明文传输下,中间人可以读取甚至篡改所有消息。Origin 验证同样关键:浏览器发起 WebSocket 连接时会自动携带 Cookie,如果服务端不校验 Origin,恶意页面可以伪造用户身份建立连接(即 CSWSH 攻击)。认证方面,URL 查询参数传 token 会泄露到日志,更安全的做法是连接建立后通过首条消息发送 JWT,服务端验证失败立即关闭连接(code=1008)。
## 追问
### CSWSH 攻击是怎么发生的?和 CSRF 有什么区别?
CSWSH 利用浏览器自动携带 Cookie 的特性:恶意网站用 JS 创建 `new WebSocket('wss://victim.com/socket')`,浏览器会带上 victim.com 的 Cookie,服务端如果只看 Cookie 不看 Origin,就会把连接当作合法用户。和 CSRF 的区别在于:CSRF 只能触发请求不能读取响应(同源策略限制),而 WebSocket 连接建立后恶意页面可以双向通信、读取服务端推送的敏感数据,危害更大。防护就是服务端必须校验 Origin 头。
### URL 参数传 Token 和连接后发认证消息,哪个更安全?
连接后发认证消息更安全。URL 参数中的 token 会被记录在服务端访问日志、浏览器历史、代理日志中,且很难清除。连接后发消息的方式 token 只在 WebSocket 帧中传输,WSS 加密后中间人看不到。但连接后认证有一个窗口期:连接已建立但未认证,这段时间内服务端不能处理任何业务消息。实践中两者可以组合——URL 传短期一次性 ticket,连接后再用 JWT 换取长期会话。
### WebSocket 上的 XSS 攻击怎么防?
WebSocket 传输的文本如果直接插入 DOM(`innerHTML`),和 HTTP 一样存在 XSS 风险。防护手段:消息内容做 HTML 转义后再渲染,或用 `textContent` 替代 `innerHTML`;服务端过滤消息中的 `<script>`、`javascript:`、`onerror=` 等危险模式;消息大小限制(如 10KB)防止超长 payload;消息结构白名单校验,只允许预定义的 type 字段。记住 XSS 的根源是渲染层,不是传输层。
### 怎么防止 WebSocket 连接被用于 DDoS?
服务端侧:限制单 IP 最大连接数(如 50),超出拒绝握手;单连接消息速率限制(如每分钟 100 条),超限断开;消息体积上限(如 64KB),防止内存炸弹;空闲连接超时断开(idleTimeout)。架构层:前端用 Nginx 做 WebSocket 代理,配置 `limit_conn` 和 `limit_req`;启用 DDoS 防护服务(Cloudflare 等);连接鉴权增加攻击成本,未认证的连接快速关闭不占资源。
```javascript
// 服务端安全校验示例
wss.on('connection', (ws, req) => {
// 1. Origin 校验
const origin = req.headers.origin;
if (!['https://yourdomain.com'].includes(origin)) {
return ws.close(1003, 'Forbidden origin');
}
// 2. 身份认证(首条消息)
ws.authenticated = false;
ws.once('message', (msg) => {
const { type, token } = JSON.parse(msg);
if (type !== 'auth') return ws.close(1008, 'Auth required');
try {
const user = jwt.verify(token, SECRET);
ws.authenticated = true;
ws.userId = user.id;
} catch {
return ws.close(1008, 'Invalid token');
}
});
// 3. 速率限制
ws.messageCount = 0;
ws.on('message', () => {
ws.messageCount++;
if (ws.messageCount > 100) ws.close(1008, 'Rate limit');
});
});
```服务端6月1日 09:19
WebSocket 性能瓶颈在哪?如何优化?WebSocket 性能优化集中在三个层面:协议层压缩与传输格式、应用层消息策略、架构层连接管理。协议层最有效的是启用 permessage-deflate 扩展,文本消息可压缩 60-80%,但小消息(<1KB)压缩反而增加开销,建议设 threshold=1024,压缩级别用 3(速度与压缩率平衡)。传输格式上,大数据场景用二进制(ArrayBuffer/TypedArray)替代 JSON 字符串,解析更快、体积更小;高频结构化消息可用 Protocol Buffers 或 MessagePack 替代 JSON。应用层关键是消息批处理,把短时间内的多条小消息合并成一条发送,减少网络往返次数。架构层用连接池复用连接、用 uWS 替代 ws 库(性能高 10-20 倍)、通过 Redis Pub/Sub 分发消息实现多进程横向扩展。
## 追问
### permessage-deflate 压缩在什么场景下反而降低性能?
消息小于 1KB 时压缩开销大于收益,因为压缩本身消耗 CPU,且 deflate 帧头和上下文开销可能让数据反而变大。高并发场景下 CPU 是瓶颈,压缩会加剧竞争。实践中建议设 threshold 只压缩大消息,或者在 CPU 占用超过 70% 时关闭压缩。另外 `clientNoContextTakeover: true` 可以限制服务端内存占用,但会降低压缩率。
### 消息批处理的延迟和吞吐怎么取舍?
批处理本质是用延迟换吞吐——攒一批再发,单次传输数据量更大,网络效率更高,但接收端看到消息的时间晚了。实时聊天场景 batchTimeout 不宜超过 50ms,行情推送可设 100-200ms。可以动态调整:消息积压多时加大 batch 减少发送次数,消息稀疏时减小 timeout 保证低延迟。
### 为什么 uWS 比 ws 库快那么多?有什么代价?
uWS 用 C++ 编写,直接操作 epoll/kqueue,避免了 Node.js 事件循环的开销;ws 库是纯 JS 实现,每条消息都经过 V8 的 JS/C++ 边界。代价是 uWS 的 API 不兼容 ws,调试困难,社区生态小,遇到问题难以排查;而且它不支持 permessage-deflate 的所有配置项。对性能要求不极致的项目,ws 的开发效率更高。
### 如何监控 WebSocket 连接的性能瓶颈?
核心指标:单连接消息吞吐量(条/秒)、端到端延迟(ping-pong RTT)、服务端内存和 fd 占用、消息队列积压深度。用 `PerformanceObserver` 或自定义中间件采集每个连接的发送/接收字节数和延迟分布,P99 延迟超过 200ms 或内存持续增长说明有瓶颈。线上可用 Prometheus + Grafana 做大盘,重点关注连接数突增和消息积压告警。
```javascript
// permessage-deflate 配置示例(Node.js ws 库)
const wss = new WebSocket.Server({
perMessageDeflate: {
threshold: 1024, // 超过 1KB 才压缩
zlibDeflateOptions: {
level: 3, // 压缩级别,3 是速度与压缩率平衡点
concurrency: 10
},
clientNoContextTakeover: false, // 允许上下文复用,压缩率更高
serverNoContextTakeover: false
}
});
// 消息批处理
class MessageBatcher {
constructor(ws, batchSize = 10, batchTimeout = 100) {
this.ws = ws;
this.queue = [];
this.timer = null;
this.batchSize = batchSize;
this.batchTimeout = batchTimeout;
}
add(msg) {
this.queue.push(msg);
if (this.queue.length >= this.batchSize) this.flush();
else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.batchTimeout);
}
}
flush() {
if (!this.queue.length) return;
this.ws.send(JSON.stringify({ type: 'batch', messages: this.queue }));
this.queue = [];
clearTimeout(this.timer);
this.timer = null;
}
}
```服务端6月1日 09:19
WebSocket 和 HTTP 有什么区别?为什么需要 WebSocket?WebSocket 是基于 TCP 的全双工通信协议,属于 HTML5 规范的一部分。与 HTTP 最大的区别在于通信模式:HTTP 是请求-响应模型,客户端发起请求后服务端才能返回数据,每次请求都携带完整头部(数百字节),连接完即断;WebSocket 通过 HTTP Upgrade 机制握手一次(客户端发送 `Upgrade: websocket` 头,服务端返回 `101 Switching Protocols`),之后在同一条 TCP 连接上双方可随时互发数据,头部开销仅 2-10 字节。这意味着服务端可以主动推送,延迟从 HTTP 轮询的数百毫秒降到毫秒级,适合聊天、实时行情、协同编辑、在线游戏等场景。WebSocket 连接是持久的,除非任一方发送关闭帧或网络中断,否则连接一直保持。
## 追问
### WebSocket 握手为什么基于 HTTP?能不能跳过 HTTP 直接建立连接?
不能跳过。WebSocket 复用 HTTP 握手是为了兼容现有基础设施——浏览器先发一个标准 HTTP GET 请求(携带 Upgrade 头),中间的代理、负载均衡、防火墙都能识别并放行;如果直接用新协议,这些中间件会拒绝或破坏连接。握手成功后协议才切换为 WebSocket,之后的数据帧完全脱离 HTTP 格式。
### HTTP/2 的 Server Push 能替代 WebSocket 吗?
不能。HTTP/2 Server Push 是服务端主动推送资源到浏览器缓存,本质还是服务于请求-响应模型,客户端无法通过 JavaScript 读取推送的原始内容;而且 Push 的资源受浏览器缓存策略控制,不适合实时数据流。WebSocket 是真正的双向消息通道,客户端可以用 JS 直接处理每一条推送消息。
### WebSocket 连接断开时如何判断是网络问题还是服务端主动关闭?
WebSocket 关闭帧(Close Frame)携带状态码:1000 表示正常关闭,1001 表示端点离开,1006 表示异常关闭(没有收到关闭帧,通常是网络中断)。浏览器 `onclose` 事件的 `code` 和 `reason` 字段可以区分:code=1006 是网络问题需要重连,code=1000/1001 是正常关闭不需要重连。
### 大量 WebSocket 连接会压垮服务端吗?怎么控制?
会。每个 WebSocket 连接占用一个文件描述符和内存(约 10-50KB),单机默认 fd 上限通常 65535。控制手段:用 `ulimit -n` 或 systemd 调高 fd 上限;设最大连接数阈值超限拒绝;用 Redis Pub/Sub 做跨进程消息分发,单机只维护本机连接;横向扩展用 sticky session 或网关层路由,把同一用户的连接固定到同一后端。
```javascript
// WebSocket 握手过程示例
const ws = new WebSocket('wss://example.com/socket');
ws.onopen = () => {
console.log('连接已建立,协议已升级');
ws.send('hello'); // 直接发送,无需 HTTP 请求
};
ws.onmessage = (event) => {
console.log('收到服务端推送:', event.data);
};
ws.onclose = (event) => {
console.log('关闭码:', event.code, '原因:', event.reason);
// code=1006 说明是异常断开,需要重连
};
```服务端6月1日 09:12
WebSocket 有哪些安全问题?CSWSH/注入/DoS 防御方案WebSocket主要面临五大安全威胁:跨站WebSocket劫持(CSWSH)、数据注入、中间人攻击、认证缺失和拒绝服务。CSWSH是最典型的——浏览器WebSocket握手自带Cookie但不受同源策略约束,攻击者在恶意页面发起ws连接即可冒用用户身份。防御核心是服务端严格校验Origin头,拒绝非白名单来源。数据注入方面,WebSocket帧内容无格式约束,服务端若不做输入验证和转义就写数据库或渲染页面,等同于XSS/SQL注入入口。中间人攻击靠WSS(WebSocket over TLS)加密传输解决,和HTTPS同理。认证授权方面,WebSocket握手阶段无法自定义Header,常见做法是URL参数带token(`wss://host/ws?token=xxx`)或通过Sec-WebSocket-Protocol子协议传递,后者更安全因为不暴露在日志里。DoS方面,恶意客户端可狂开连接或发送巨量数据,需限制单IP连接数、单帧大小和消息频率。
## 追问
### CSWSH和CSRF有什么区别?
CSRF针对HTTP请求,浏览器自动携带Cookie;CSWSH针对WebSocket握手,同样自动带Cookie但连接建立后变为长连接,攻击者能持续收发消息,危害更大。防御上CSRF用CSRF Token,CSWSH靠Origin校验。
### URL传token有什么安全隐患?
token出现在URL中会被浏览器历史记录、服务器访问日志、Referer头记录。改进方案:用一次性ticket——先通过HTTPS接口获取短时效ticket,再通过URL传ticket建立WebSocket,服务端验证后立即销毁,后续靠连接状态维持会话。
### WSS能防中间人,能防数据注入吗?
不能。WSS只保证传输链路加密和完整性,不校验内容语义。恶意客户端照样通过WSS发送XSS payload或SQL注入语句,服务端必须独立做输入校验和输出转义。
### 如何限制WebSocket的DoS攻击?
服务端网关层做四件事:单IP最大连接数限制、单帧最大体积限制(如64KB)、消息频率限流(如每秒10条)、空闲超时断开。Nginx可通过`limit_conn`和`proxy_read_timeout`实现基础防护。
### Sec-WebSocket-Protocol传token具体怎么实现?
客户端发起握手时在Header中设置`Sec-WebSocket-Protocol: access_token, your_protocol`,服务端从该头提取token验证,验证通过后在响应中只回传`your_protocol`(不回传token),这样连接正常建立。好处是token不出现在URL中,缺点是语义上属于对该字段的滥用。服务端6月1日 09:12
WebSocket 性能怎么优化?连接/传输/服务端三层优化策略WebSocket性能优化要从连接、传输、服务端三个层面入手。连接层:用单例模式复用连接,避免重复建连;连接池管理多服务端场景,按负载分配。传输层:消息压缩(pako或服务端gzip)减少带宽;批量发送合并高频小消息;用二进制格式替代JSON减少序列化开销;心跳间隔根据网络环境调优,移动端可适当缩短。服务端:ws库配置maxPayload防大包攻击,合理设置heartbeat超时;客户端设置binaryType为arraybuffer避免Blob转换损耗。负载均衡需用sticky session保证同一客户端请求落到同一服务节点。
## 追问
### 连接复用和连接池有什么区别?
连接复用是单个客户端对同一服务端只维护一条连接,通过单例模式实现,避免重复握手开销。连接池则是预先创建多条连接供不同业务模块使用,适用于需要连接多个不同服务端的场景。实际开发中,连接复用更常见,连接池多用于微服务网关层。
### 什么时候该用二进制而不是JSON?
高频传输、数据量大、结构固定的场景优先二进制。比如实时坐标、传感器数据,用ArrayBuffer或protobuf比JSON体积小30%-60%,解析也更快。如果是低频控制指令,JSON可读性好,开发调试方便,没必要换。
### 消息批量发送怎么实现?
设一个缓冲区,短时间内的消息先攒起来,定时或达到阈值再一次性发送。比如50ms内的消息合并成一条发送。注意设置最大延迟,避免消息等太久。断线重连时缓冲区的消息要能补发。
### 心跳间隔怎么定?
局域网5-10秒足够,公网移动端建议15-30秒。间隔太短浪费流量和电量,太长又不能及时检测断连。关键原则:心跳超时时间至少是间隔的3倍,给网络抖动留余量。NAT超时一般2-5分钟,心跳间隔必须短于这个值。
### sticky session有什么问题?
单点故障——某台服务端挂了,它上面的连接全部断开,不能自动漂移到其他节点。解决思路:服务端用Redis Pub/Sub同步状态,客户端断连后重试可落到新节点并恢复上下文。sticky session是妥协方案,真正想高可用得做有状态迁移。服务端6月1日 09:12
Node.js 怎么搭建 WebSocket 服务器?ws 库实战指南Node.js 搭建 WebSocket 服务器最主流的方式是用 `ws` 库,轻量且性能好。基础用法只需几行代码:创建 `WebSocket.Server` 实例,监听 `connection` 事件处理消息收发。实际项目中通常把它挂到已有的 HTTP 服务器上,这样 HTTP 和 WS 共用同一端口,部署和运维更方便。认证方面,`ws` 提供 `verifyClient` 回调,在握手阶段拦截非法连接,配合 JWT 做令牌校验即可。房间功能需要自己实现——用 Map 按房间 ID 存储客户端集合,提供 join/leave/broadcast 方法。广播就是遍历房间内所有连接逐个 send,注意排除发送者本身。获取客户端真实 IP 要从 `req.headers['x-forwarded-for']` 或 `req.socket.remoteAddress` 取,反向代理场景下前者更可靠。消息格式验证建议在收到消息后先用 JSON.parse + schema 校验,避免畸形数据引发异常。
```js
const WebSocket = require('ws');
const server = require('http').createServer();
const wss = new WebSocket.Server({ server });
const rooms = new Map();
wss.on('connection', (ws, req) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'join') {
if (!rooms.has(msg.room)) rooms.set(msg.room, new Set());
rooms.get(msg.room).add(ws);
ws.room = msg.room;
}
if (msg.type === 'chat') {
const room = rooms.get(msg.room);
if (!room) return;
room.forEach(c => {
if (c !== ws && c.readyState === WebSocket.OPEN)
c.send(JSON.stringify({ text: msg.text }));
});
}
});
ws.on('close', () => {
if (ws.room && rooms.has(ws.room)) {
rooms.get(ws.room).delete(ws);
if (rooms.get(ws.room).size === 0) rooms.delete(ws.room);
}
});
});
server.listen(3000);
```
## 追问
### ws 和 Socket.IO 怎么选?
`ws` 是纯 WebSocket 实现,体积小、性能高、无额外协议。Socket.IO 在 WebSocket 之上加了自动降级(长轮询)、命名空间、房间等,但协议不兼容标准 WS 客户端。如果客户端不可控(比如浏览器),用 Socket.IO 更稳;如果前后端都是自己掌控,`ws` 更干净。
### 高并发下房间广播性能怎么优化?
遍历 Set 逐个 send 在万级连接时会有瓶颈。优化思路:用 `ws` 的批量发送;消息做二进制编码(MessagePack 替代 JSON);房间内连接超多时改用 pub/sub 模式(Redis),多进程各自只发自己管理的连接。
### verifyClient 里做异步校验怎么办?
`verifyClient` 的回调 `cb` 可以稍后调用,所以你能在里面写数据库查询或远程校验逻辑,校验完再调 `cb(true/false)` 就行。别把请求阻塞太久,设个超时。
### 如何防止消息洪水攻击?
在 `connection` 事件中用令牌桶限流,比如每秒只允许每个连接发 N 条消息,超出直接断开。也可以在 nginx 层面对 WS 连接做 `limit_conn` 限制,双重保险。服务端6月1日 09:12
浏览器中 WebSocket 怎么用?API 详解与封装实践创建WebSocket实例即可建立长连接,通过四个事件回调处理通信生命周期:`onopen`连接就绪后用`send()`发消息,`onmessage`接收服务端数据,`onclose`和`onerror`处理断开与异常。协议用`ws://`或加密的`wss://`,消息支持文本、JSON、Blob和ArrayBuffer四种格式。连接状态通过`readyState`判断——0正在连接、1已连接、2正在关闭、3已关闭。收二进制数据前需设置`binaryType`为`"arraybuffer"`或`"blob"`。主动关闭调用`close()`并传入状态码(如1000正常关闭、1001离开页面)。生产环境建议封装客户端类,加入自动重连、心跳检测和消息队列,避免连接中断丢消息。
## 追问
### readyState各状态值在什么场景下出现?
0(CONNECTING)在`new WebSocket()`后瞬间出现,握手完成前;1(OPEN)是握手成功的正常工作态;2(CLOSING)是调用`close()`后服务端还未返回关闭帧的过渡态;3(CLOSED)是关闭帧确认或连接异常断开后的终态。注意:网络断开不会立刻变3,需心跳超时才能检测到。
### 心跳检测怎么实现?
连接建立后启动定时器,每隔N秒(通常30s)发送`ping`帧或自定义心跳消息,服务端回`pong`或应答。若超时未收到回复则判定断开,主动`close()`并触发重连。收到任何消息都重置超时计时器。关键点:心跳间隔要小于Nginx等中间代理的空闲超时(默认60s),否则代理会静默断开连接。
### 断线重连策略如何设计?
采用指数退避:首次1s后重连,失败则2s、4s、8s,上限30s左右。每次`onclose`触发时启动重连定时器,`onopen`时重置退避计数。注意区分正常关闭(状态码1000)和异常断开——前者不重连。重连前要清理旧实例的事件监听,避免内存泄漏。
### 消息队列有什么用?
连接未就绪时`send()`会报错,所以发消息前必须检查`readyState`。非OPEN状态下将消息推入队列,等`onopen`后再批量发出。队列需设上限(如100条),防止离线过久内存暴涨。对于实时性要求高的场景(如聊天),旧消息可丢弃只保留最新状态。服务端6月1日 09:11
WebSocket 常见问题怎么排查?连接失败/消息丢失/频繁断开WebSocket连接问题主要集中在四个环节:建立、握手、传输、稳定性。连接无法建立时,先确认服务器是否启动、端口是否正确、防火墙是否放行,再检查协议——本地开发用ws,生产环境必须wss,部分反向代理默认不转发WebSocket升级请求,需显式配置。握手失败通常是服务端校验不通过:Origin白名单未放行、子协议不匹配、认证信息缺失。消息丢失多因连接已断开但客户端未感知,或发送队列溢出被丢弃。连接频繁断开要排查心跳是否缺失、Nginx代理超时配置、网络中间件是否主动杀长连接。调试时重点监听onerror和onclose事件,通过close code定位原因(1006异常关闭、1008策略违规等),浏览器DevTools的Network面板可查看完整帧数据。
## 追问
### ws和wss混用会导致什么问题?
浏览器强制HTTPS页面只能发起wss连接,ws会被直接拦截。即使服务端支持ws,混用也会触发安全策略报错。开发环境可用ws,上线必须切wss并配置SSL证书。
### Nginx反向代理WebSocket需要什么额外配置?
必须加`Upgrade`和`Connection`头:
```nginx
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
```
`proxy_read_timeout`默认60秒,超过不活动就断开,需按业务调大。
### 如何检测连接是否还活着?
实现心跳机制:客户端定时发ping帧,服务端回pong。若连续N次未收到响应则判定断连,主动重连。不要依赖TCP的keepalive,那个超时太长(默认2小时),不实用。
### close code各代表什么?
1000正常关闭,1001端点离开,1006异常关闭(未收到关闭帧,通常是网络断开),1008策略违规,1011服务端异常。1006最常见,说明连接不是正常关闭的,要排查网络和中间件。
### 消息发送队列溢出怎么处理?
发送前判断`readyState`是否为`OPEN`,非OPEN状态入队缓存,连接恢复后补发。设队列上限,超出则丢弃旧消息或拒绝新消息,避免内存撑爆。关键业务消息要做服务端ACK确认,不能发完就当成功。服务端6月1日 02:20
WebSocket 是什么?和 HTTP 有什么区别?WebSocket 是 HTML5 引入的全双工通信协议,客户端和服务端可以同时收发数据,连接建立后持续保持,直到任一方主动关闭。它通过一次 HTTP 握手升级协议,之后不再走 HTTP 请求-响应模型,而是以帧为单位双向传输,帧头仅 2-14 字节,远小于 HTTP 每次请求携带的头部开销。适用于聊天、实时行情、多人游戏、协同编辑、监控推送等需要低延迟双向通信的场景。
## 追问
### WebSocket 握手过程是怎样的?
客户端发起一个普通 HTTP 请求,携带 `Upgrade: websocket` 和 `Connection: Upgrade` 头,并附上 `Sec-WebSocket-Key`。服务端同意后返回 101 状态码,带上 `Sec-WebSocket-Accept`(由客户端 Key 经 SHA-1 计算得出)。此后 TCP 连接升级为 WebSocket 连接,不再走 HTTP 协议。
### HTTP 长轮询能替代 WebSocket 吗?
不能。长轮询是客户端发请求后服务端持有连接直到有数据才返回,本质仍是请求-响应模式。每次返回数据后客户端必须重新发起新请求,延迟高、开销大、无法服务端主动推送。WebSocket 连接建立后服务端随时可推数据,无需客户端轮询。
### WebSocket 数据帧结构是什么?
帧由以下字段组成:FIN(1位,是否最后一帧)、RSV1-3(保留)、Opcode(4位,帧类型:0x1文本、0x2二进制、0x8关闭、0x9 ping、0xA pong)、MASK(1位,是否掩码)、Payload Length(7位或扩展)、Masking-key(0或4字节)、Payload Data。客户端发送帧MASK必须为1,服务器为0。
### 什么场景下应该选 HTTP 而非 WebSocket?
请求频率低、以客户端拉取为主的场景用 HTTP 更合适。WebSocket 需要维护长连接,占用服务端资源,且断线重连、心跳保活都需要额外处理。如果只是偶尔查个数据、提交个表单,HTTP 更简单可靠。服务端6月1日 02:20
WebSocket 握手过程是怎样工作的?HTTP 升级机制与安全防御WebSocket 握手是基于 HTTP 的协议升级过程。客户端发送一个特殊的 HTTP GET 请求,携带 `Upgrade: websocket` 和 `Connection: Upgrade` 头部,表明要切换协议。同时带上 `Sec-WebSocket-Key`(16字节随机值的 Base64 编码)和 `Sec-WebSocket-Version`(通常为13)。服务端验证请求合法后,返回 `101 Switching Protocols` 状态码,并附带 `Sec-WebSocket-Accept` 头部,其值由客户端 Key 拼接固定 GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`,经 SHA-1 哈希后再 Base64 编码得到。握手完成后,连接不再走 HTTP 协议,而是切换为 WebSocket 帧格式进行双向通信。还可通过 `Sec-WebSocket-Protocol` 协商子协议,`Sec-WebSocket-Extensions` 协商扩展(如压缩)。
## 追问
### Sec-WebSocket-Accept 为什么要拼接固定 GUID?
这是 RFC 6455 的设计:GUID 是公开的常量,不提供加密作用,而是确保握手方确实理解 WebSocket 协议——普通 HTTP 客户端不会拼接这个 GUID,从而防止误升级。服务端无需维护状态,只需按规则计算即可验证。
```javascript
const crypto = require('crypto');
function acceptValue(key) {
return crypto.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
.digest('base64');
}
```
### 什么是 CSWSH 攻击?如何防御?
CSWSH(Cross-Site WebSocket Hijacking)指恶意网页利用浏览器自动携带 Cookie 的特性,向目标站点发起 WebSocket 连接,冒充已登录用户。防御核心是服务端验证 `Origin` 头部,拒绝非白名单来源的升级请求。此外,避免仅依赖 Cookie 鉴权,应结合 Token 机制(如在 URL 参数或首条消息中携带)。
### wss 和 ws 的区别是什么?
`ws://` 是明文传输,`wss://` 在 TCP 之上加了 TLS 加密层,等同于 HTTP 与 HTTPS 的关系。生产环境必须使用 `wss`,否则握手和后续帧数据均可被中间人窃听或篡改。TLS 还能防止缓存投毒攻击——代理服务器不会将加密流量误判为普通 HTTP 响应并缓存。
### 握手失败会怎样?
服务端如果不支持 WebSocket 或验证不通过,不会返回 101,而是返回普通 HTTP 响应(如 400/403)。客户端收到非 101 响应后,`WebSocket` 对象触发 `error` 事件并关闭连接,不会尝试回退。若需降级为轮询,需在应用层自行实现。服务端6月1日 02:17
WebSocket 握手过程是怎样的?HTTP 升级机制详解WebSocket 握手基于 HTTP 协议升级机制。客户端发送一个普通的 HTTP GET 请求,携带几个关键头部:`Upgrade: websocket` 表示要求协议升级,`Connection: Upgrade` 表示连接需切换协议,`Sec-WebSocket-Key` 是一个随机的 Base64 编码值(16 字节),`Sec-WebSocket-Version: 13` 指定协议版本,`Origin` 标识请求来源。服务器收到后,如果同意升级,返回 `101 Switching Protocols` 状态码,并携带 `Sec-WebSocket-Accept` 头部,其值由客户端的 Key 拼接固定 GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`,经 SHA-1 哈希后再 Base64 编码得到。这个计算过程既确认了服务器理解 WebSocket 协议,也防止了误响应。握手完成后,底层 TCP 连接从 HTTP 模式切换为 WebSocket 模式,双方可以全双工通信,不再遵循 HTTP 请求-响应模型。安全层面:服务端应验证 `Origin` 头部防止跨站劫持(CSRF),`Sec-WebSocket-Key` 的校验确保对方确实是 WebSocket 客户端而非普通浏览器,生产环境建议使用 `wss://` 加密传输。
## 追问
### Sec-WebSocket-Accept 的计算为什么不是直接返回 Key?
直接回传 Key 无法证明服务器理解 WebSocket 协议——任何 HTTP 服务器都会原样返回请求头。拼接固定 GUID 再做 SHA-1 + Base64,是一种轻量级"握手证明":只有知道协议规范的 WebSocket 服务端才会执行这个计算,普通 HTTP 服务器或代理不会。同时客户端也会校验返回的 Accept 值,不匹配则拒绝连接。
### Origin 头在握手中的作用是什么?
`Origin` 让服务端判断握手请求是否来自可信源。WebSocket 不受同源策略约束,任何网页都能发起连接,所以服务端必须在握手阶段校验 Origin,拒绝非授权来源,防止恶意页面利用已认证的用户会话建立 WebSocket 连接(即 CSRF 攻击)。
### 为什么握手用 HTTP 而不是直接新建 TCP 连接?
复用 HTTP 可以利用现有基础设施:经过 HTTP 代理、防火墙、负载均衡器时不会被视为未知协议而被拦截。同时 HTTP 80/443 端口通常是开放的,降低了部署门槛。本质上是用 HTTP 作为"引导协议",完成协商后再切换到 WebSocket 二进制帧协议。
### wss 和 ws 的区别是什么?
`ws://` 是明文传输,`wss://` 在 TCP 之上加了 TLS 加密层,等价于 HTTP 与 HTTPS 的关系。握手阶段 wss 的 HTTP 升级请求走 HTTPS,后续 WebSocket 帧同样加密传输。生产环境必须用 wss,否则数据可被中间人窃听或篡改。服务端6月1日 02:15
WebSocket 心跳机制怎么实现?ping/pong 超时重连方案WebSocket心跳机制通过定时发送ping/pong帧来维持连接活跃。客户端每隔固定间隔向服务器发送ping,服务器收到后返回pong,若客户端在超时时间内未收到pong则判定连接断开并触发重连。心跳的核心作用有三个:一是检测连接是否存活,TCP连接半打开时应用层无法感知;二是防止NAT超时或防火墙因空闲连接而切断链路;三是实现快速故障恢复,避免等到下次数据发送时才发现连接已断。WebSocket协议本身定义了ping/pong控制帧,但实际开发中也常用自定义文本消息(如`{"type":"ping"}`)实现,灵活性更高且便于扩展业务数据。
## 追问
### 心跳间隔怎么定?
推荐30-60秒。间隔过短增加服务器负担和带宽消耗,间隔过长又无法及时检测断开。具体值需权衡:移动网络下NAT超时通常2-5分钟,心跳间隔应小于NAT超时的一半。对实时性要求高的场景可缩短到15-25秒。
### 超时处理怎么做?
发送ping后启动超时定时器,若在阈值内(通常为心跳间隔的2-3倍)未收到pong,则视为连接失效:
```js
let heartbeatTimer, timeoutTimer;
function startHeartbeat() {
heartbeatTimer = setInterval(() => {
ws.send('ping');
timeoutTimer = setTimeout(() => {
ws.close(); // 超时未收到pong,关闭连接
}, heartbeatInterval * 3);
}, heartbeatInterval);
}
ws.onmessage = (e) => {
if (e.data === 'pong') {
clearTimeout(timeoutTimer); // 收到pong,清除超时
}
};
```
### 双向心跳是什么?
只由客户端发起心跳时,如果客户端崩溃,服务器无从得知。双向心跳让服务器也定时发ping,双方互检连接状态,避免单侧故障盲区。实现时注意避免双方ping同时触发造成冲突,可错开间隔或由一方主导。
### 重连策略怎么设计?
断开后应采用指数退避重连,避免大量客户端同时重连导致服务器雪崩:
```js
let retryCount = 0;
const maxRetry = 5;
function reconnect() {
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
setTimeout(() => {
retryCount++;
connect();
}, delay);
}
ws.onclose = () => {
if (retryCount < maxRetry) reconnect();
};
```
### 还有哪些最佳实践?
- **动态调整间隔**:网络良好时拉长间隔节省资源,弱网下缩短间隔加快检测
- **状态监控**:记录心跳延迟和丢包率,用于运维告警和间隔调优
- **重连成功后重置退避计数**,避免下次断开仍从高延迟开始服务端6月1日 02:15
WebSocket 消息帧格式是怎样的?各字段含义详解WebSocket消息帧是通信的基本单位,由固定格式的二进制结构组成。一帧包含以下字段:FIN(1位)标识是否为最后一个分片;RSV1-3(各1位)保留给扩展使用;Opcode(4位)标识帧类型,0x0为继续帧、0x1文本帧、0x2二进制帧、0x8关闭帧、0x9 Ping、0xA Pong;MASK(1位)标识是否使用掩码;Payload Length(7位或扩展)表示载荷长度;Masking-key(0或4字节)为掩码密钥;Payload Data为实际数据。
客户端发送的帧MASK必须为1,服务器发送的帧MASK必须为0。Payload Length采用变长编码:0-125直接用7位表示;126表示后续2字节为真实长度;127表示后续8字节为真实长度。掩码算法将Payload Data的每字节与Masking-key循环异或(XOR),服务器收到后用同样方式还原。大消息可通过分片传输,首帧FIN=0、Opcode为实际类型,中间帧FIN=0、Opcode=0x0,末帧FIN=1、Opcode=0x0。关闭帧(Opcode=0x8)可携带状态码和原因,用于正常或异常断开连接。
## 追问
### 掩码算法具体怎么计算?
客户端发送时生成4字节随机Masking-key,对Payload Data逐字节异或:
```js
for (let i = 0; i < payload.length; i++) {
decoded[i] = payload[i] ^ maskingKey[i % 4];
}
```
编码和解码是同一操作,异或两次还原原文。设计目的是防止缓存污染攻击,不让中间代理误判载荷内容。
### 分片消息如何重组?
接收方维护一个缓冲区。收到FIN=0的帧时,将Payload追加到缓冲区,Opcode只在首帧有效。后续帧Opcode为0x0,表示"接续上一帧"。直到收到FIN=1的帧,拼接完成,最终消息类型取首帧的Opcode。注意控制帧(Ping/Pong/Close)不允许分片,可在数据帧分片中间插入。
### 为什么要区分三种Payload Length编码?
用7位最多只能表示125,不够用。加2字节可到65535,覆盖绝大多数场景。超过64KB才动用8字节,最大支持2^63-1。这种变长设计兼顾了小帧的紧凑性和大帧的扩展性——小帧不浪费头部空间,大帧也不受限制。
### 关闭帧里能带什么内容?
Payload前2字节是状态码(如1000正常关闭、1001端点离开、1002协议错误),剩余部分是UTF-8编码的关闭原因字符串,最长123字节(受125字节载荷上限减去2字节状态码限制)。双方都可以发起关闭帧,收到关闭帧的一方应回复一个关闭帧,然后双方关闭TCP连接。服务端6月1日 02:15
WebSocket 和 HTTP 轮询、长轮询有什么区别?各自适用什么场景?HTTP轮询是客户端按固定间隔向服务器发请求,无论有无数据都返回响应。实现最简单,但大部分请求是无效的,浪费带宽和服务器资源,延迟取决于轮询间隔。HTTP长轮询是客户端发请求后服务器不立即返回,而是挂起连接直到有新数据或超时才响应,客户端收到后再发下一个请求。实时性明显好于普通轮询,但每次数据推送都需要重新建立连接,且服务器需要维护大量挂起请求。WebSocket通过一次HTTP握手升级为持久连接,建立后双方可以随时主动发数据,是真正的全双工通信。帧头仅2-10字节,开销远低于HTTP每次请求携带的完整头部。三种方式中,WebSocket实时性最好、开销最低,但需要服务器和客户端同时支持;轮询兼容性最强但效率最差;长轮询是两者之间的折中。
## 追问
### 性能对比
| 维度 | 轮询 | 长轮询 | WebSocket |
|------|------|--------|-----------|
| 实时性 | 差(依赖间隔) | 较好 | 优 |
| 服务器负载 | 高(大量无效请求) | 中 | 低 |
| 带宽消耗 | 高 | 中 | 低 |
| 实现复杂度 | 低 | 中 | 中 |
| 浏览器兼容性 | 最好 | 好 | 好(IE10+) |
### 各自适用什么场景?
轮询适合对实时性要求不高的场景,比如配置中心定期拉取最新配置。长轮询适合无需双向通信但需要服务端及时推送的场景,比如消息通知、扫码登录状态检查。WebSocket适合高频双向通信场景,比如即时聊天、协同编辑、实时行情推送、在线游戏。
### WebSocket连接如何建立?
客户端发一个特殊的HTTP请求,携带`Upgrade: websocket`头:
```http
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
```
服务器返回101状态码表示协议切换成功:
```http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```
之后TCP连接保持,双方通过WebSocket帧格式通信,不再走HTTP协议。
### 长轮询和WebSocket在断线处理上有什么区别?
长轮询本身是"请求-响应"模式,断线后客户端只需重新发请求即可,天然具备重连能力,但重连期间会丢失消息,需要服务端做消息缓存。WebSocket断线后需要客户端主动重连并重新握手,通常配合心跳检测(ping/pong帧)判断连接存活,重连后还需处理会话恢复和消息补发逻辑。服务端6月1日 02:13
WebSocket 断线重连怎么实现?指数退避+心跳完整方案WebSocket 有四个连接状态:`CONNECTING(0)`、`OPEN(1)`、`CLOSING(2)`、`CLOSED(3)`。断线重连的核心是在 `onclose` 回调中判断关闭原因,对异常关闭自动重建连接。基础实现需控制重连次数上限,避免无限重试消耗资源。重连间隔采用指数退避策略,初始延迟如 1 秒,每次翻倍,上限 30 秒,避免短时间内大量重连请求冲击服务端。同时要区分正常关闭(code 1000)和异常关闭,正常关闭不应触发重连。心跳检测是重连机制的重要补充,客户端定时发送 ping,若 pong 超时未返回则主动关闭连接触发重连。还需监听浏览器的 `online`/`offline` 事件,离线时暂停重连,恢复在线后再尝试连接。完整方案应包含:指数退避间隔、最大重连次数、心跳检测、优雅关闭判断、离线状态感知、连接状态通知上层、断线期间消息队列缓存。
```javascript
class ReconnectWebSocket {
constructor(url) {
this.url = url;
this.reconnectCount = 0;
this.maxReconnect = 5;
this.heartbeatInterval = null;
this.heartbeatTimeout = null;
this.messageQueue = [];
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectCount = 0;
this.startHeartbeat();
this.flushQueue();
};
this.ws.onclose = (e) => {
this.stopHeartbeat();
if (e.code !== 1000) this.reconnect();
};
this.ws.onmessage = (e) => {
if (e.data === 'pong') {
clearTimeout(this.heartbeatTimeout);
return;
}
this.onMessage(e.data);
};
}
reconnect() {
if (this.reconnectCount >= this.maxReconnect) return;
const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 30000);
setTimeout(() => {
this.reconnectCount++;
this.connect();
}, delay);
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send('ping');
this.heartbeatTimeout = setTimeout(() => this.ws.close(), 5000);
}
}, 30000);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
this.messageQueue.push(data);
}
}
}
```
## 追问
### 为什么用指数退避而不是固定间隔重连?
固定间隔在服务端故障时会产生大量同步重连请求(雷群效应)。指数退避让重连间隔逐步增大,分散请求压力。加上随机抖动(jitter)效果更好:`delay = min(base * 2^n + Math.random() * 1000, 30000)`,避免多个客户端同一时刻同时重连。
### 哪些关闭码不应该触发重连?
关闭码 1000 表示正常关闭,1001 表示端点离开(如页面关闭),都不应重连。需要重连的是异常关闭码:1006(异常关闭,无关闭帧)、1002/1003(协议错误)、1011/1012(服务端异常/重启)、1013(稍后重试)。判断逻辑:`code !== 1000 && code !== 1001` 时触发重连。
### 心跳检测和重连怎么配合?
心跳负责主动发现"静默断连"——TCP 连接已断但 `onclose` 未触发。客户端每隔 30 秒发 ping,服务端回 pong,5 秒内未收到 pong 则判定连接已死,调用 `ws.close()` 主动关闭,触发 `onclose` 再进入重连流程。没有心跳,客户端可能长时间不知道连接已断。
### 离线检测怎么实现?
监听 `window.addEventListener('online', reconnect)` 和 `window.addEventListener('offline', stopReconnect)`。离线时暂停重连定时器,避免无效请求;恢复在线后立即尝试连接,并重置退避计数。比单纯靠 `onclose` 重连更及时。
### 断线期间的消息怎么处理?
维护一个消息队列,连接断开时将待发送消息入队而非丢弃。重连成功后(`onopen` 触发)按顺序发送队列中的消息。对于实时性要求高的场景,重连后可主动请求服务端补发缺失数据,队列只作为兜底。