5. 数据大屏实时通信第一步:为什么选择 WebSocket,以及如何接入 Socket.IO

数据大屏实时通信第一步:为什么选择 WebSocket,以及如何接入 Socket.IO

前言

前面几篇文章主要在聊地图大量点位的渲染优化:

text 复制代码
分层渲染
  ↓
聚合展示
  ↓
增量更新
  ↓
只更新变化点

这些优化解决的是:

text 复制代码
前端拿到数据之后,怎么更快地渲染到地图上。

但真实的数据大屏里,还有一个更靠前的问题:

text 复制代码
数据怎么实时到达前端?

如果数据链路不稳定,后面的地图渲染优化做得再多,也只能优化"已经到达前端的数据"。

比如无人机位置、机场状态、事件告警这些数据,本身就是持续变化的。如果还用传统接口一遍遍请求,不仅延迟高,而且会浪费很多请求。

所以在进入 WebSocket 稳定性优化之前,我先把第一步单独拆出来:

text 复制代码
为什么选 WebSocket?
为什么项目里用 Socket.IO?
如何初步和服务器建立连接?
第一版连接方案有哪些问题?

这一篇先解决"从 0 到 1 建立实时通信"的问题。下一篇再继续讲:

text 复制代码
心跳检测
退避重连
断点续传
消息去重

为什么选择 WebSocket

在做数据大屏之前,我也考虑过几种常见的实时数据方案:

text 复制代码
HTTP 轮询
  ↓
定时请求接口

SSE
  ↓
服务端单向推送

WebSocket
  ↓
客户端和服务端双向通信

不同方案适合不同场景。

如果只是普通后台管理系统,比如每隔 10 秒刷新一次列表,HTTP 轮询其实就够了。

但数据大屏不一样。

我的场景里有这些特点:

text 复制代码
1. 内网部署
2. 大屏常驻展示
3. 无人机、机场、事件等状态需要实时变化
4. 数据延迟最好控制在 200ms 以内
5. 后端会持续推送增量数据
6. 前端不仅要接收消息,还要告诉服务端当前消费进度

如果用 HTTP 轮询,大概会变成这样:

js 复制代码
setInterval(async () => {
  const res = await getLatestData();
  updateScreen(res.data);
}, 1000);

这个方案简单,但问题也很明显。

如果轮询间隔太长:

text 复制代码
数据延迟高,大屏不够实时。

如果轮询间隔太短:

text 复制代码
大量无效请求,服务端和前端都有压力。

更关键的是,轮询天然是客户端主动问:

text 复制代码
有新数据吗?
有新数据吗?
有新数据吗?

但真实业务里,服务端才知道什么时候有新数据。

所以更合适的方式应该是:

text 复制代码
服务端有变化,就主动推给前端。

这就是 WebSocket 更适合实时大屏的原因。

WebSocket 建立连接后,客户端和服务端之间会保持一条长连接。后续数据可以直接通过这条连接推送:

text 复制代码
服务端产生无人机位置变化
  ↓
通过 WebSocket 推给前端
  ↓
前端转成 marker patch
  ↓
地图局部更新

相比轮询,它的优势是:

text 复制代码
1. 延迟更低
2. 不需要频繁建立 HTTP 请求
3. 服务端可以主动推送
4. 前端也可以主动发送 ack、心跳、lastSeq
5. 更适合持续在线的数据大屏

为什么不直接用 SSE

SSE 也是一种服务端推送方案。

它的特点是:

text 复制代码
服务端 → 客户端

也就是服务端单向推送。

如果只是通知类消息,比如:

text 复制代码
订单状态变化
系统通知
任务进度

SSE 是可以考虑的。

但我的大屏场景里,前端也需要给服务端发送信息:

text 复制代码
心跳
当前 lastSeq
消息 ack
订阅开关
业务过滤条件

也就是说,它不是单纯的"服务端告诉前端",而是需要双向通信:

text 复制代码
服务端推送业务数据
前端反馈连接状态和消费进度

所以最终没有选择 SSE,而是选择 WebSocket。

简单总结就是:

text 复制代码
普通列表刷新:HTTP 轮询可以接受
服务端单向通知:SSE 可以考虑
实时大屏 + 双向通信 + 低延迟:WebSocket 更合适

为什么选择 Socket.IO

这里还要区分一个概念:

text 复制代码
WebSocket 是浏览器原生协议能力。
Socket.IO 是基于实时通信封装出来的一套库。

项目里实际使用的是:

js 复制代码
socket.io-client

也就是 Socket.IO 客户端。

刚开始我也可以直接用浏览器原生 WebSocket:

js 复制代码
const socket = new WebSocket("ws://example.com");

socket.onopen = () => {};
socket.onmessage = (event) => {};
socket.onclose = () => {};
socket.onerror = () => {};

原生 WebSocket 的优点是:

text 复制代码
协议标准
依赖少
链路更直接

但它也比较"薄"。

很多工程化能力需要自己补:

text 复制代码
重连
事件命名
消息分发
连接状态管理
兼容降级
命名空间
房间/订阅
服务端配套能力

