WebSocket、SSE、MQTT 在工业实时看板中的落地对比

4 阅读8分钟

最近在做一个工业园区的实时监控大屏项目,需要展示几百个设备的实时数据。技术选型时在 WebSocket、SSE、MQTT 之间纠结了很久,踩了不少坑。今天把实战经验整理出来,希望能帮到有类似需求的朋友。

一、三种方案的本质差异

1.1 WebSocket:全双工的"对讲机"

WebSocket 是基于 TCP 的全双工通信协议。建立连接后,客户端和服务器可以随时互相发消息,就像两个人拿着对讲机聊天。

核心特点:

  • 双向通信:客户端能主动推送,服务器也能主动推送
  • 低延迟:建立连接后无需 HTTP 握手,数据帧传输开销小
  • 持久连接:一次握手,长期有效

适用场景:

  • 实时聊天、在线协作
  • 需要客户端频繁向服务器发送指令的场景
  • 实时游戏、交易系统

1.2 SSE:单向的"广播电台"

Server-Sent Events (SSE) 是基于 HTTP 的单向推送技术。服务器像广播电台一样,客户端只能收听,不能回话。

核心特点:

  • 单向通信:只能服务器推送到客户端
  • 基于 HTTP:利用现有基础设施,实现简单
  • 自动重连:断线后浏览器会自动重连
  • 文本数据:只支持 UTF-8 文本

适用场景:

  • 新闻推送、股票行情
  • 物流跟踪、订单状态更新
  • 监控大屏(只看不操作)

1.3 MQTT:物联网的"消息总线"

MQTT 是专为物联网设计的轻量级发布/订阅协议。它像一个消息中转站,设备发布消息到主题(Topic),订阅者接收消息。

核心特点:

  • 发布/订阅模式:解耦发送方和接收方
  • QoS 保证:三级服务质量,确保消息可靠送达
  • 轻量级:协议头只有 2 字节,适合低带宽环境
  • 支持遗嘱消息:设备异常断线时自动通知

适用场景:

  • 物联网设备通信
  • 工业现场数据采集
  • 车联网、智能家居

二、工业看板场景的实战对比

我们的项目需求:

  • 300+ 设备实时数据展示
  • 数据刷新频率:1-5 秒
  • 支持历史数据查询
  • 偶尔需要下发控制指令

2.1 性能对比

维度WebSocketSSEMQTT
并发连接数10000+10000+50000+
单连接延迟10-50ms50-100ms20-80ms
服务器资源消耗
客户端资源消耗
网络带宽占用极低

实测数据(300 设备,每秒推送一次):

  • WebSocket:服务器 CPU 占用 15%,内存 800MB
  • SSE:服务器 CPU 占用 12%,内存 600MB
  • MQTT:Broker CPU 占用 8%,内存 400MB

2.2 开发复杂度对比

WebSocket:

// 客户端代码
const ws = new WebSocket('ws://localhost:8080');ws.onopen = () => {
  console.log('连接成功');
  // 订阅设备数据
  ws.send(JSON.stringify({ 
    action: 'subscribe', 
    devices: ['device001', 'device002'] 
  }));
};ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateChart(data); // 更新图表
};ws.onerror = (error) => {
  console.error('连接错误:', error);
};ws.onclose = () => {
  console.log('连接断开,尝试重连...');
  setTimeout(() => reconnect(), 3000);
};

优点:

  • 双向通信,可以随时下发控制指令
  • 浏览器原生支持,无需额外库

缺点:

  • 需要手动实现心跳、重连机制
  • 服务端需要维护连接状态
  • 负载均衡需要支持 WebSocket(Sticky Session)

SSE:

// 客户端代码
const eventSource = new EventSource('/api/device-stream');
​
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateChart(data);
};
​
eventSource.onerror = (error) => {
  console.error('SSE 错误:', error);
  // 浏览器会自动重连
};
​
// 服务端代码 (Node.js + Express)
app.get('/api/device-stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
​
  const interval = setInterval(() => {
    const data = getDeviceData(); // 获取设备数据
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);
​
  req.on('close', () => {
    clearInterval(interval);
  });
});

优点:

  • 实现极简,浏览器自动重连
  • 基于 HTTP,穿透防火墙容易
  • 服务器资源占用低

缺点:

  • 单向通信,下发指令需要额外接口
  • 只支持文本数据
  • HTTP/1.1 下有连接数限制(6 个)

MQTT:

