JS-深度拆解 WebSocket:从握手原理到健壮的心跳重连机制

96 阅读4分钟

前言

在实时性要求极高的场景(如聊天室、金融行情、即时游戏)中,传统的 HTTP “请求-响应”模式显得捉襟见肘。WebSocket 作为 HTML5 推出的双向通信协议,成为了解决这一痛点的终极武器。

一、 什么是 WebSocket?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。

  • 双向性:服务器可以主动向客户端推数据,客户端也可以主动向服务器发数据。
  • ** 端口号**:默认端口也是80(ws)和443(wss)
  • 持久性:一旦连接建立,除非主动关闭,否则连接将一直保持。
  • 轻量级:数据包头部较小,减少了不必要的网络开销。

二、 连接过程:HTTP 的“变脸”艺术

WebSocket 并不是凭空产生的,它的建立依赖于 HTTP 协议的一次“升级”。

  1. TCP 三次握手:首先建立基础的 TCP 连接。

  2. 发送 Upgrade 请求:tcp连接成功后,客户端先向服务器发送一个GET请求,服务器根据请求头中的ConnectionUpgrade,来决定是否需要使用websocket协议:

    • Upgrade: websocket
    • Connection: Upgrade
  3. 服务器响应 101:如果服务器支持,返回状态码 101 Switching Protocols

  4. 协议转换:握手成功,接下来客户端和服务器可以随时主动发送消息给对方了。


三、 基础用法回顾

  • 使用webSocket构造函数创建连接,指定 ws(非加密)或 wss(加密)协议

  • 设置相关的监听函数:

    • onopen:连接建立时触发
    • onmessage:接收服务器消息(数据通过 event.data获取)
    • onerror:通信错误处理
    • onclose:连接关闭时触发
  • 设置发送与关闭方法

    • send()发送文本或二进制数据
    • close()主动终止连接
const webSocket = new WebSocket('ws://localhost:5001')
//连接成功后的回调
webSocket.onopen = function () {
  console.log('连接成功...')
  webSocket.send('Hello!')
}
//连接失败后的回调
webSocket.onerror = function () {
  console.log('连接失败...')
}
//从服务器接收到信息时的回调
webSocket.onmessage = function (event) {
  console.log('接收到消息...' + event.data)
}
//连接关闭后的回调
webSocket.onclose = function () {
  console.log('连接已关闭...')
}



四、 核心痛点:连接稳定性与心跳检测

1. 为什么需要心跳机制?

  • 网络沉默:某些网络设备(如防火墙、代理)会自动切断长时间没有数据流动的 TCP 连接。
  • 无感知断开:如果用户突然断网(如进入电梯),前端的 onclose 有时不会立即触发。如果不发数据,前端可能以为还连着。

2. 解决方案:Ping/Pong 机制

客户端每隔一段时间发送一个心跳包(Ping),服务端收到消息,并且还活着的话,就会发送一个数据包(Pong)给客户端,告诉自己还活着,说明链路通畅。


五、 实战:封装一个健壮的 WebSocket类

// WebSocket类
class SocketClient {
constructor(url, options = {}) {
  this.url = url;
  this.ws = null;
  this.lockReconnect = false; // 避免重复重连

  // 配置项
  this.options = {
    heartbeatInterval: 20000, // 心跳间隔
    reconnectInterval: 5000, // 重连间隔
    ...options,
  };

  this.heartbeatTimer = null; // 心跳定时器

  this.onMessageCallback = options.onMessage || null; //  接收消息回调
  this.onOpenCallback = options.onOpen || null; // 连接成功回调

  this.createWebSocket();
}

// 创建连接
createWebSocket() {
  this.close(false); // 清理旧实例和心跳

  try {
    this.ws = new WebSocket(this.url);
    this.initEventHandle();
  } catch (e) {
    console.error("WebSocket 初始化失败:", e);
    this.reConnect();
  }
}

// 初始化相关监听事件
initEventHandle() {
  this.ws.onopen = () => {
    console.log("WebSocket 连接成功");
    this.startHeartbeat();
    if (this.onOpenCallback) this.onOpenCallback();
  };

  this.ws.onmessage = (event) => {
    // 只要收到消息,就重置心跳
    this.resetHeartbeat();

    console.log("收到原始消息:", event.data);

    // 处理业务逻辑
    try {
      const data = JSON.parse(event.data);
      if (this.onMessageCallback) this.onMessageCallback(data);
    } catch (e) {
      if (this.onMessageCallback) this.onMessageCallback(event.data);
    }
  };

  this.ws.onerror = (error) => {
    console.error("WebSocket 异常:", error);
    this.reConnect();
  };

  this.ws.onclose = (event) => {
    console.log("WebSocket 已关闭:", event.code);
  };
}

//发送消息
send(data) {
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
    this.ws.send(
      typeof data === "string" ? data : JSON.stringify(data)
    );
  } else {
    console.warn("WebSocket 未连接,消息发送失败");
  }
}

//心跳管理
startHeartbeat() {
  this.heartbeatTimer = setTimeout(() => {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.send({ type: "heartbeat", timestamp: Date.now() });
      console.log("发送心跳...");
      this.startHeartbeat(); // 递归开启下一次心跳
    }
  }, this.options.heartbeatInterval);
}

//重置心跳
resetHeartbeat() {
  clearTimeout(this.heartbeatTimer);
  this.startHeartbeat();
}

// 重连逻辑,lockReconnect是为了防止在重连时间间隔内再次连接
reConnect() {
  if (this.lockReconnect) return;
  this.lockReconnect = true;

  console.log(
    `将在 ${this.options.reconnectInterval / 1000}s 后尝试重连...`
  );

  setTimeout(() => {
    this.lockReconnect = false;
    this.createWebSocket();
  }, this.options.reconnectInterval);
}

// 主动关闭, 是否永久关闭(不再重连)
close(permanent = true) {
  clearTimeout(this.heartbeatTimer);
  if (this.ws) {
    // 如果是永久关闭,移除所有监听器防止触发重连逻辑
    if (permanent) {
      this.ws.onclose = null;
      this.ws.onerror = null;
    }
    this.ws.close(); //调用ws close关闭连接
    this.ws = null;
  }
}
}

六、 总结与注意事项

  1. 安全性:在生产环境务必使用 wss:// (WebSocket over TLS),防止数据被中间人窃听。
  2. 同源策略:WebSocket 不受同源策略限制,但服务器可以通过 Origin 头部进行安全校验。
  3. 状态判定:在调用 send() 之前,务必检查 ws.readyState === WebSocket.OPEN (即 1),否则会报错。