跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScriptNode.js大前端

WebSocket 客户端实践:重连、心跳与可靠通信

从零搭建一个健壮的 WebSocket 客户端,涵盖自动重连、心跳保活和消息分发,避免原生 API 在断开、空闲回收和逻辑散乱方面的常见陷阱。给出基础封装类、React 集成示例,并扩展到认证、指数退避重试和消息队列等生产常用变体。同时提供 Node.js 服务端简单实现及前端多连接管理方案,最后讨论何时该用 WebSocket 以及运维注意事项。

魔尊发布于 2026/6/300 浏览

搞实时通信,WebSocket 算是标配。但直接裸用原生 API 会有不少暗坑:连接断了不会自动重连,长时间闲置被代理或防火墙掐掉,消息一多处理逻辑就散得没法维护。这篇文章准备从零搭一个足够健壮的 WebSocket 客户端,涵盖重连、心跳、消息分发,再加几个生产环境常用的变体。

原生 WebSocket 的尴尬

下面这种写法估计很多人都用过:

const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => socket.send('Hello');
socket.onmessage = (e) => console.log(e.data);
socket.onclose = () => console.log('closed');
socket.onerror = (e) => console.error(e);

问题明显:

  • 断开后悄无声息,没有任何重连尝试。
  • 没有心跳,中间链路可能会悄悄回收连接。
  • onmessage 里的 if/else 会随着消息类型增加迅速膨胀。
  • 错误处理基本等于没写。

所以我们需要自己封装一层。

基础连接类