// 客户端代码 (使用 MQTT.js)
import mqtt from 'mqtt';
​
const client = mqtt.connect('ws://localhost:8083/mqtt', {
  clientId: 'dashboard_' + Math.random().toString(16).substr(2, 8),
  clean: true,
  reconnectPeriod: 3000
});
​
client.on('connect', () => {
  console.log('MQTT 连接成功');
  // 订阅设备主题
  client.subscribe('devices/+/data', { qos: 1 });
});
​
client.on('message', (topic, payload) => {
  const data = JSON.parse(payload.toString());
  const deviceId = topic.split('/')[1];
  updateChart(deviceId, data);
});
​
// 下发控制指令
function sendCommand(deviceId, command) {
  client.publish(`devices/${deviceId}/command`, 
    JSON.stringify(command), 
    { qos: 1 }
  );
}

优点:

  • 发布/订阅模式,扩展性强
  • QoS 保证消息可靠性
  • 协议轻量,适合大规模设备
  • 支持主题通配符(+、#)

缺点:

  • 需要部署 MQTT Broker(如 EMQX、Mosquitto)
  • 学习曲线稍陡
  • Web 端需要 WebSocket 桥接

三、实战踩坑记录

3.1 WebSocket 的坑

坑 1:连接断开后数据丢失

工业现场网络不稳定,WebSocket 断开重连期间的数据会丢失。

解决方案:

class ReliableWebSocket {
  constructor(url) {
    this.url = url;
    this.messageQueue = []; // 离线消息队列
    this.lastMessageId = 0;
    this.connect();
  }
​
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      // 重连后请求丢失的数据
      this.ws.send(JSON.stringify({
        action: 'sync',
        lastMessageId: this.lastMessageId
      }));
      
      // 发送队列中的消息
      while (this.messageQueue.length > 0) {
        this.ws.send(this.messageQueue.shift());
      }
    };
​
    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.lastMessageId = data.messageId;
      this.handleMessage(data);
    };
​
    this.ws.onclose = () => {
      setTimeout(() => this.connect(), 3000);
    };
  }
​
  send(message) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      this.messageQueue.push(message);
    }
  }
}

坑 2:负载均衡问题

Nginx 默认配置下,WebSocket 连接会被分配到不同服务器,导致连接失败。

解决方案(Nginx 配置):

upstream websocket_backend {
    ip_hash; # 使用 IP 哈希保证同一客户端连接到同一服务器
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
}
​
server {
    location /ws {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600s; # 增加超时时间
    }
}

3.2 SSE 的坑

坑 1:HTTP/1.1 连接数限制

浏览器对同一域名的 HTTP/1.1 连接数有限制(Chrome 为 6 个),多个标签页会互相影响。

解决方案:

  • 升级到 HTTP/2(无连接数限制)
  • 使用不同子域名分散连接
  • 使用 SharedWorker 共享连接
// 使用 SharedWorker 共享 SSE 连接
// shared-sse-worker.js
let eventSource = null;
const ports = [];self.onconnect = (e) => {
  const port = e.ports[0];
  ports.push(port);
​
  if (!eventSource) {
    eventSource = new EventSource('/api/device-stream');
    eventSource.onmessage = (event) => {
      // 广播给所有标签页
      ports.forEach(p => p.postMessage(event.data));
    };
  }
​
  port.onmessage = (e) => {
    if (e.data === 'close') {
      const index = ports.indexOf(port);
      if (index > -1) ports.splice(index, 1);
      
      if (ports.length === 0) {
        eventSource.close();
        eventSource = null;
      }
    }
  };
};

坑 2:数据格式限制

SSE 只支持文本,传输二进制数据(如图片、文件)需要 Base64 编码,效率低。

解决方案:

  • 二进制数据走单独的 HTTP 接口
  • 或者改用 WebSocket

3.3 MQTT 的坑

坑 1:Web 端需要 WebSocket 桥接

MQTT 原生基于 TCP,浏览器无法直接连接。需要 Broker 提供 WebSocket 支持。

解决方案(EMQX 配置):

# emqx.conf
listeners.ws.default {
  bind = "0.0.0.0:8083"
  max_connections = 10240
  websocket.mqtt_path = "/mqtt"
}

坑 2:主题设计不当导致性能问题

最初我们用了 devices/data 这样的单一主题,所有设备数据都往这里发,客户端收到大量无关数据。

错误示范:

// ❌ 所有设备共用一个主题
client.subscribe('devices/data');
// 客户端收到 300 个设备的数据,但只需要其中 10 个