而当前项目后端已经是 Socket.IO 这一套通信方式,前端自然也使用:

js 复制代码
import io from "socket.io-client";

我选择继续使用 Socket.IO,主要是因为:

text 复制代码
1. 和现有后端协议匹配,改造成本最低
2. 支持事件名通信,比如 uavMessage、dockMessage
3. 连接、断开、错误事件封装更完整
4. 页面代码更接近业务语义

比如原生 WebSocket 收到消息后,通常还要自己解析消息类型:

js 复制代码
socket.onmessage = (event) => {
  const packet = JSON.parse(event.data);

  if (packet.type === "uavMessage") {
    handleUavMessage(packet);
  }

  if (packet.type === "dockMessage") {
    handleDockMessage(packet);
  }
};

Socket.IO 可以直接按事件监听:

js 复制代码
socket.on("uavMessage", handleUavMessage);
socket.on("dockMessage", handleDockMessage);

这对业务大屏来说更直观。

不过也要注意一点:

text 复制代码
Socket.IO 不等于原生 WebSocket。

如果服务端是原生 WebSocket 服务,前端不能直接用 Socket.IO 客户端连接。两边协议要匹配。

当前项目里前后端已经约定使用 Socket.IO,所以我的选型不是重新造一套实时通信,而是在现有 Socket.IO 的基础上继续增强:

text 复制代码
Socket.IO 负责连接和事件通信
业务层负责心跳、断点续传、消息去重和增量消费

初步建立 WebSocket 连接

在项目里,我先封装了一个最简单的 SocketManager

它做的事情很少:

text 复制代码
连接服务器
登录鉴权
监听事件
发送事件
断开连接

初始版本大概是这样:

js 复制代码
import io from "socket.io-client";

class SocketManager {
  constructor() {
    this.socket = null;
  }

  connect(url, options = {}) {
    const defaultOptions = {
      path: "/web.socket-new",
      autoConnect: true,
      transports: ["websocket"],
    };

    const socketOptions = { ...defaultOptions, ...options };
    this.socket = io(url, socketOptions);

    return this;
  }

  disconnect() {
    this.socket.disconnect();
  }

  login() {
    if (this.socket) {
      this.socket.emit("login", {
        token: window.localStorage.getItem("thirdToken"),
        clientId: Math.random(),
        uavSwitch: true,
      });
    }
  }

  on(event, handler) {
    if (this.socket) {
      this.socket.on(event, handler);
    }
  }

  emit(event, data) {
    if (this.socket) {
      this.socket.emit(event, data);
    }
  }
}

const socketManager = new SocketManager();

export default socketManager;

这里几个配置比较关键。

path

js 复制代码
path: "/web.socket-new"

表示 Socket.IO 连接服务端时使用的路径,需要和后端配置保持一致。

autoConnect

js 复制代码
autoConnect: true

表示创建 socket 实例后自动连接。

transports

js 复制代码
transports: ["websocket"]

表示强制使用 WebSocket 传输。

Socket.IO 默认可能会有轮询等传输方式。为了让大屏实时链路更明确,我这里直接指定:

text 复制代码
只走 websocket。

页面里如何使用

有了 SocketManager 之后,页面里就不用直接操作 io() 了。

页面只需要关心:

text 复制代码
什么时候连接
什么时候登录
监听哪些业务消息
收到消息后怎么更新页面
页面销毁时怎么断开

代码大概是这样:

js 复制代码
import socketManager from "@/utils/socketManager";

export default {
  mounted() {
    socketManager.connect("http://xxxxxx.xxxx.com");

    socketManager.on("connect", () => {
      socketManager.login();
    });

    socketManager.on("dockMessage", (data) => {
      const hangarData = data.message;
      this.insertHangarData(hangarData);
    });

    socketManager.on("uavMessage", (data) => {
      const liveData = data.message;
      this.inisertUavData(liveData);
    });
  },

  beforeDestroy() {
    socketManager.disconnect();
  },
};

这段逻辑可以拆成几步。

第一步,连接服务器:

js 复制代码
socketManager.connect("http://shangcheng.cldeye.com");

第二步,连接成功后登录:

js 复制代码
socketManager.on("connect", () => {
  socketManager.login();
});

这里的 login 不是页面登录,而是 WebSocket 连接建立后的业务鉴权。

它会把本地保存的 token 发给服务端:

js 复制代码
socket.emit("login", {
  token: window.localStorage.getItem("thirdToken"),
  clientId: Math.random(),
  uavSwitch: true,
});

服务端校验通过后,再开始推送业务消息。

第三步,监听机场消息:

js 复制代码
socketManager.on("dockMessage", (data) => {
  const hangarData = data.message;
  this.insertHangarData(hangarData);
});

第四步,监听无人机消息:

js 复制代码
socketManager.on("uavMessage", (data) => {
  const liveData = data.message;
  this.inisertUavData(liveData);
});

这样,一个最小可用的实时通信链路就建立起来了。

初始链路是什么样

整体流程可以画成这样:

text 复制代码
页面进入
  ↓
socketManager.connect
  ↓
Socket.IO 与服务端建立连接
  ↓