先把重连、心跳和消息处理器包装起来。核心思路:用一个 WebSocketClient 类管理生命周期,通过 on(type, handler) 注册不同类型的消息回调,内部维护重连次数和心跳定时器。

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.connected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.reconnectDelay = 1000;
    this.messageHandlers = {};
    this.heartbeatInterval = null;
  }

  connect() {
    this.socket = new WebSocket(this.url);
    this.socket.onopen = () => {
      console.log('connected');
      this.connected = true;
      this.reconnectAttempts = 0;
      this.startHeartbeat();
    };
    this.socket.onmessage = (event) => {
      this.handleMessage(event.data);
    };
    this.socket.onclose = () => {
      console.log('disconnected');
      this.connected = false;
      this.stopHeartbeat();
      this.reconnect();
    };
    this.socket.onerror = (error) => {
      console.error('error:', error);
    };
  }

  reconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`reconnecting... ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
      setTimeout(() => this.connect(), this.reconnectDelay * this.reconnectAttempts);
    } else {
      console.error('max reconnect attempts reached');
    }
  }

  startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      if (this.connected) {
        this.send({ type: 'heartbeat' });
      }
    }, 30000);
  }

  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  send(data) {
    if (this.connected) {
      this.socket.send(JSON.stringify(data));
    } else {
      console.error('not connected');
    }
  }

  on(type, handler) {
    if (!this.messageHandlers[type]) {
      this.messageHandlers[type] = [];
    }
    this.messageHandlers[type].push(handler);
  }

  handleMessage(data) {
    try {
      const message = JSON.parse(data);
      const { type, payload } = message;
      if (this.messageHandlers[type]) {
        this.messageHandlers[type].forEach(handler => handler(payload));
      }
    } catch (error) {
      console.error('parse error:', error);
    }
  }

  disconnect() {
    this.stopHeartbeat();
    if (this.socket) {
      this.socket.close();
    }
  }
}

使用起来也简单:

const ws = new WebSocketClient('ws://localhost:8080');
ws.connect();
ws.on('chat', (payload) => console.log('chat:', payload));
ws.on('notification', (payload) => console.log('notif:', payload));
ws.send({ type: 'chat', payload: { text: 'hi' } });

在 React 里集成

有了上面的类,放到 React 里只需要在 effect 中初始化,并记得卸载时断开。

import React, { useEffect, useCallback, useRef, useState } from 'react';

function WebSocketComponent() {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const wsRef = useRef(null);

  useEffect(() => {
    wsRef.current = new WebSocketClient('ws://localhost:8080');
    wsRef.current.connect();
    wsRef.current.on('chat', (payload) => {
      setMessages(prev => [...prev, payload]);
    });
    return () => wsRef.current?.disconnect();
  }, []);

  const handleSend = useCallback(() => {
    if (input.trim() && wsRef.current) {
      wsRef.current.send({ type: 'chat', payload: { text: input, user: 'me' } });
      setInput('');
    }
  }, [input]);

  return (
    <div>
      <div className="messages">
        {messages.map((m, i) => (
          <div key={i}><strong>{m.user}:</strong> {m.text}</div>
        ))}
      </div>
      <div className="input-area">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
        />
        <button onClick={handleSend}>Send</button>
      </div>
    </div>
  );
}

常见扩展

实际项目里往往还需要一些额外机制,这几个是出现频率很高的。

带认证

把 token 拼在 URL 参数里是最直接的方式。

class AuthWebSocketClient extends WebSocketClient {
  constructor(url, token) {
    super(url);
    this.token = token;
  }
  connect() {
    this.socket = new WebSocket(`${this.url}?token=${this.token}`);
    // 其他逻辑完全复用父类
  }
}

指数退避重试

固定间隔重连在服务器压力大时不太友好,退避策略可以让重连间隔逐渐拉长。

class RetryWebSocketClient extends WebSocketClient {
  constructor(url, options = {}) {
    super(url);
    this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.exponentialBackoff = options.exponentialBackoff ?? true;
  }
  reconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = this.exponentialBackoff
        ? this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
        : this.reconnectDelay;
      console.log(`reconnecting in ${delay}ms`);
      setTimeout(() => this.connect(), delay);
    }
  }
}

消息队列

连接还没建立时,可以把要发的消息先存起来,连上后再一次性刷出去,避免丢消息。

class QueueWebSocketClient extends WebSocketClient {
  constructor(url) {
    super(url);
    this.messageQueue = [];
  }
  connect() {
    super.connect();
    this.socket.onopen = () => {
      console.log('connected');
      this.connected = true;
      this.reconnectAttempts = 0;
      this.startHeartbeat();
      this.flushQueue();
    };
  }
  send(data) {
    if (this.connected) {
      this.socket.send(JSON.stringify(data));
    } else {
      this.messageQueue.push(data);
    }
  }
  flushQueue() {
    if (this.connected && this.messageQueue.length > 0) {
      console.log(`flushing ${this.messageQueue.length} messages`);
      this.messageQueue.forEach(m => this.socket.send(JSON.stringify(m)));
      this.messageQueue = [];
    }
  }
}

服务端的一个简单例子

前端写得再稳,服务端也得配合。下面是一个基于 Node.js ws 库的服务,处理心跳并广播消息。

const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
const clients = new Set();

server.on('connection', (socket) => {
  console.log('client connected');
  clients.add(socket);
  socket.send(JSON.stringify({ type: 'system', payload: 'welcome' }));

  socket.on('message', (data) => {
    try {
      const message = JSON.parse(data);
      if (message.type === 'heartbeat') {
        socket.send(JSON.stringify({ type: 'heartbeat' }));
        return;
      }
      clients.forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(JSON.stringify(message));
        }
      });
    } catch (e) {
      console.error('invalid message', e);
    }
  });

  socket.on('close', () => {
    clients.delete(socket);
    console.log('client disconnected');
  });

  socket.on('error', (err) => console.error('socket error', err));
});

前端多连接管理

如果一个页面需要连多个 WebSocket 端点,手动维护很烦,可以用一个单例管理器来统一创建和回收。

class WebSocketManager {
  static instance = null;
  constructor() {
    this.clients = {};
  }
  static getInstance() {
    if (!WebSocketManager.instance) {
      WebSocketManager.instance = new WebSocketManager();
    }
    return WebSocketManager.instance;
  }
  createClient(name, url, options = {}) {
    const client = new QueueWebSocketClient(url, options);
    this.clients[name] = client;
    client.connect();
    return client;
  }
  getClient(name) {
    return this.clients[name];
  }
  removeClient(name) {
    this.clients[name]?.disconnect();
    delete this.clients[name];
  }
  disconnectAll() {
    Object.values(this.clients).forEach(c => c.disconnect());
    this.clients = {};
  }
}

还得注意的事

  • 错误监控:可以给连接类加个错误计数器,连续报错超过阈值就主动断开避免资源泄露,同时上报日志。
  • 不要为了用而用:WebSocket 适合需要服务端主动推送、频繁小数据交换的场景。如果只是偶尔拉一次服务端状态,轮询就够了,实现简单,排障也容易。
  • 生产环境走 wss,证书配置和反向代理(如 Nginx)的支持都别忘了。
  • 心跳间隔可以和服务端协商,别太短加重负载,太长起不到保活作用。

这些封装并不复杂,但能避免不少线上'连接断了、消息丢了'的尴尬。直接用第三方库当然也行,理解背后的这些机制,出问题的时候你才知道该往哪查。

目录

  1. 原生 WebSocket 的尴尬
  2. 基础连接类
  3. 在 React 里集成
  4. 常见扩展
  5. 带认证
  6. 指数退避重试
  7. 消息队列
  8. 服务端的一个简单例子
  9. 前端多连接管理
  10. 还得注意的事
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 从零搭建一个能调用 API 的 AI Agent
  • 汉诺塔问题的递归与非递归 C++ 解法
  • 7款国内AI助手横评:豆包、元宝、千问、Kimi、DeepSeek、MiniMax、GLM
  • 2026 算法求职:为什么我劝你深耕多模态大模型
  • 2023年网络安全趋势观察:十个绕不开的方向
  • 用 MGeo 和 Neo4j 搭建中文地址语义知识图谱
  • Temperature 和 Top-P 调参手记:从输出翻车到稳定产出的经验
  • 在 OpenHarmony 上跑通 tflite_web:WASM 推理适配要点
  • pycdc 上手指南:从 .pyc 还原 Python 源码
  • OpenClaw + MCP:给自托管 AI 助手接上任意工具
  • Spring Boot 自动配置:原理、条件注解与手写 Starter
  • GLM 语言模型拆解:从概率图到 PyTorch 代码
  • Java泛型实用理解:擦除、通配符与限制
  • Copilot 指令文件解析:copilot-instructions.md vs AGENTS.md vs .instructions.md
  • 逐字回复怎么实现?大模型 Stream 流式输出在 LangChain 中的实践
  • 新能源汽车电机热网络温度预测模型技术解析
  • 昇腾平台 DeepSeek-R1 与 Qwen2.5 强化学习训练优化实践
  • Git 入门实战:配置、提交、版本回退与文件恢复
  • MySQL 数据类型选型避坑实录
  • Stable Diffusion WebUI Forge 模型评估实战指南:三大核心指标解析

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online