前端视角下关于 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 在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率
相关推荐
jump68012 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信12 小时前
我们需要了解的Web Workers
前端
brzhang12 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
huangdengji12 小时前
基于openresty反向代理、dns劫持、实现对http请求、响应内容抓包
网络协议·http·openresty
yivifu12 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花12 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋12 小时前
场景模拟:基础路由配置
前端
六月的可乐12 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐13 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
記億揺晃着的那天13 小时前
解密 HTTPS:从握手到安全通信
网络协议·安全·https·ssl证书