正确做法:

// ✅ 按设备 ID 分主题
const myDevices = ['device001', 'device002', 'device003'];
myDevices.forEach(id => {
  client.subscribe(`devices/${id}/data`);
});
​
// ✅ 或使用通配符订阅特定区域
client.subscribe('devices/area-A/+/data'); // 只订阅 A 区域设备

坑 3:QoS 选择不当

QoS 0(最多一次):消息可能丢失,但性能最好 QoS 1(至少一次):消息保证送达,但可能重复 QoS 2(恰好一次):消息不丢不重,但性能最差

工业场景建议:

  • 实时数据(温度、压力):QoS 0(丢一两条无所谓)
  • 告警信息:QoS 1(必须送达,重复可去重)
  • 控制指令:QoS 2(不能重复执行)

四、选型建议

4.1 决策树

是否需要客户端主动发送数据?
├─   需要双向通信
   ├─ 设备数量 < 1000  WebSocket
   └─ 设备数量 > 1000  MQTT

└─   单向推送即可
    ├─ 实现简单优先  SSE
    ├─ 需要 QoS 保证  MQTT
    └─ 需要二进制数据  WebSocket

4.2 我们的最终方案

混合架构:MQTT + HTTP

  • 数据采集层: 设备通过 MQTT 上报数据到 Broker(QoS 0)

  • 数据处理层: 后端服务订阅 MQTT,处理后存入时序数据库

  • 前端展示层:

    • 实时数据:WebSocket 订阅(从后端服务推送,已过滤)
    • 历史数据:HTTP API 查询
    • 控制指令:HTTP POST 下发(后端转 MQTT)

架构图:

设备 --MQTT--> EMQX Broker --MQTT--> 后端服务 --WebSocket--> 前端大屏
                                    |
                                    +--> 时序数据库(InfluxDB)
                                    |
                                    <--HTTP-- 前端查询/控制

为什么这样设计?

  1. 设备到 Broker 用 MQTT:轻量、可靠、支持海量设备
  2. 后端到前端用 WebSocket:可以做数据过滤、聚合,减少前端压力
  3. 控制指令走 HTTP:便于权限校验、日志记录

4.3 性能优化技巧

1. 数据聚合

// 后端聚合多个设备数据,减少推送频率
const dataBuffer = {};
const FLUSH_INTERVAL = 1000; // 1 秒推送一次
​
mqttClient.on('message', (topic, payload) => {
  const deviceId = topic.split('/')[1];
  dataBuffer[deviceId] = JSON.parse(payload.toString());
});
​
setInterval(() => {
  if (Object.keys(dataBuffer).length > 0) {
    wss.clients.forEach(client => {
      client.send(JSON.stringify(dataBuffer));
    });
    dataBuffer = {};
  }
}, FLUSH_INTERVAL);

2. 增量更新

// 只推送变化的数据
let lastSnapshot = {};
​
function pushUpdate(newData) {
  const changes = {};
  for (const [key, value] of Object.entries(newData)) {
    if (lastSnapshot[key] !== value) {
      changes[key] = value;
    }
  }
  
  if (Object.keys(changes).length > 0) {
    ws.send(JSON.stringify({ type: 'update', data: changes }));
    lastSnapshot = { ...lastSnapshot, ...changes };
  }
}

3. 客户端节流

// 前端限制更新频率,避免页面卡顿
let updateTimer = null;
let pendingData = null;ws.onmessage = (event) => {
  pendingData = JSON.parse(event.data);
  
  if (!updateTimer) {
    updateTimer = setTimeout(() => {
      updateChart(pendingData);
      pendingData = null;
      updateTimer = null;
    }, 100); // 最多 100ms 更新一次
  }
};

五、总结

三种方案各有千秋:

  • WebSocket: 双向通信的万金油,适合大部分实时场景
  • SSE: 单向推送的轻量选择,实现简单但功能受限
  • MQTT: 物联网的专业选手,大规模设备首选

工业场景的建议:

  • 小型项目(< 100 设备):WebSocket 足够
  • 中型项目(100-1000 设备):WebSocket 或 MQTT 都可以
  • 大型项目(> 1000 设备):MQTT + 后端聚合 + WebSocket 推送

最后提醒:不要过度设计,根据实际需求选择最简单够用的方案。我们项目最初用 MQTT 直连前端,结果发现 300 个设备根本用不到 MQTT 的优势,反而增加了复杂度。后来改成 WebSocket 直连,开发效率提升了一倍。