最稳妥方案是“Modbus主站程序+WebSocket服务端”双进程协作,主站负责协议解析与设备通信,WS服务仅分发数据;须用本地Socket桥接、带设备上下文的消息格式、服务端订阅过滤,且寄存器变更需同步更新前端配置。

直接上结论:Modbus 设备数据要实时推送到 Web 看板,**不能靠前端轮询,也不该让浏览器直连 Modbus TCP 服务**;最稳妥的路径是「Modbus 主站程序 + WebSocket 服务端」双进程协作,中间用内存共享或轻量消息队列桥接,WebSocket 仅负责分发,不参与协议解析。
为什么不能用浏览器直接 WebSocket 连 Modbus 设备
Modbus 协议本身不支持 WebSocket 封装——它没有 WebSocket 握手、帧格式、ping/pong 机制。常见错误现象包括:
- 浏览器
new WebSocket('ws://192.168.1.100:502')直接报ERR_CONNECTION_REFUSED或WebSocket opening handshake was canceled,因为 502 端口是 Modbus TCP 默认端口,不是 WebSocket 服务 - 强行用
websockify做透明代理(如websockify 8765 192.168.1.100:502),会导致 Modbus 功能码被截断或 CRC 校验失败——websockify默认转发原始字节流,但 Modbus TCP 报文头含事务标识符、协议标识符等字段,需保持会话上下文,而 WebSocket 连接是无状态的 - 即使短时连通,也无法处理 Modbus 的超时重试、连接保活、多从站复用等工业现场刚需
Modbus 主站程序必须独立运行,且需主动推送数据到 WS 服务
真正可行的架构是:一个常驻进程作为 Modbus 主站(读 PLC/仪表),另一个进程作为 WebSocket 服务端(供前端订阅)。两者之间用可靠通道通信,推荐以下方式:
-
推荐用本地 Unix Domain Socket 或 TCP loopback:比 HTTP API 更低延迟,避免 JSON 序列化开销。例如主站向
127.0.0.1:15554发送二进制结构体或紧凑 JSON,WS 服务监听该地址并广播给已连接的客户端 - 避免用 Redis Pub/Sub:虽能解耦,但引入额外依赖和网络跳转,在边缘设备(如树莓派)上资源占用高,且消息无序风险真实存在
- 切勿在 WebSocket
@OnMessage回调里同步调用 Modbus 读取——Spring Boot 的@ServerEndpoint或 Swoole 的on('message')是单线程事件回调,阻塞即卡死整个 WS 连接池 - 主站程序应按设备分组维护连接池,对每个 Modbus 从站使用独立 TCP 连接+心跳,失败后自动降级重连,而不是共用一个 socket
WebSocket 消息格式必须带设备上下文,不能只传 raw value
前端看板需要知道“哪个设备、哪个寄存器、什么时间、值是多少”,否则无法做状态着色、趋势绘图、告警联动。典型错误是只推送 {"value": 25.6},结果多个传感器混在一起,前端无法区分。
建议统一使用如下结构(可压缩为 MsgPack 减少带宽):
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
{
"device_id": "plc-001",
"protocol": "modbus-tcp",
"address": "40001",
"type": "holding-register",
"value": 2560,
"timestamp": 1748047240123,
"unit": "℃"
}
-
address字段必须是 Modbus 地址规范写法(如40001表示保持寄存器第 1 个,而非偏移 0),方便前端映射到配置表 - 不要把
value做单位换算后传——换算逻辑(如 2560 → 25.6℃)应由主站程序完成,并在unit字段明确标注,避免前端二次计算出错 - 如果设备支持批量读(如功能码 0x03 一次读 10 个寄存器),主站应合并为单条 JSON 数组推送,减少 WS 频次
前端订阅需绑定设备 ID 和寄存器范围,避免全量广播
一个产线可能有上百台设备,但单个看板页面通常只关注其中几台。若 WS 服务对所有连接广播全部数据,会造成:
- 浏览器 CPU 持续解析无用消息,滚动卡顿
- 移动端流量浪费严重(尤其 4G 环境)
- 无法实现权限隔离(如班组长只能看本班组设备)
正确做法是客户端首次连接后,立刻发送订阅请求:
socket.send(JSON.stringify({
"action": "subscribe",
"devices": ["plc-001", "sensor-003"],
"registers": ["40001", "30005"]
}));
WS 服务端据此维护每个 socket 的 Map<string set>></string> 订阅表,只转发匹配的消息。这个逻辑必须在服务端做,不能靠前端过滤。
真正容易被忽略的是:**Modbus 寄存器地址变更(如设备升级后寄存器偏移调整)必须同步更新前端订阅列表,否则看板数据就“失联”了——这不是代码问题,是工程配置闭环问题。**

















