WebSocket学习+心跳检测+断线重连

在 HTTP协议中,客户端必须先发起请求,服务器才能响应。这就像发邮件,你发一封,对方回一封。但如果需要开发一个即时聊天室股票大盘或者实时视频监控的功能,让客户端每隔几秒钟问一次服务器“有新消息吗?”(轮询),既浪费带宽,延迟又高。WebSocket是解决这个问题的最优解。

一、 什么是 WebSocket?

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

  • HTTP: 单向传输。只能由客户端发起,服务端响应。
  • WebSocket: 双向传输。建立连接后,服务器和客户端可以随时向对方发送数据。

它的最大优点是:建立连接后,数据传输不仅实时,而且头部开销极小(不再需要像 HTTP 那样每次都带上一大堆 Header)。

二、 它是如何工作的?

WebSocket 并不是凭空建立的,它借用了 HTTP 来“搭桥”。

  1. 发起请求:客户端发送一个标准的 HTTP GET 请求,但在 Header 里带上 Upgrade: websocket,告诉服务器:“我想把协议升级为 WebSocket”。
  2. 协议切换:服务器如果支持,会返回 101 Switching Protocols 状态码。
  3. 通道建立:此时,HTTP 连接中断,底层的 TCP 连接保持打开,双方开始使用 WebSocket 的二进制帧格式传输数据。

三、 生产环境中的痛点:连接“假死”

在本地开发(localhost)时,WebSocket 看起来非常稳定。但在真实的公网环境中,情况要复杂得多:

  1. 网络波动:手机进入电梯、WiFi 切换 4G,连接随时可能断开。
  2. 僵尸连接(Half-Open):有时候网线被拔掉,或者中间的路由器为了节省资源默默切断了连接,但客户端和服务器都不知道,双方还以为连接连着,结果发消息谁也收不到。

为了解决这个问题,我们需要引入心跳检测(Heartbeat)和断线重连机制。

心跳检测:定期向服务器发送Ping,接收服务器传回来的Pong,如果没有在规定时间内返回,就认为连接已经断开

四、 封装一个带有心跳检测和短线重连的 WebSocket 类

  1. 定期发送 Ping:证明我还活着。
  2. 倒计时等待 Pong:如果发了 Ping 之后 3 秒内没收到回复,判定连接已断,主动关闭。
  3. 自动重连:连接断开后,延迟几秒尝试重新连接。
class WebSocketManager {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.lockReconnect = false;  // 避免重复重连
        
        // 心跳配置
        this.heartbeatConfig = {
            pingTimeout: 5000,    // 每 5秒发送一次心跳
            pongTimeout: 3000,    // 发送心跳后,3秒内没收到回复视为断线
            pingMsg: 'ping',
        };
        this.pingTimer = null;
        this.pongTimer = null;
    }

    connect() {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
        try {
            this.ws = new WebSocket(this.url);
            this.initEventHandle();
        } catch (e) {
            this.reconnect();
        }
    }

    initEventHandle() {
        this.ws.onopen = () => {
            console.log('连接成功');
            this.startHeartbeat(); // 开启心跳
        };

        this.ws.onmessage = (event) => {
            // 收到任何消息(包括 pong),都说明连接正常,重置心跳
            this.startHeartbeat();
            
            if (event.data !== 'pong') {
                console.log('收到业务数据:', event.data);
            }
        };

        this.ws.onclose = () => {
            console.log('连接断开');
            this.stopHeartbeat();
            this.reconnect(); // 尝试重连
        };
    }

    reconnect() {
        if (this.lockReconnect) return;
        this.lockReconnect = true;
        
        // 设置延迟,避免频繁请求炸服
        setTimeout(() => {
            this.connect();
            this.lockReconnect = false;
        }, 3000);
    }

    startHeartbeat() {
        this.stopHeartbeat(); // 清除旧定时器
        
        this.pingTimer = setTimeout(() => {
            if(this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(this.heartbeatConfig.pingMsg); // 发送 ping
                
                // 发送后开启倒计时,如果超时未响应则关闭连接
                this.pongTimer = setTimeout(() => {
                    this.ws.close(); 
                }, this.heartbeatConfig.pongTimeout);
            }
        }, this.heartbeatConfig.pingTimeout);
    }

    stopHeartbeat() {
        clearTimeout(this.pingTimer);
        clearTimeout(this.pongTimer);
    }
}
// 使用
const ws = new WebSocketManager('ws://localhost:8080');
ws.connect();