前端视角下关于 WebSocket 的简单理解

参考 RFC 6455: The WebSocket Protocol

WebSocket 协议基础

  • 协议本质:在单个 TCP 连接上提供全双工通信通道的协议
  • 核心优势:
    • 双向实时通信(服务器主动推送)
    • 低延迟(相比 HTTP 轮询)
    • 高效数据传输(减少 HTTP 头部开销)
  • 协议握手:
bash 复制代码
# 来自客户端的握手数据
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
复制代码
- Connection:设置 Upgrade,表示客户端希望连接升级
- Upgrade:设置 websocket,表示希望升级到 Websocket 协议
- Sec-WebSocket-Key:客户端发送的一个 base64 编码的密文,用于简单的认证秘钥。要求服务端必须返回一个对应加密的 Sec-WebSocket-Accept 应答,否则客户端会抛出错误,并关闭连接
- Sec-WebSocket-Protocol:子协议选择, 标识客户端支持的协议
- Sec-WebSocket-Version :表示支持的 Websocket 版本
- Sec-WebSocket-Extensions:户端期望使用的协议级别的扩展
bash 复制代码
# 服务端的握手响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
复制代码
- HTTP/1.1 101 Switching Protocols:表示服务端接受 WebSocket 协议的客户端连接
- Sec-WebSocket-Accept:验证客户端请求报文,同样也是为了防止误连接。具体做法是把请求头里 Sec-WebSocket-Key 的值,加上一个专用的 UUID,再计算摘要
  • 结束握手:任何一端都可以发送一个包含特定关闭握手的控制帧数据。收到此帧后,另一端在不发送任何数据后会发送一个结束帧作为响应。收到另一端的结束帧后,最开始发送控制帧的端在没有数据需要发送时,就会安全的关闭此连接。在发送了一个表明连接需要被关闭的控制帧后,这个客户端不会再发送任何的数据;在收到一个表明连接需要被关闭的控制帧后,这个客户端会丢弃此后的所有数据。

WebSocket 帧结构

在 WebSocket 协议中,数据是通过一系列数据帧来进行传输的。为了避免安全问题,客户端必须在它发送到服务器的所有帧中添加掩码(Mask),服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。另外服务端禁止在发送数据帧给客户端时添加掩码,客户端如果收到了一个添加了掩码的帧,必须立即关闭连接。

bash 复制代码
      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 ...                |
     +---------------------------------------------------------------+

基础帧头

位偏移 字段名称 长度 详细说明
0 FIN 1 bit 标识是否为消息的最后一帧(1=最终帧,0=还有后续帧)
1-3 RSV1, RSV2, RSV3 各1 bit 保留位,必须为0(除非扩展协议定义特殊用途)
4-7 Opcode 4 bits 帧类型标识: 0x0=连续帧;0x1=文本帧;0x2=二进制帧;0x8=关闭帧; 0x9=PING;0xA=PONG
8 Mask 1 bit 是否使用掩码(客户端到服务器必须设为1)
9-15 Payload Length 7 bits 数据长度(实际值分三种情况): 0-125:实际长度;126:后续2字节表示长度;127:后续8字节表示长度

Opcode类型表

Hex 类型 描述
0x0 Continuation 连续帧(分片消息)
0x1 Text UTF-8文本数据
0x2 Binary 二进制数据
0x8 Close 连接关闭指令
0x9 Ping 心跳检测请求
0xA Pong 心跳检测响应

长度解码规则

Payload Length值 后续字节数 实际长度范围
0-125 0 0-125字节
126 2 126-65,535字节
127 8 65,536-2^64-1字节

控制帧特殊说明

所有的控制帧必须有一个 126 字节或者更小的负载长度,并且不能被分片

  1. Close帧(0x8):

    • 前2字节:状态码(如1000表示正常关闭)
    • 可选 UTF-8 原因短语
  2. Ping/Pong 帧(0x9/0xA):

    • 必须实现心跳应答机制
    • Pong 的 Payload 需与对应 Ping 一致

示例帧解析

Hello 文本帧原始字节(Hex):

bash 复制代码
81 85 37 FA 21 3D 7F 9F 4D 51 58

解析结果:

  • 81 → FIN=1, Opcode=0x1(文本帧)
  • 85 → Mask=1, Payload Length=5
  • 37 FA 21 3D → 掩码密钥
  • 7F 9F 4D 51 58 → 加密后的 "Hello"

前端 WebSocket 高可用框架设计

暂时没有设计日志和统计系统,项目地址 websocket-pro-client

组件关系图

WebSocketManager WebSocketClient TaskScheduler EventEmitter Heartbeat PriorityQueue

  • WebSocketManager:入口类,管理所有连接实例和共享资源
  • WebSocketClient:单个连接实例,处理连接生命周期和消息收发
  • Heartbeat:心跳检测管理,维护连接活性
  • TaskScheduler:任务调度器,控制并发消息发送
  • PriorityQueue:优先级队列,确保高优先级消息优先处理
  • EventEmitter:事件中心,统一处理所有连接事件

分层架构设计

