第 7 章 Modbus TCP¶
Modbus TCP 是 Modbus PDU 直接跑在 TCP/IP 上的传输变体(Modbus Organization 文档:MODBUS Messaging on TCP/IP Implementation Guide V1.0b)。它把 RTU 里的"从站地址 + CRC16"换成"MBAP 头 + TCP 自身校验",其余 PDU 完全不变——这一"最小改动"哲学让 RTU 与 TCP 网关实现极薄,也让同一套应用逻辑可在两种传输间平移。机器人上 Modbus TCP 常用于慢变量跨网段接入:BMS、电力仪表、温湿度、环境传感器,通过 TCP 走到工控网络与上位。
TCP 侧去掉 CRC,网关把 RTU 设备接进来:
flowchart LR
cli["Modbus TCP 客户端"] --> mbap["MBAP 头 + PDU<br/>无 CRC(由 TCP 保证)"]
mbap --> srv["502 端口服务端"]
rtu["Modbus RTU 从站"] -.->|"串口网关转换"| srv
1. 从 RTU 到 TCP:映射与差异¶
RTU 与 TCP 的关系可以按"哪些字段换了、哪些保持不变"来记:
| 层 | Modbus RTU | Modbus TCP |
|---|---|---|
| PDU(协议层) | 功能码 + 数据 | 完全相同 |
| ADU 头 | Slave Addr(1 B) | MBAP 头(7 B) |
| ADU 尾 | CRC-16(2 B) | 无(依赖 TCP 校验) |
| 传输 | RS-485 半双工串口 | TCP,端口 502(IANA 注册) |
| 会话 | 一次事务一帧 | 长连接、pipelining 允许、多客户端并发 |
| 单元标识 | Slave Addr | MBAP 里的 Unit ID(可为网关侧的 RTU 从站地址) |
默认端口 502:/etc/services 里 modbus/tcp 502;实际部署常因权限(< 1024 需 root)或安全考虑改用 5020 / 50200 等,第 6 章「pymodbus 最小主从站示例」也用了 5020。
2. MBAP 头结构¶
MBAP(Modbus Application Protocol)头 7 字节,取代 RTU 的"从站地址 + CRC"两端字段。
┌───────────────────── ADU(TCP 完整帧)─────────────────────┐
│ MBAP Header (7 B) │ PDU (1–253 B) │
│ TID(2) │ PID(2) │ Length(2) │ Unit ID(1) │ FC + Data │
└────────────────────────────────────────────────────────────────┘
四个字段各有独立语义。
2.1 事务标识与协议标识¶
- Transaction Identifier(TID,2 字节):客户端自己选、服务端原样回;用来在同一 TCP 连接上把响应配对到请求。TCP 是字节流、没有消息边界,若客户端连续发出请求 A、B、C,响应可能以 A、B、C 顺序到达但也可能服务端交错返回;TID 让客户端从流里挑出属于自己某次请求的响应。
- Protocol Identifier(PID,2 字节):固定 0x0000,标识"这是 Modbus 报文"。保留给未来其他协议共存(同一端口复用);目前所有实现都发 0x0000。
2.2 长度字段与单元标识¶
- Length(2 字节):紧跟其后的字节数(Unit ID + PDU 全部),最大 253 + 1 = 254;TCP 分帧靠这个字段决定"一条完整 Modbus 报文有多长"。收方读满 MBAP 后按 Length 继续读,边界清晰。
- Unit Identifier(1 字节):取代 RTU 的 Slave Addr。当服务端本身就是从站时,Unit ID 一般填 0xFF 或 0x00(本端);当服务端是TCP → RTU 网关时,Unit ID 就是网关下挂的 RTU 从站地址(1–247),网关据此把请求转发到 RTU 段。
要点:MBAP 头没有 CRC——TCP 层已经有校验和、重传与按序保证,Modbus 层不再重复。这也是"Modbus TCP 有 CRC"这种误解的来源:帧里没有 CRC 字段,校验由 TCP 层完成。
3. 无 CRC 与 TCP 传输语义¶
Modbus 应用层不再关心位翻转、丢包与乱序——TCP 已经解决。工程后果有三:
- 响应丢失只能靠超时判定:TCP 保证"要么送达要么断开",但若服务端应用层卡住不回,客户端只能等到自己设的请求超时(1–10 s 常见);断开重连后 TID 空间需要重置。
- 服务端断开时 TCP 会立刻反映:
recv()返回 0 或抛ConnectionResetError;pymodbus上层会看到ModbusException。相比之下 RTU 上"从站没响应"和"从站响应错乱"混在超时里不易区分。 - 网络延迟叠加到 RTT:局域网内 TCP 一般 < 1 ms;跨广域网或经 VPN 时可到几十毫秒,直接放大到 Modbus 响应时延。RTU 主站轮询设计里的"每从站 100 ms"预算在 TCP 上要重新审视。
工程惯例:Modbus TCP 端口的长连接是常态,客户端不做"每次事务新建连接";服务端应容忍客户端长时间空连(keep-alive)与突然断开。pymodbus 与 libmodbus 都内建自动重连;重试节奏与从站侧的会话上限一起配置。
4. 并发多客户端与会话管理¶
Modbus TCP 允许一台从站同时对多个客户端:
- 服务端一般用多线程 / 异步事件循环,每客户端一个会话;不同客户端的请求可以在到达顺序上交错。
- 从站侧的写操作没有事务隔离:客户端 A 与 B 同时向 4x 空间某地址写不同值,最终值是最后到达服务端并被处理的那次;不存在"锁"或"版本"语义。工程上如需要并发写安全,由应用层设计(单一写主、其他只读;或用某几个寄存器做互信号)。
- 读并发无冲突:任意时刻读回的都是"从站当前值",不违反语义。
Modbus TCP 不支持多主。多个客户端并发访问只是"多个主站对一个从站",从站仍被动响应、不主动上报;跨从站的协调、事件通知、订阅推送都不在协议范围内;需要"多主"或"事件驱动"时应改用其他协议(OPC-UA、MQTT 桥接),事件告警则另开 SNMP trap 或邮件通道,与 Modbus 的主从模型无关。
5. 网关注册与 RTU / TCP 桥接¶
Modbus TCP 常见拓扑之一:上位机走 TCP、现场设备走 RTU,中间一台TCP / RTU 网关。
┌────────┐ TCP:502 ┌────────────┐ RS-485 (RTU) ┌────────┐┌────────┐┌────────┐
│ 上位机 │─────────────→│ Modbus │─────────────────→│ 从站 1 ││ 从站 2 ││ 从站 3 │
│(client)│ │ 网关 │ │ unit=1 ││ unit=2 ││ unit=3 │
└────────┘ └────────────┘ └────────┘└────────┘└────────┘
- 客户端在 MBAP 的 Unit ID 里填目标 RTU 从站地址(1–247),网关据此决定往 RTU 段发往哪个 Slave Addr。
- 网关侧同时是 TCP 服务器与 RTU 主站,收到 TCP 请求 → 剥 MBAP 头 → 保留 PDU → 拼 RTU ADU(加 Slave Addr + CRC)→ 串口发出;响应按反向流程处理。
- 网关不"缓存"数据(除非实现做了数据聚合);一次 TCP 请求对应一次 RTU 事务,RTT 相加。
工程坑:
- 网关串行化:网关对同一条 RTU 段的多个 TCP 客户端请求会串行发出(RTU 半双工),后到的请求要排队;若客户端按 TCP pipelining 快速发多条,网关前部积压到某阈值可能触发
0x06 SERVER DEVICE BUSY。控制客户端并发度、按 RTU 段的实际吞吐预算发。 - 广播语义:RTU 段广播(Unit ID = 0)通过网关时行为不一致——有的网关直接返回 ACK 而不等所有从站,有的转发但丢弃响应。工程上避免通过网关广播。
6. RTU over TCP 的另一种扩展¶
RTU over TCP(也叫 Modbus RTU Serial over TCP、"透传")与 Modbus TCP 不是同一个东西:
| 维度 | Modbus TCP | RTU over TCP |
|---|---|---|
| 头 | MBAP 7 B | 无(或自定义分隔) |
| 尾 | 无 | CRC-16 2 B |
| 帧内容 | 标准 PDU | 完整 RTU ADU 塞进 TCP |
| 分帧 | 靠 MBAP Length | 靠超时或长度约定 |
| 语义 | 原生 TCP | 串口透传 |
RTU over TCP 常见于串口服务器(如 Moxa NPort、有人 USR-TCP232)——设备本身只讲 RTU,串口服务器把 TCP 收到的字节直接透传到 RS-485,串口侧仍以 t3.5 静默分帧。协议层看是 Modbus RTU、传输层是 TCP;客户端需要按 RTU 帧格式打(含 Slave Addr + CRC)、并按长度或超时判断帧边界。
pymodbus 里 Modbus TCP 走 ModbusSocketFramer(FramerType.SOCKET),RTU over TCP 走 ModbusRtuFramer(FramerType.RTU);两者对帧头与帧尾的判定规则不同,选错会把 MBAP 头当数据或把 CRC 当载荷,响应解析随之失败。
判据:Modbus TCP 前 7 字节是 MBAP 头——TID(可任意)│ PID 00 00 │ Length │ Unit ID,功能码从第 8 字节起;RTU over TCP 没有 MBAP,功能码在第 2 字节、末尾 2 字节是 CRC,按 Slave Addr + PDU + CRC 解析。
7. pymodbus 与 Wireshark 抓包对照¶
以下示例展示一台 TCP 服务端与一台客户端之间的一次读取,并用 tshark 解出 MBAP 字段。运行环境:Ubuntu 22.04、Python 3.10、pymodbus 3.6、tshark 3.6;服务端与第 6 章「pymodbus 最小主从站示例」相同(略)。
发起一次读多个保持寄存器:
## 运行环境:Ubuntu 22.04(Python 3.10),pymodbus 3.6
import asyncio
from pymodbus.client import AsyncModbusTcpClient
async def one_read():
cli = AsyncModbusTcpClient("127.0.0.1", port=5020)
await cli.connect()
# 读 4x 空间起始 0、数量 4;unit=1(对应网关下的 1 号 RTU 从站,或本地从站)
rr = await cli.read_holding_registers(address=0, count=4, slave=1)
# rr.registers 为 4 个 16 位寄存器值,顺序与地址 0–3 对应
cli.close()
asyncio.run(one_read())
tshark 抓包与解码:
## 运行环境:Ubuntu 22.04,tshark 3.6.14
sudo tshark -i lo -Y "tcp.port == 5020 && modbus" -T fields -e hex -e modbus_mbap \
-e modbus.func_code -e modbus.read_write_addr -e modbus.read_write_qty
tshark 输出(一次 read_holding_registers 请求 + 响应):
00 01 00 00 00 06 01 03 00 00 00 04 0001..01 3 0 4
00 01 00 00 00 0b 01 03 08 00 00 00 00 00 00 00 00 0001..01 — — —
第一行请求:TID=0x0001、PID=0x0000、Length=0x0006、Unit ID=0x01、FC=0x03、Addr=0x0000、Qty=0x0004。第二行响应:TID 回显、Length=0x000B、Unit ID=0x01、FC=0x03、ByteCount=0x08、后 8 字节为 4 个寄存器的数据(本例全 0)。
对照要点:MBAP 的 Length 字段是"Unit ID + PDU 全部字节数"——响应里 Length=0x000B = 1(Unit ID)+ 1(FC)+ 1(ByteCount)+ 8(数据),共 11 字节,字段自洽。客户端把 Length 用来切包;TCP 分段落在 MBAP 中段的场景下靠 Length 补齐才是完整 Modbus 报文。
8. 要点¶
- Modbus TCP = Modbus PDU + MBAP 头:PDU 与 RTU 完全一致,MBAP 7 字节(TID / PID=0x0000 / Length / Unit ID)取代 Slave Addr 与 CRC;默认端口 502。
- TID 用于响应配对:长连接上多请求并发时,客户端靠 TID 从字节流里挑回自己的响应;PID 恒 0x0000,保留给未来协议共存。
- CRC 由 TCP 承担:帧内不再有 CRC;响应丢失只能靠超时判定,断开立即反映;跨广域网时 RTT 直接进入响应时延预算。
- 多客户端并发不等于多主:写并发无事务隔离、从站仍被动应答,仍是主从模型。需要事件驱动或跨从站协调请换协议(OPC-UA、MQTT 桥接)。
- Unit ID 承载网关下游地址:网关场景下 Unit ID = 目标 RTU Slave Addr;同一物理 TCP 连接可以访问多个下游从站,网关做 PDU 转发与帧格式转换。
- RTU over TCP ≠ Modbus TCP:串口透传方案里 ADU 仍是完整 RTU(含 Slave Addr 与 CRC),MBAP 缺席;两种 framer 按 MBAP 头与 CRC 尾的差异区分,不可混用。