4. Modbus TCP协议详解

Modbus TCP 是 Modbus 协议在 TCP/IP 网络上的实现,它去掉了 RTU 模式中的 CRC 校验,增加了 MBAP 报文头,使得数据可以在以太网上高效、可靠地传输。本章将深入解析 Modbus TCP 的帧结构、MBAP 报文头、与 RTU 的关键差异以及端口 502 的由来。

4.1 Modbus TCP 帧结构

Modbus TCP 的报文由两部分组成:MBAP 报文头和协议数据单元(PDU)。PDU 部分与 RTU 模式中的功能码和数据区完全一致,但不再包含地址码和 CRC 校验。

核心公式: Modbus TCP 报文 = MBAP 报文头(7字节) + 功能码(1字节) + 数据(N字节)

下图展示了 Modbus TCP 报文的完整封装结构,从应用层到物理层的映射关系:

Modbus TCP 报文封装结构 应用层:Modbus TCP 报文 MBAP 报文头(7字节) PDU(功能码 + 数据) 传输层:TCP 段(源端口 502 / 目的端口 502) 网络层:IP 数据报 链路层:以太网帧

4.2 MBAP 报文头详解

MBAP(Modbus Application Protocol)报文头是 Modbus TCP 区别于 RTU 的核心所在,长度为固定的 7 字节,包含以下四个字段:

字段 长度(字节) 描述 取值范围
事务处理标识符 2 用于请求/响应的配对,客户端每发送一个新请求,该值递增 0x0000 ~ 0xFFFF
协议标识符 2 固定为 0x0000,表示 Modbus 协议 0x0000
长度 2 后续字节数(功能码 + 数据的总长度) 0x0001 ~ 0xFFFF
单元标识符 1 用于标识从站设备地址,类似 RTU 中的从站地址 0x00 ~ 0xFF
实战提示: 事务处理标识符是调试 Modbus TCP 通信的关键。当客户端同时发送多个请求时,通过事务处理标识符可以正确匹配每个请求对应的响应。在 Python 中使用 pymodbus 库时,该字段由库自动管理,但理解其原理有助于排查通信乱序问题。

下图展示了 MBAP 报文头在 TCP 报文中的具体位置和字节序:

MBAP 报文头字节布局(大端序) 事务处理 标识符(2B) 协议 标识符(2B) 长度 (2B) 单元 标识符 功能码 (1B) 数据 (N字节) 字节偏移:0 2 4 6 7 8 长度字段 = 后续字节数 = 1(功能码)+ N(数据),不包含 MBAP 头本身的 7 字节

4.3 Modbus TCP 与 RTU 的差异

Modbus TCP 和 RTU 虽然共享相同的 PDU 结构,但在物理层、数据链路层和应用层存在显著差异。下表总结了关键区别:

对比项 Modbus RTU Modbus TCP
物理层 RS-232 / RS-485 串行总线 以太网(RJ45、光纤)
传输方式 半双工,主从轮询 全双工,客户端/服务器
从站地址 报文第1字节,1字节 MBAP 头的单元标识符,1字节
校验方式 CRC-16(2字节),报文末尾 无应用层校验,依赖 TCP 校验和
报文头 无,直接以地址码开始 MBAP 报文头(7字节)
最大报文长度 256 字节(含地址和 CRC) 260 字节(含 MBAP 头)
默认端口 不适用 502
通信模式 一主多从,广播 多客户端可同时连接同一服务器
常见误区: 很多初学者认为 Modbus TCP 比 RTU 快,实际上在相同波特率下,TCP 由于 TCP/IP 协议栈的开销,单次事务的延迟可能高于 RTU。TCP 的优势在于高带宽(100Mbps/1Gbps)和并发连接能力,而非单次响应速度。

下图直观对比了 RTU 和 TCP 两种模式的报文结构差异:

Modbus RTU 与 TCP 报文结构对比 RTU 报文: 地址码 功能码 数据区(N字节) CRC-16(2B) TCP 报文: MBAP 报文头(7B) 功能码 数据区(N字节) RTU 有地址码和 CRC TCP 用 MBAP 头替代地址码,无 CRC RTU 的地址码对应 TCP 的单元标识符;RTU 的 CRC 由 TCP/IP 协议栈的校验和替代

