4. Modbus TCP帧结构

Modbus TCP 并不是简单地把 Modbus RTU 的字节流塞进 TCP 连接,而是在原有 PDU 之外增加了一个固定 7 字节的 MBAP 报文头(Modbus Application Protocol Header)。MBAP 的存在,使 Modbus 从“串行链路上的主从轮询”升级为“以太网上的请求/响应事务模型”。理解 MBAP 的五个字段,是驱动开发中正确组包、拆包、校验和排障的基础。

一、MBAP 报文头的整体结构

一个完整的 Modbus TCP 报文由 MBAP 头(7 字节)和 PDU(协议数据单元)两部分组成。MBAP 头内部又划分为事务标识、协议标识、长度字段和单元标识四个逻辑字段,其中长度字段占 2 字节,因此实际是 5 个字段、7 个字节。PDU 则沿用 Modbus 原有的功能码 + 数据域结构,与 Modbus RTU 的 PDU 完全一致。

Modbus TCP 帧结构:MBAP 头 + PDU MBAP 报文头(7 字节) 事务标识 2 字节 协议标识 2 字节 = 0 长度字段 2 字节 单元标识 1 字节 (长度 覆盖区) PDU(协议数据单元) 功能码 1 字节 数据域 N 字节 长度字段 = 单元标识(1) + PDU 字节数 字节 0-1 字节 2-3 字节 4-5 字节 6 字节 7 起:PDU MBAP 头固定 7 字节,PDU 长度可变;长度字段是拆包的关键依据 所有多字节字段均采用大端序(Big-Endian)传输

二、五个字段逐一拆解

事务标识(Transaction Identifier,2 字节):由客户端生成,用于把请求和响应配对。客户端每发一个请求就递增或随机生成一个事务号,服务器在响应中原样回填。驱动开发中必须维护“事务号 → 待处理请求”的映射表,否则并发请求时无法判断响应属于哪一次调用。

协议标识(Protocol Identifier,2 字节):固定为 0x0000,表示这是 Modbus 协议。该字段的存在是为了让 MBAP 头具备通用性,未来可承载其他协议。驱动收到非 0 的协议标识时,应直接丢弃该报文。

长度字段(Length,2 字节):表示从单元标识开始到 PDU 结束的字节数,即“1 + PDU 长度”。注意它不包含 MBAP 头前 6 个字节。这是 TCP 流式传输下最重要的拆包依据:先读 6 字节拿到长度,再按长度读取剩余部分。

单元标识(Unit Identifier,1 字节):在纯 TCP 设备中通常为 0x00 或 0xFF;当使用 Modbus TCP 网关连接串行从站时,该字段用于标识网关后面的具体从站地址,作用等同于 RTU 中的从站地址。

PDU:功能码 + 数据域,与 Modbus RTU 完全一致。例如读保持寄存器 0x03,PDU 为“03 起始地址高 起始地址低 数量高 数量低”。

三、组包与拆包流程

驱动发送请求时,按以下顺序组包:分配事务号 → 写入协议标识 0x0000 → 计算长度 = 1 + PDU 长度 → 写入单元标识 → 追加 PDU → 通过 TCP 发送。接收响应时,先读满 6 字节,解析出长度字段,再读取“长度”个字节,最后按事务号匹配请求。

// 读保持寄存器请求:从站 1,起始地址 0x0000,数量 0x000A
// 事务号 0x0001
MBAP + PDU 字节序列:
00 01        // 事务标识 = 1
00 00        // 协议标识 = 0
00 06        // 长度 = 1(单元标识) + 5(PDU)
01           // 单元标识 = 1
03           // 功能码:读保持寄存器
00 00        // 起始地址
00 0A        // 寄存器数量

// 正常响应(10 个寄存器 = 20 字节数据)
00 01 00 00 00 17 01 03 14 [20 字节数据]
// 长度 0x0017 = 23 = 1 + 1(功能码) + 1(字节数) + 20(数据)

四、字段速查表

字段 长度 典型值 作用与开发要点
事务标识 2 字节 递增/随机 请求响应配对,需维护映射表
协议标识 2 字节 0x0000 非 0 直接丢弃
长度字段 2 字节 1 + PDU 长度 TCP 拆包核心依据
单元标识 1 字节 0x00 / 0xFF / 从站号 网关场景标识串行从站
PDU 可变 功能码 + 数据 与 RTU 的 PDU 完全一致

五、典型误区与复盘

误区一:把长度字段当成整个报文长度。长度字段只覆盖“单元标识 + PDU”,不包含 MBAP 前 6 字节。若按整包长度解析,会多读 6 字节导致后续报文错位。

误区二:忽略 TCP 粘包/半包。TCP 是字节流,一次 recv 可能收到半个报文或两个半报文。必须用“先读 6 字节 → 解析长度 → 再读剩余”的状态机方式处理,不能假设一次读取就是一个完整帧。

误区三:事务号固定不变。若始终使用同一事务号,在并发或超时重发场景下无法区分响应归属,容易把旧响应误配给新请求。

误区四:字节序错误。MBAP 中所有多字节字段均为大端序,若用主机小端序直接内存拷贝,事务号和长度都会解析错误。

调试建议:抓包时先看第 3、4 字节是否为 00 00,若不是则不是标准 Modbus TCP;再看第 5、6 字节长度是否等于实际剩余字节数,可快速定位拆包错误。用 Wireshark 过滤 tcp.port == 502 并启用 modbus 解析器,能直观看到各字段含义。

掌握 MBAP 头的字段语义与长度计算规则后,Modbus TCP 驱动的组包、拆包、事务匹配就有了明确依据。后续实现中,无论使用阻塞 socket、epoll 还是异步框架,核心逻辑都围绕“7 字节头 + 长度驱动读取 + 事务号匹配”这三件事展开。