触发 connect
  ↓
前端发送 login
  ↓
服务端校验 token
  ↓
服务端持续推送 dockMessage / uavMessage
  ↓
前端更新大屏数据

这也是很多实时大屏第一版最常见的写法。

它的优点是:

text 复制代码
接入成本低
代码结构清楚
业务事件语义明确
可以快速验证实时数据链路

但第一版只解决了:

text 复制代码
正常网络下,怎么把服务端消息推到前端。

它还没有解决:

text 复制代码
断线怎么办?
重连怎么办?
断线期间的数据怎么办?
重连后收到重复消息怎么办?
页面反复进入导致重复监听怎么办?

第一版会遇到的问题

正常网络下,这套代码可以跑。

但一旦进入弱网、断网、重连场景,就会暴露出几个问题。

连接断了只能被动等待

第一版里没有心跳检测。

如果网络出现假连接:

text 复制代码
浏览器以为连接还在
服务端其实已经不可用
业务消息不再推送
页面数据停留在旧状态

前端很难及时发现。

重连策略不可控

如果完全依赖默认重连,或者自己写简单立即重连:

text 复制代码
disconnect
  ↓
connect
  ↓
disconnect
  ↓
connect

在弱网环境下,可能会给服务端造成很大压力。

尤其是多个大屏客户端同时断线、同时恢复时,服务端会被集中重连打到。

重连后容易全量刷新

很多第一版代码在重连后会重新拉一次全量数据:

text 复制代码
重连成功
  ↓
重新请求全量数据
  ↓
清空地图
  ↓
重新渲染

这样虽然简单,但是页面会闪烁,地图点位也会被重复创建。

前面做过的增量更新,也会被这一步绕开。

断线期间消息可能丢失

假设前端收到:

text 复制代码
seq = 100

然后断线了。

断线期间服务端产生:

text 复制代码
seq = 101, 102, 103

如果没有断点续传,前端重连后可能直接收到最新消息:

text 复制代码
seq = 104

中间的 101, 102, 103 就丢了。

补发消息可能重复消费

为了避免消息丢失,服务端可能会在重连后补发一段历史消息。

比如前端最后消费到:

text 复制代码
seq = 100

服务端为了保险,从:

text 复制代码
seq = 98

开始补发。

这时候前端会重复收到:

text 复制代码
98, 99, 100

如果前端没有去重,就可能重复更新数据,导致状态错乱。

下一步怎么优化

所以第一版 WebSocket 接入,只是把实时通信链路跑通。

它解决的是:

text 复制代码
服务端怎么把消息推给前端。

但对于数据大屏来说,还需要继续解决:

text 复制代码
连接是否还活着?
断线后什么时候重连?
重连时从哪里继续消费?
重复消息怎么跳过?
收到消息后怎么避免全量刷新?

下一步就要把这套最小可用连接升级成:

text 复制代码
心跳检测
  ↓
退避重连
  ↓
断点续传
  ↓
消息去重
  ↓
增量消费

这也就是下一篇要讲的 WebSocket 稳定性优化。

总结

这一步的核心不是一上来就把 WebSocket 写得很复杂,而是先把选型和基础链路想清楚。

在实时大屏场景里:

text 复制代码
HTTP 轮询简单,但延迟和无效请求问题明显。
SSE 适合服务端单向推送,但不适合需要频繁双向交互的大屏。
WebSocket 更适合低延迟、持续在线、双向通信的实时数据场景。
Socket.IO 则是在 WebSocket 能力之上,提供了更适合业务开发的事件通信模型。

项目第一版通过 socket.io-client 建立连接:

text 复制代码
connect
  ↓
login
  ↓
监听 dockMessage / uavMessage
  ↓
更新大屏

这样可以快速完成实时数据接入。

但它还不是一个足够稳定的大屏实时通道。

真正上线到弱网、内网、常驻大屏环境后,还要继续补上:

text 复制代码
心跳检测
退避重连
断点续传
消息去重
增量更新

也就是下一篇要继续优化的内容。

相关推荐
岁月留痕1681 小时前
17 实战项目二:实现“日记”项目多页面管理
前端
李顿波1 小时前
Chrome 插件弹窗一直停留在初始的小尺寸 —— 你看到的小方块
前端·javascript·chrome
悟空瞎说1 小时前
Cesium 与 Three.js 融合实战:在数字地球上渲染自定义 3D 场景
前端
渣波1 小时前
React 移动端首页架构实战:从并发请求到防御性编程的深度解析
前端·javascript
a1117761 小时前
原生 Markdown 阅读与编辑器 开源项目
前端·开源·软件
laity171 小时前
python发光表白爱心(从零到一实现)
前端·后端
程序员爱钓鱼1 小时前
Rust impl详解:为Struct定义方法与关联函数
前端·后端·rust
用户059540174461 小时前
Qdrant 召回不一致踩坑实录:跑了 300 次测试才发现是索引没刷新
前端·css
Highcharts.js2 小时前
Highcharts 主流前端框架无缝集成指南
前端·vue.js·前端框架·highcharts·可视化图表