直接用浏览器读取工业传感器数据,首当其冲的问题是浏览器无法建立原始TCP连接,而Modbus TCP恰恰基于TCP 502端口。但这不是死路,借助一个轻量的WebSocket转TCP网关,前端完全可以用jQuery封装Modbus TCP协议帧,通过WebSocket发送二进制报文,再由网关转发给实际的传感器或PLC设备。这样做的好处是前端代码保持轻量,不需要引入复杂的工业通信库,就能在网页上展示温度、压力、流量等寄存器数据。

一、Modbus TCP报文结构与浏览器限制
Modbus TCP报文由两部分组成:MBAP头和PDU。MBAP头固定7个字节,包含事务标识符、协议标识符、长度和单元标识符。事务标识符占2字节,由客户端生成,用于匹配请求与响应;协议标识符固定为0x0000;长度占2字节,表示后续字节数;单元标识符占1字节,在大多数网关场景下可以填0x01。PDU部分则与标准Modbus一致,例如读取保持寄存器使用功能码0x03,后面紧跟2字节起始地址和2字节寄存器数量。
浏览器本身无法直接向502端口发起TCP连接,所以必须经过一个中间层。常见做法是使用Node.js的net模块编写一个WebSocket服务,监听前端连接,再把收到的二进制数据原样写入到Modbus TCP设备的Socket中。也可以使用现成的协议转换工具,但自己写一个几十行的网关更灵活,也便于调试。前端只负责生成正确的请求帧、发送到WebSocket、接收二进制响应并解析。
jQuery在前端的作用不是直接处理TCP,而是简化DOM更新、事件绑定和异步流程。可以用jQuery的Deferred对象封装WebSocket的发送与接收逻辑,让读取函数返回一个Promise风格的对象,这样业务代码里写起来更接近同步流程,减少回调嵌套。
二、用jQuery风格封装Modbus TCP请求帧
先定义一个函数,用于构建读取保持寄存器的请求帧。函数接收起始地址、寄存器数量和事务ID,返回一个ArrayBuffer。MBAP头的前两个字节写入事务ID,使用DataView可以方便地控制大端字节序,因为Modbus TCP的网络字节序就是大端。
下面这段代码展示了请求帧的生成过程。注意长度字段的计算:从单元标识符开始到PDU结束,一共6字节。地址和数量都是2字节无符号整数,写入时使用setUint16并指定big-endian。
function buildReadHoldingRegistersRequest(transactionId, startAddress, quantity) {
const buffer = new ArrayBuffer(12);
const view = new DataView(buffer);
// MBAP头:事务标识符(2字节)
view.setUint16(0, transactionId);
// 协议标识符(2字节),固定为0
view.setUint16(2, 0);
// 长度(2字节):单元标识符 + PDU = 1 + 5 = 6
view.setUint16(4, 6);
// 单元标识符(1字节)
view.setUint8(6, 0x01);
// PDU:功能码03(1字节)
view.setUint8(7, 0x03);
// 起始地址(2字节)
view.setUint16(8, startAddress);
// 寄存器数量(2字节)
view.setUint16(10, quantity);
return buffer;
}
这里的事务ID需要在一个请求周期内唯一,通常用一个自增变量即可。为了避免与网关通信时产生匹配错误,可以在发送前保存事务ID与回调的对应关系。地址字段一般从0开始,但很多文档使用40001这样的编号,实际发送时要减去40001或根据设备手册计算偏移。
如果读取的数据类型是32位浮点数或32位整数,需要一次读取两个寄存器,即数量填2。发送时起始地址和数量都要符合设备支持的寄存器范围,否则设备会返回异常码。可以用jQuery的$.Deferred封装发送函数,让调用方使用done和fail处理成功与失败。
function readHoldingRegisters(socket, startAddress, quantity) {
const deferred = $.Deferred();
const transactionId = (window.__modbusTransactionId = (window.__modbusTransactionId || 0) + 1);
// 保存事务ID对应的Deferred,用于匹配响应
window.__modbusPendingRequests = window.__modbusPendingRequests || {};
window.__modbusPendingRequests[transactionId] = deferred;
const requestBuffer = buildReadHoldingRegistersRequest(transactionId, startAddress, quantity);
try {
socket.send(requestBuffer);
} catch (error) {
delete window.__modbusPendingRequests[transactionId];
deferred.reject(error);
}
// 设置超时,避免请求悬挂
setTimeout(function () {
if (window.__modbusPendingRequests[transactionId]) {
delete window.__modbusPendingRequests[transactionId];
deferred.reject(new Error('Modbus request timeout'));
}
}, 3000);
return deferred.promise();
}
这段代码把WebSocket发送的二进制数据与jQuery的Deferred结合起来。发送成功后,响应到达时会根据事务ID找到对应的Deferred并resolve。超时机制保证在设备无响应的情况下前端能及时感知,避免界面一直显示加载中。
三、通过WebSocket桥接发送并解析响应
前端需要先建立WebSocket连接。连接地址指向网关服务,例如ws://127.0.0.1:8080。网关收到前端发来的二进制帧后,用net模块连接到Modbus TCP设备的502端口,并把数据转发过去。反过来,设备返回的数据也由网关原样回传给前端。这样前端WebSocket的message事件中拿到的就是纯净的Modbus TCP响应帧。
解析响应帧同样使用DataView。响应帧的MBAP头与请求帧结构相同,事务标识符必须与请求一致,否则应丢弃。长度字段指示后续字节数,PDU第一个字节是功能码,正常应该是0x03。如果功能码的最高位被置1,说明是异常响应,后续一个字节是异常码,常见的有0x02非法数据地址、0x03非法数据值等。
function parseReadHoldingRegistersResponse(buffer) {
const view = new DataView(buffer);
// 检查长度是否足够
if (buffer.byteLength < 9) {
throw new Error('Response too short');
}
const transactionId = view.getUint16(0);
const protocolId = view.getUint16(2);
const length = view.getUint16(4);
const unitId = view.getUint8(6);
const functionCode = view.getUint8(7);
if (protocolId !== 0) {
throw new Error('Invalid protocol identifier');
}
// 异常响应:功能码最高位为1
if (functionCode & 0x80) {
const exceptionCode = view.getUint8(8);
throw new Error('Modbus exception code: ' + exceptionCode);
}
if (functionCode !== 0x03) {
throw new Error('Unexpected function code: ' + functionCode);
}
const byteCount = view.getUint8(8);
const registerData = [];
for (let i = 0; i < byteCount; i += 2) {
registerData.push(view.getUint16(9 + i));
}
return {
transactionId: transactionId,
unitId: unitId,
registers: registerData
};
}
解析出的寄存器值默认是16位无符号整数,存储在数组中。如果传感器数据是32位浮点数,需要把两个相邻的16位寄存器组合成一个32位值。例如使用两个寄存器的原始字节,按照大端顺序拼接后再用DataView读取为float。也可以根据设备手册判断字节序是ABCD还是CDAB,必要时进行字节交换。
WebSocket的message事件里收到的是Blob或ArrayBuffer,需要转换为ArrayBuffer后再解析。下面这段代码把响应匹配到之前的Deferred,并完成整个读取流程。
socket.onmessage = function (event) {
const reader = new FileReader();
reader.onload = function () {
const buffer = reader.result;
try {
const view = new DataView(buffer);
const transactionId = view.getUint16(0);
const pending = window.__modbusPendingRequests[transactionId];
if (!pending) {
return; // 不是我们等待的请求
}
delete window.__modbusPendingRequests[transactionId];
const result = parseReadHoldingRegistersResponse(buffer);
pending.resolve(result);
} catch (error) {
// 解析失败时无法知道事务ID,需要做兜底处理
console.error('Failed to parse response', error);
}
};
reader.readAsArrayBuffer(event.data);
};
上面用到FileReader把Blob转成ArrayBuffer,如果网关直接发送ArrayBuffer,可以跳过这一步。注意异常处理时,如果响应帧本身无法解析,就无法对应到具体事务ID,这时可以在代码里把超时机制作为最终防线。
四、完整示例与常见问题
把前面的函数组合起来,就能写一个完整的HTML页面。页面里包含一个按钮,点击后读取地址0开始的2个寄存器,并把结果显示在页面上。为了演示方便,这里假设网关运行在本地8080端口,Modbus TCP设备地址为127.0.0.1:502。
下面是一个简化版的完整页面源码,所有代码都放在一个HTML文件中。注意在浏览器中直接打开时,WebSocket连接ws://127.0.0.1:8080需要网关服务已启动,否则连接会失败。实际部署时可以把网关地址改为局域网IP或域名。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Modbus TCP前端读取示例</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<button id="readBtn">读取传感器数据</button>
<p>寄存器值:<span id="result">--</span></p>
<script>
$(function () {
var socket = new WebSocket('ws://127.0.0.1:8080');
socket.binaryType = 'arraybuffer';
socket.onopen = function () {
console.log('WebSocket connected');
};
socket.onerror = function (error) {
console.error('WebSocket error', error);
};
$('#readBtn').on('click', function () {
readHoldingRegisters(socket, 0, 2)
.done(function (result) {
var values = result.registers.join(', ');
$('#result').text(values);
})
.fail(function (error) {
$('#result').text('读取失败: ' + error.message);
});
});
});
</script>
</body>
</html>
这段代码演示了jQuery事件绑定和DOM更新的基本用法。实际项目中,按钮点击可以换成定时器轮询,例如每5秒读取一次,实现类似实时监控的效果。定时器触发时要注意上一次请求是否完成,避免堆积。可以用一个布尔变量标记请求进行中,或用Deferred的链式调用控制节奏。
常见问题之一是字节序。Modbus TCP网络字节序是大端,但有些传感器内部存储使用小端,导致读到的数值看起来很大或为负数。遇到这种情况,可以在解析后手动交换高低字节,或在组合32位值时调整寄存器顺序。另一个常见问题是事务标识符溢出,虽然2字节最大65535,但长时间运行后可以从0重新开始,只要保证同一时刻没有重复即可。
网关实现也需要留意,TCP连接可能会被设备断开,需要增加重连机制。前端WebSocket断开后也要在onclose事件里尝试重连,否则页面会静默失去数据更新能力。此外,读取寄存器的地址和数量必须在设备允许范围内,越界访问会返回异常码0x02,前端解析异常码后应给出明确提示,方便现场排查。
五、优化读取流程与异常处理
实际工业场景中,一个页面往往需要读取多个传感器或多个寄存器区域。可以为每个读取任务分配独立的事务ID,并维护一个请求队列。不要同时发送过多请求,因为Modbus TCP设备通常是串行处理,并发过高只会增加网关压力,甚至导致设备响应超时。
优化方向上,可以把读取函数进一步封装成一个轮询器,支持批量读取、失败重试和自动恢复。用jQuery的$.when可以并行等待多个Deferred,但要注意Modbus设备是否支持快速连续请求。一般建议使用串行队列,每次只发送一个请求,完成后再发下一个,这样对设备最友好。
异常处理除了超时和异常码,还要覆盖WebSocket连接断开、网关无响应、数据帧不完整等情况。前端可以设置一个全局状态指示器,当连续多次读取失败时,在界面上显示设备离线提示,并停止轮询,等网络恢复后再手动或自动重新连接。
还有一点值得注意,jQuery的$.Deferred虽然好用,但在现代浏览器中也可以直接使用原生Promise。保持jQuery风格是为了与旧项目集成方便,如果团队已经全面使用ES6,可以把Deferred替换成Promise,整体逻辑不变。核心的协议帧构建和解析代码与jQuery无关,完全可以复用。
通过以上步骤,前端工程师就能在不依赖专业组态软件的情况下,用轻量的Web技术栈实现对Modbus TCP工业传感器的数据读取。方案的关键在于理解报文结构、搭建好WebSocket桥接、做好字节序和异常处理,剩下的工作就是在前端页面里展示数据了。
Modbus TCPjQuery工业传感器修改时间:2026-10-06 00:13:33