WebSocket 实时通信

从 HTTP 轮询到 WebSocket:掌握浏览器与服务器的全双工实时通信技术


学习目标

读完本文,你将学会:

  • 理解 WebSocket 与 HTTP 轮询的本质区别
  • 掌握 WebSocket API 的完整使用方法
  • 实现心跳检测、断线重连等生产级功能
  • 对比 WebSocket、SSE、长轮询的适用场景

一、为什么需要 WebSocket

1.1 HTTP 的局限性

HTTP 是请求-响应模型:客户端发起请求,服务器才能响应。服务器无法主动向客户端推送数据。

客户端 ──请求──▶ 服务器
客户端 ◀──响应── 服务器

(服务器无法主动说"有新消息了")

1.2 实时通信方案的演进

方案 原理 延迟 资源消耗 适用场景
短轮询 客户端定时发送 HTTP 请求 取决于轮询间隔 高(大量无效请求) 简单场景
长轮询 服务器挂起请求直到有数据 接近实时 中(连接保持) 兼容旧浏览器
SSE (Server-Sent Events) HTTP 长连接,服务器单向推送 股票行情、新闻推送
WebSocket TCP 全双工连接 极低 聊天、游戏、协同编辑
短轮询:  请求→响应  请求→响应  请求→响应  (频繁往返)
长轮询:  请求→等待→响应  请求→等待→响应  (减少部分往返)
SSE:     连接建立→服务器持续推送→推送→推送  (单向)
WebSocket: 握手一次→双向自由通信  (全双工)

1.3 WebSocket 的优势

  • 全双工通信:客户端和服务器可同时发送数据
  • 低延迟:建立连接后无需重复握手
  • 低开销:数据帧头仅 2-14 字节,远小于 HTTP 头
  • 跨域支持:原生支持 CORS

二、WebSocket 协议基础

2.1 握手过程

WebSocket 连接始于一次 HTTP 升级请求:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

之后,TCP 连接从 HTTP 协议切换为 WebSocket 协议。

2.2 数据帧结构

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)  |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - -+
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+
  • FIN:是否为最后一帧
  • opcode:操作码(1=文本,2=二进制,8=关闭,9=ping,10=pong)
  • MASK:客户端发送的数据必须掩码
  • Payload len:负载长度(0-125 直接表示,126 则用后续 2 字节,127 用 8 字节)

三、WebSocket API 详解

3.1 创建连接

const ws = new WebSocket('wss://echo.websocket.org/');

// 连接成功
ws.onopen = () => {
  console.log('连接已建立');
  ws.send('Hello Server!');
};

// 接收消息
ws.onmessage = (event) => {
  console.log('收到:', event.data);
};

// 连接关闭
ws.onclose = (event) => {
  console.log('连接关闭', event.code, event.reason);
};

// 发生错误
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error);
};

3.2 连接状态

console.log(ws.readyState);
// CONNECTING = 0  连接中
// OPEN       = 1  已连接
// CLOSING    = 2  关闭中
// CLOSED     = 3  已关闭

3.3 发送数据

// 发送文本
ws.send('文本消息');

// 发送二进制(Blob)
const blob = new Blob(['二进制数据'], { type: 'text/plain' });
ws.send(blob);

// 发送 ArrayBuffer
const buffer = new ArrayBuffer(8);
ws.send(buffer);

3.4 关闭连接

// 正常关闭(code 1000 表示正常关闭)
ws.close(1000, '用户主动关闭');

常用关闭码:

状态码 含义
1000 正常关闭
1001 终端离开(如浏览器关闭)
1006 异常关闭(连接意外断开)
1008 策略违反
1011 服务器错误

四、生产级 WebSocket 封装

4.1 心跳检测

长时间无数据传输,中间代理可能断开连接。心跳机制保持连接活跃:

class HeartbeatWebSocket {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.heartbeatInterval = 30000; // 30 秒
    this.heartbeatTimer = null;
  }

  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      this.startHeartbeat();
    };
    
    this.ws.onmessage = (event) => {
      if (event.data === 'pong') return; // 忽略心跳响应
      this.onMessage?.(event.data);
    };
    
    this.ws.onclose = () => {
      this.stopHeartbeat();
    };
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
      }
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    clearInterval(this.heartbeatTimer);
  }
}

4.2 断线重连

class ReconnectWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.attempts = 0;
    this.ws = null;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      this.attempts = 0;
      console.log('连接成功');
    };

    this.ws.onclose = () => {
      this.attemptReconnect();
    };

    this.ws.onerror = () => {
      this.ws.close();
    };
  }

  attemptReconnect() {
    if (this.attempts >= this.maxReconnectAttempts) {
      console.error('重连次数已达上限');
      return;
    }
    this.attempts++;
    console.log(`${this.attempts} 秒后尝试重连...`);
    setTimeout(() => this.connect(), this.reconnectInterval);
  }
}

完整代码见 CODE-ADVANCED/10-WebSocket实时通信/websocket-reconnect.js


五、实战案例:实时聊天室

5.1 需求分析

  • 多用户实时收发消息
  • 显示在线用户列表
  • 消息历史记录

5.2 客户端实现

const chat = {
  ws: null,
  username: '',

  connect(name) {
    this.username = name;
    this.ws = new WebSocket('wss://chat.example.com');

    this.ws.onopen = () => {
      this.send({ type: 'join', username: name });
    };

    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      switch (msg.type) {
        case 'message': this.displayMessage(msg); break;
        case 'userList': this.updateUserList(msg.users); break;
        case 'system': this.displaySystem(msg.text); break;
      }
    };
  },

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  },

  sendMessage(text) {
    this.send({ type: 'message', text, from: this.username });
  }
};

完整代码见 CODE-ADVANCED/10-WebSocket实时通信/websocket-chat.html


六、WebSocket vs SSE vs 长轮询

┌─────────────────┬──────────────┬──────────────┬──────────────┐
│     特性        │  WebSocket   │     SSE      │   长轮询     │
├─────────────────┼──────────────┼──────────────┼──────────────┤
│ 通信方向        │ 双向         │ 服务器→客户端 │ 双向(伪)   │
│ 协议            │ TCP (ws/wss) │ HTTP         │ HTTP         │
│ 实时性          │ 极高         │ 高           │ 中           │
│ 浏览器兼容      │ IE10+        │ IE 不支持    │ 全部         │
│ 自动重连        │ 需手动实现   │ 原生支持     │ 需手动实现   │
│ 二进制数据      │ 支持         │ 不支持       │ 需 Base64   │
│ 多域名连接      │ 有限制       │ 无限制       │ 无限制       │
│ 防火墙穿透      │ 可能受阻     │ 易穿透       │ 易穿透       │
└─────────────────┴──────────────┴──────────────┴──────────────┘

选择建议

  • 需要双向实时(聊天、游戏)→ WebSocket
  • 仅需服务器推送(股票、日志)→ SSE
  • 需要兼容旧浏览器 → 长轮询

二、常见误区与注意点

误区 正确做法
WebSocket 可以替代所有 HTTP WebSocket 用于实时场景,REST API 仍适合 CRUD
连接永不关闭 页面切换/网络变化时连接会断开,需处理重连
不需要考虑兼容性 IE9 及以下不支持,需准备降级方案
所有数据都用 WebSocket 文件上传等大流量操作仍用 HTTP 更合适
忽略错误处理 必须处理 onerror、onclose,实现重连策略

三、动手练习

练习 1:心跳检测实现

在基础 WebSocket 连接上添加心跳机制,如果 3 次心跳无响应则认为连接已断开,触发重连。

练习 2:简易协同白板

使用 WebSocket 实现多人实时绘图:鼠标移动时发送坐标,其他客户端实时绘制。


四、AI 辅助学习

4.1 本节知识点的 AI 提问模板

  • “WebSocket 的 Sec-WebSocket-Key 是如何生成的?”
  • “如何在 WebSocket 中实现消息确认机制(ACK)?”
  • “WebSocket 连接数有浏览器限制吗?”

4.2 用 AI 验证你的理解

尝试向 AI 描述 WebSocket 握手过程,让它判断你的理解是否正确。

4.3 警惕 AI 的常见错误

  • AI 可能混淆 WebSocket 和 Socket.IO(后者是库,不是协议)
  • AI 可能忘记提到 WebSocket 的掩码机制

五、配套代码

本文示例代码位于:CODE-ADVANCED/10-WebSocket实时通信/

文件名 说明
websocket-basic.html WebSocket API 基础演示
websocket-chat.html 实时聊天室完整实现
websocket-reconnect.js 断线重连与心跳封装

六、本章小结

  • WebSocket 通过 HTTP 升级握手建立 TCP 全双工连接
  • 心跳检测和断线重连是生产环境的必备功能
  • WebSocket 适合双向实时场景,SSE 适合单向推送
  • 注意处理连接状态、错误情况和浏览器兼容性

如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