外部服务 网络层 核心层 用户层 事件订阅 API调用 WebSocket Server Browser WebSocket Connection Pool WebSocketClient1 WebSocketClient2 Heartbeat Task Scheduler PriorityQueue EventEmitter UI Components WebSocketManager

架构说明

  1. 用户层

    • UI Components:业务组件,通过标准API与核心层交互
    • 事件流:通过EventEmitter实现松耦合通信
  2. 核心层

    • WebSocketManager:单例入口
    • Connection Pool:连接池维护策略:
      • 最大连接数限制(默认5个)
      • LRU(最近最少使用)淘汰机制
      • 相同URL自动复用连接
  3. 网络层

    • 封装原生WebSocket API,增加:
      • 自动重连装饰器
      • 二进制数据分片处理
      • CORS安全校验

数据流转过程

用户界面 WebSocketManager WebSocketClient 网络层 TaskScheduler connect('wss://api.example.com') 创建新连接 初始化WebSocket onopen 启动心跳检测 连接状态更新 'connected'事件 发送PING 返回PONG loop [心跳检测] send(data, priority) 添加发送任务 有序发送数据 onerror 触发重连流程 alt [网络异常] 用户界面 WebSocketManager WebSocketClient 网络层 TaskScheduler

详细设计说明

连接管理

连接状态机:
connect() onopen onerror/onclose close() onclose onerror/onclose disconnected connecting connected closing

连接池实现:

ts 复制代码
class WebSocketManager {
  private connectionPool: Map<string, WebSocketClient>;
  
  // 获取或创建连接
  public connect(url: string): WebSocketClient {
    if (this.connectionPool.has(url)) {
      return this.connectionPool.get(url)!; // 复用现有连接
    }
    
    const client = new WebSocketClient(url, this.config);
    this.connectionPool.set(url, client);
    return client;
  }
  
  // 关闭所有连接
  public closeAll(): void {
    this.connectionPool.forEach(client => client.close());
  }
}

连接池 vs 单连接

  • 优点:避免重复握手开销,支持多租户隔离
  • 缺点:增加内存占用,需要维护状态一致性

错误处理体系

错误类型 处理方式
连接错误 自动触发重连机制,累计重试次数
心跳超时 主动关闭连接并标记为异常断开,触发快速重连
消息发送失败 根据优先级存入队列,连接恢复后自动重发
协议错误 关闭连接并触发error事件,不自动重连

错误捕获示例

ts 复制代码
// 网络层错误捕获
socket.addEventListener('error', (event) => {
  this.status = 'disconnected';
  this.emit('error', {
    type: 'network',
    error: event,
    willReconnect: this.reconnectAttempts < this.config.maxReconnectAttempts
  });
  this.scheduleReconnect();
});

// 应用层错误处理
public send(data: any): Promise<void> {
  return new Promise((resolve, reject) => {
    if (this.status !== 'connected') {
      reject(new Error('Connection not ready'));
      return;
    }
    
    try {
      this.socket.send(data);
      resolve();
    } catch (error) {
      this.emit('error', {
        type: 'send',
        error,
        data
      });
      reject(error);
    }
  });
}

智能重连机制

重连算法流程:
Client Manager 连接断开 计算退避时间 尝试重连 连接恢复 增加重试计数 重新计算等待时间 alt [成功] [失败] loop [重试逻辑] Client Manager

核心代码:

ts 复制代码
private scheduleReconnect(): void {
  // 1. 检查重试上限
  if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
    this.emit('reconnect_failed', {
      attempts: this.reconnectAttempts,
      maxAttempts: this.config.maxReconnectAttempts
    });
    return;
  }

  // 2. 指数退避计算
  const baseDelay = this.config.reconnectDelay;
  const exponent = Math.pow(this.config.reconnectExponent, this.reconnectAttempts);
  const cappedDelay = Math.min(
    baseDelay * exponent,
    this.config.maxReconnectDelay
  );

  // 3. 添加随机抖动(避免集群同时重连的"惊群效应")
  const jitterRatio = 0.2; // ±20%的随机波动
  const jitter = cappedDelay * jitterRatio * (Math.random() * 2 - 1); // [-0.2,0.2]范围
  const actualDelay = Math.max(1000, cappedDelay + jitter); // 保证至少1秒

  // 4. 设置定时器
  this.reconnectTimer = setTimeout(() => {
    this.reconnectAttempts++;
    this.emit('reconnect', {
      attempt: this.reconnectAttempts,
      nextDelay: actualDelay
    });
    
    // 5. 实际重连操作
    this.connect()
  }, actualDelay);
}

指数退避公式

每次重试间隔 = min(初始延迟 * (退避系数^重试次数), 最大延迟)

bash 复制代码
# 初始值
initialDelay = 1000ms, 
exponent = 1.5, 
maxDelay = 30000ms

# 重试间隔增长示例:
第1次: 1000ms
第2次: 1500ms (1000*1.5^1)
第3次: 2250ms (1000*1.5^2)
...
第10次: 30000ms (达到上限)

另外可添加服务端过载保护:

