最近在做一个工业园区的实时监控大屏项目,需要展示几百个设备的实时数据。技术选型时在 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 性能对比
| 维度 | WebSocket | SSE | MQTT |
|---|---|---|---|
| 并发连接数 | 10000+ | 10000+ | 50000+ |
| 单连接延迟 | 10-50ms | 50-100ms | 20-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-- 前端查询/控制
为什么这样设计?
- 设备到 Broker 用 MQTT:轻量、可靠、支持海量设备
- 后端到前端用 WebSocket:可以做数据过滤、聚合,减少前端压力
- 控制指令走 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 直连,开发效率提升了一倍。