4.4 端口 502 的由来与意义

端口 502 是 IANA(互联网号码分配局)为 Modbus TCP 协议分配的系统端口。选择 502 的原因和历史背景如下:

  • 标准化需求: 在 Modbus TCP 推出之前,各厂商使用不同的端口号,导致互操作性差。IANA 在 1999 年正式分配 502 端口给 Modbus TCP。
  • 低于 1024 的保留端口: 502 属于系统保留端口(0-1023),在 Linux/Unix 系统中需要 root 权限才能绑定,这在一定程度上保证了安全性。
  • 防火墙友好: 固定端口使得网络管理员可以精确控制 Modbus TCP 流量,在防火墙上只开放 502 端口即可。
端口扫描安全提示: 在厂务网络中,502 端口是攻击者的主要目标之一。建议通过 VLAN 隔离、ACL 访问控制列表和端口认证等方式,限制只有授权的客户端才能访问 502 端口。

4.5 Python 实战:解析 Modbus TCP 报文

以下代码演示如何使用 Python 的 struct 模块手动解析一个 Modbus TCP 请求报文,理解 MBAP 头的各个字段:

import struct

# 模拟一个 Modbus TCP 请求报文(读取保持寄存器,起始地址 0,数量 10)
# MBAP头: 事务ID=0x0001, 协议ID=0x0000, 长度=0x0006, 单元ID=0x01
# PDU: 功能码=0x03, 起始地址=0x0000, 寄存器数量=0x000A
raw_tcp_frame = bytes([
    0x00, 0x01,  # 事务处理标识符
    0x00, 0x00,  # 协议标识符
    0x00, 0x06,  # 长度(后续6字节)
    0x01,        # 单元标识符
    0x03,        # 功能码:读保持寄存器
    0x00, 0x00,  # 起始地址
    0x00, 0x0A   # 寄存器数量
])

# 解析 MBAP 头
mbap_header = raw_tcp_frame[:7]
transaction_id, protocol_id, length, unit_id = struct.unpack('!HHHB', mbap_header)

print("=== Modbus TCP 报文解析 ===")
print(f"事务处理标识符: 0x{transaction_id:04X}")
print(f"协议标识符:     0x{protocol_id:04X} (应为 0x0000)")
print(f"长度:           {length} 字节 (后续 PDU 长度)")
print(f"单元标识符:     0x{unit_id:02X}")

# 解析 PDU
pdu = raw_tcp_frame[7:]
function_code = pdu[0]
start_address = struct.unpack('!H', pdu[1:3])[0]
quantity = struct.unpack('!H', pdu[3:5])[0]

print(f"\n功能码:         0x{function_code:02X} (读保持寄存器)")
print(f"起始地址:       {start_address}")
print(f"寄存器数量:     {quantity}")
print(f"报文总长度:     {len(raw_tcp_frame)} 字节")

运行上述代码,输出结果如下:

=== Modbus TCP 报文解析 ===
事务处理标识符: 0x0001
协议标识符:     0x0000 (应为 0x0000)
长度:           6 字节 (后续 PDU 长度)
单元标识符:     0x01

功能码:         0x03 (读保持寄存器)
起始地址:       0
寄存器数量:     10
报文总长度:     13 字节
调试技巧: 使用 Wireshark 抓包时,设置过滤条件 tcp.port == 502 可以快速筛选出所有 Modbus TCP 报文。Wireshark 会自动解析 MBAP 头,显示事务 ID、协议 ID、长度和单元 ID 等字段,是学习 Modbus TCP 协议的最佳工具。

4.6 本章小结

Modbus TCP 通过 MBAP 报文头实现了在 TCP/IP 网络上的标准化传输,其核心要点包括:

  • 帧结构: MBAP 头(7字节)+ PDU(功能码+数据),总长度不超过 260 字节。
  • MBAP 头: 包含事务处理标识符、协议标识符、长度和单元标识符四个字段,其中事务处理标识符用于请求/响应配对。
  • 与 RTU 差异: TCP 去掉了地址码和 CRC,增加了 MBAP 头;物理层从串行总线变为以太网;通信模式从主从轮询变为客户端/服务器。
  • 端口 502: IANA 分配的标准化端口,便于防火墙管理和网络隔离。