ts 复制代码
if (this.reconnectAttempts > 3) {
  // 随机跳过1次重试
  if (Math.random() < 0.3) {
    this.reconnectAttempts++;
    this.scheduleReconnect();
    return;
  }
}

心跳检测系统

Client Server PING 收到PING后立即响应 PONG 启动超时计时器 重置计时器 标记连接异常 主动关闭 alt [正常响应] [超时未响应] Client Server

核心代码:

ts 复制代码
class Heartbeat {
  private lastPong: number = 0;
  
  public start(): void {
    this.intervalId = setInterval(() => {
      if (this.socket.readyState === WebSocket.OPEN) {
        this.socket.send('ping');
        this.timeoutId = setTimeout(() => {
          this.onTimeout(); // 心跳超时处理
        }, this.timeout);
      }
    }, this.interval);
  }
  
  public recordPong(): void {
    this.lastPong = Date.now();
    clearTimeout(this.timeoutId);
    this.emit('latency', Date.now() - this.lastPong);
  }
}

消息调度系统

优先级队列设计:

优先级 消息类型 默认权重
0 系统控制消息(如心跳) 最高
1 用户关键操作
2 普通数据更新
3 批量日志/非实时数据

优先级调度策略:

  • 严格优先级,适用于金融交易系统等
  • 加权轮询,适用于物联网数据采集等
  • 动态调整,适用于视频流传输等

调度器核心代码:

ts 复制代码
interface Task {
  task: () => Promise<void>;
  priority: number;
}

class PriorityQueue {
  private items: Task[] = [];

  public enqueue(task: Task): void {
    let added = false;
    for (let i = 0; i < this.items.length; i++) {
      if (task.priority > this.items[i].priority) {
        this.items.splice(i, 0, task);
        added = true;
        break;
      }
    }
    if (!added) {
      this.items.push(task);
    }
  }

  public dequeue(): Task | undefined {
    return this.items.shift();
  }

  public get length(): number {
    return this.items.length;
  }
}
class TaskScheduler {
  public addTask(task: () => Promise<void>, priority: number): Promise<void> {
    return new Promise((resolve, reject) => {
      this.queue.enqueue({
        task: async () => {
          try {
            await task();
            resolve();
          } catch (error) {
            reject(error);
          }
        },
        priority
      });
      this.run();
    });
  }

  private run(): void {
    while (this.runningTasks < this.maxConcurrent && this.queue.length > 0) {
      const { task } = this.queue.dequeue()!;
      this.runningTasks++;
      
      task().finally(() => {
        this.runningTasks--;
        this.run(); // 递归执行下一个任务
      });
    }
  }
}

性能优化策略

  • 使用ArrayBuffer传输图像数据
  • 消息压缩
  • 带宽自适应(自动适应从2G到5G的网络环境,基于网络类型调整策略,如心跳间隔,最大连接数等)

总结

WebSocket 是一种网络传输协议,位于 OSI 模型的应用层。可在单个 TCP 连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

特点

  • 全双工,通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合
  • 二进制帧,采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,相比 http/2,WebSocket 更侧重于"实时通信",而 http/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别。不像 http/2 那样定义流,也就不存在多路复用、优先级等特性,自身就是全双工,也不需要服务器推送
  • 协议名,引入 wswss 分别代表明文和密文的 WebSocket 协议,且默认端口使用 80 或 443,几乎与 http 一致
  • 握手,WebSocket 也要有一个握手过程,然后才能正式收发数据。

优点

  • 较少的控制开销:数据包头部协议较小,不同于 http 每次请求需要携带完整的头部
  • 更强的实时性:相对于 http 请求需要等待客户端发起请求服务端才能响应,延迟明显更少
  • 保持创连接状态:创建通信后,可省略状态信息,不同于 http 每次请求需要携带身份验证
  • 更好的二进制支持:定义了二进制帧,更好处理二进制内容
  • 支持扩展:用户可以扩展 WebSocket 协议、实现部分自定义的子协议
  • 更好的压缩效果:WebSocket 在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率
相关推荐
拾光拾趣录20 分钟前
基础 | 🔥闭包99%盲区?内存泄漏炸弹💣已埋!
前端·面试
拾光拾趣录41 分钟前
🔥前端性能优化9大杀招,第5招面试必挂?📉
前端·面试
用户21411832636021 小时前
dify案例分享-AI 助力初中化学学习:用 Qwen Code+Dify 一键生成交互式元素周期表网页
前端
上海大哥2 小时前
Flutter 实现工程组件化(Windows电脑操作流程)
前端·flutter
刘语熙2 小时前
vue3使用useVmode简化组件通信
前端·vue.js
XboxYan2 小时前
借助CSS实现一个花里胡哨的点赞粒子动效
前端·css
OEC小胖胖3 小时前
第七章:数据持久化 —— `chrome.storage` 的记忆魔法
前端·chrome·浏览器·web·扩展
OEC小胖胖3 小时前
第六章:玩转浏览器 —— `chrome.tabs` API 精讲与实战
前端·chrome·浏览器·web·扩展
不老刘3 小时前
基于clodop和Chrome原生打印的标签实现方法与性能对比
前端·chrome·claude·标签打印·clodop