WebSocket 实时通信:从握手原理到在线协作的技术实现
从 HTTP 轮询的局限到 WebSocket 双向通信,涵盖握手原理、心跳重连、聊天室实战和生产环境部署方案。
第一次接触实时通信是做一个在线客服系统。当时啥也不懂,用 setInterval 每 3 秒轮询一次接口拉新消息。上线第一天就被运维找上门——几百个客服同时在线,每人每 3 秒一个请求,服务器 QPS 直接爆了。那次之后我才开始认真研究 WebSocket。
Socket.IO 的「自动降级」听起来很美好——WebSocket 不行就降级到长轮询。但实际调试的时候简直是噩梦,你都不知道当前连接到底用的什么协议,出了问题 Network 面板里一片混乱。而且它引入了自己的一层协议封装,50KB+ 的包体积。大部分场景原生 WebSocket 就够了,别一上来就 Socket.IO。
1. HTTP 做不了实时通信吗?
HTTP 是请求-响应模型,客户端不问,服务器不说。为了实现「实时」,人们发明了各种 hack:
| 方案 | 原理 | 缺点 |
|---|---|---|
| 短轮询 | 定时发请求问「有新消息吗」 | 浪费带宽和服务器资源,延迟高 |
| 长轮询 | 请求挂着不返回,有消息才响应 | 服务器需要维护大量挂起连接 |
| SSE | 服务器单向推送事件流 | 只能服务器 → 客户端,无法双向 |
| WebSocket | 全双工双向通信 | 需要额外的服务器支持 |
WebSocket 在 TCP 层建立一个持久连接,客户端和服务器可以随时互发消息,没有 HTTP 的请求-响应开销。真正的实时通信场景(聊天、协作编辑、实时数据看板),WebSocket 是最佳选择。
2. WebSocket 握手过程
WebSocket 连接从一个 HTTP 请求开始,通过 Upgrade 机制「升级」为 WebSocket 协议。这就是为什么 WebSocket 的 URL 以 ws:// 或 wss:// 开头。
客户端发起握手请求
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13服务器响应 101 切换协议
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=握手完成后,底层的 TCP 连接保持打开,双方就可以通过这个连接自由收发消息了。消息以「帧」(Frame)为单位传输,开销很小——一个文本帧的头部只需要 2-10 字节,而 HTTP 请求头动辄几百字节。
3. 浏览器端 WebSocket API
基础用法
// 创建连接
const ws = new WebSocket('wss://api.example.com/chat')
// 连接建立
ws.onopen = () => {
console.log('已连接')
ws.send(JSON.stringify({ type: 'join', room: 'general' }))
}
// 接收消息
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('收到消息:', data)
}
// 连接关闭
ws.onclose = (event) => {
console.log(`连接关闭: ${event.code} ${event.reason}`)
}
// 错误处理
ws.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
// 发送消息
ws.send(JSON.stringify({ type: 'message', text: 'Hello!' }))
// 主动关闭
ws.close(1000, '正常关闭')常见坑:WebSocket 的 send() 只能发送字符串、ArrayBuffer 或 Blob。不能直接发对象,必须先 JSON.stringify()。收到的 event.data 同样是字符串,要自己 JSON.parse()。
4. 心跳与自动重连
网络环境不稳定,WebSocket 连接可能在任何时候断开。生产环境必须实现心跳检测和自动重连机制。
带心跳和指数退避重连的封装
function createWebSocket(url: string) {
let ws: WebSocket | null = null
let retryCount = 0
let heartbeatTimer: number
const maxRetries = 5
function connect() {
ws = new WebSocket(url)
ws.onopen = () => {
console.log('已连接')
retryCount = 0 // 重置重试计数
startHeartbeat()
}
ws.onclose = () => {
stopHeartbeat()
reconnect()
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'pong') return // 心跳响应,忽略
// 处理业务消息...
}
}
function startHeartbeat() {
heartbeatTimer = window.setInterval(() => {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30000) // 每 30 秒发一次心跳
}
function stopHeartbeat() {
clearInterval(heartbeatTimer)
}
function reconnect() {
if (retryCount >= maxRetries) {
console.error('达到最大重试次数,放弃重连')
return
}
// 指数退避:1s, 2s, 4s, 8s, 16s
const delay = Math.pow(2, retryCount) * 1000
retryCount++
console.log(`${delay / 1000}s 后重连 (第 ${retryCount} 次)`)
setTimeout(connect, delay)
}
connect()
return {
send: (data: unknown) => ws?.send(JSON.stringify(data)),
close: () => { ws?.close(); stopHeartbeat() },
}
}5. 服务端实现(Node.js)
用 ws 库在 Node.js 上搭建一个简单的聊天室服务:
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
// 存储所有连接的客户端
const clients = new Set()
wss.on('connection', (ws) => {
clients.add(ws)
console.log(`新连接,当前在线: ${clients.size}`)
ws.on('message', (raw) => {
const data = JSON.parse(raw.toString())
if (data.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }))
return
}
// 广播消息给所有在线客户端
if (data.type === 'message') {
const broadcast = JSON.stringify({
type: 'message',
user: data.user,
text: data.text,
time: Date.now(),
})
for (const client of clients) {
if (client !== ws && client.readyState === 1) {
client.send(broadcast)
}
}
}
})
ws.on('close', () => {
clients.delete(ws)
console.log(`连接断开,当前在线: ${clients.size}`)
})
})生产建议:ws 库只有 ~3KB(gzip 后),无依赖,性能极好,适合需要精细控制的场景。如果你需要 Room 管理、命名空间等高级功能,可以考虑 Socket.IO,但要清楚它的额外开销。
6. 生产环境注意事项
负载均衡:Sticky Session
WebSocket 是有状态的长连接。如果后端有多个实例,必须确保同一个客户端的连接始终路由到同一个实例。Nginx 可以用 ip_hash 或基于 Cookie 的 sticky session 实现。也可以用 Redis Pub/Sub 在实例间同步消息,避免 sticky session 的限制。
认证方式
WebSocket 握手阶段走的是 HTTP,所以可以通过 Cookie 或 URL 参数传 token(ws://host/chat?token=xxx)。但注意 URL 参数会出现在日志里,敏感 token 建议放在握手后的第一条消息中,或使用 Cookie。
Nginx 代理配置
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s; # 24h,防止 Nginx 主动断开
}7. 何时该用 WebSocket,何时不该
适合 WebSocket
- • 即时聊天、在线客服
- • 多人协作编辑(文档、白板)
- • 实时游戏状态同步
- • 金融行情推送、实时交易
- • IoT 设备状态监控
用 SSE 或轮询更合适
- • 服务器单向推送通知(用 SSE)
- • 低频数据更新(每分钟一次,轮询就够了)
- • 不需要客户端主动发消息的场景
- • AI 流式输出(SSE 是标准方案)
相关阅读
想深入了解浏览器端的多线程编程?推荐阅读 Web Worker 多线程实战:让浏览器不再卡顿。了解更多浏览器底层原理?看看 浏览器渲染原理。