技术原理2026-03-24 12 分钟

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 多线程实战:让浏览器不再卡顿。了解更多浏览器底层原理?看看 浏览器渲染原理